|
|
|
// Package homebrew implements the Home Brew DMR IPSC protocol
|
|
|
|
package homebrew
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/binary"
|
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/tehmaze/go-dmr"
|
|
|
|
)
|
|
|
|
|
|
|
|
var logger *log.Logger
|
|
|
|
|
|
|
|
type AuthStatus uint8
|
|
|
|
|
|
|
|
const (
|
|
|
|
AuthNone AuthStatus = iota
|
|
|
|
AuthBegin
|
|
|
|
AuthDone
|
|
|
|
AuthFailed
|
|
|
|
)
|
|
|
|
|
|
|
|
// Messages as documented by DL5DI, G4KLX and DG1HT, see "DMRplus IPSC Protocol for HB repeater (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")
|
|
|
|
)
|
|
|
|
|
|
|
|
// We ping the peers every minute
|
|
|
|
var (
|
|
|
|
AuthTimeout = time.Second * 90
|
|
|
|
PingInterval = time.Minute
|
|
|
|
PingTimeout = time.Second * 150
|
|
|
|
RepeaterConfigurationInterval = time.Minute * 5
|
|
|
|
)
|
|
|
|
|
|
|
|
// Peer is a remote repeater that also speaks the Homebrew protocol
|
|
|
|
type Peer struct {
|
|
|
|
ID uint32
|
|
|
|
Addr *net.UDPAddr
|
|
|
|
AuthKey []byte
|
|
|
|
Status AuthStatus
|
|
|
|
Nonce []byte
|
|
|
|
Token []byte
|
|
|
|
Incoming bool
|
|
|
|
UnlinkOnAuthFailure bool
|
|
|
|
PacketReceived dmr.PacketFunc
|
|
|
|
Last struct {
|
|
|
|
PacketSent time.Time
|
|
|
|
PacketReceived time.Time
|
|
|
|
PingSent time.Time
|
|
|
|
PongReceived time.Time
|
|
|
|
RepeaterConfigurationSent time.Time
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Peer) UpdateToken(nonce []byte) {
|
|
|
|
p.Nonce = nonce
|
|
|
|
hash := sha256.New()
|
|
|
|
hash.Write(p.Nonce)
|
|
|
|
hash.Write(p.AuthKey)
|
|
|
|
p.Token = []byte(hex.EncodeToString(hash.Sum(nil)))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Homebrew is implements the Homebrew IPSC DMR Air Interface protocol
|
|
|
|
type Homebrew struct {
|
|
|
|
Config *RepeaterConfiguration
|
|
|
|
Peer map[string]*Peer
|
|
|
|
PeerID map[uint32]*Peer
|
|
|
|
PacketReceived dmr.PacketFunc
|
|
|
|
|
|
|
|
conn *net.UDPConn
|
|
|
|
closed bool
|
|
|
|
id []byte
|
|
|
|
mutex *sync.Mutex
|
|
|
|
stop chan bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// New creates a new Homebrew repeater
|
|
|
|
func New(config *RepeaterConfiguration, addr *net.UDPAddr) (*Homebrew, error) {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
if config == nil {
|
|
|
|
return nil, errors.New("homebrew: RepeaterConfiguration can't be nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
h := &Homebrew{
|
|
|
|
Config: config,
|
|
|
|
Peer: make(map[string]*Peer),
|
|
|
|
PeerID: make(map[uint32]*Peer),
|
|
|
|
id: []byte(fmt.Sprintf("%08x", config.ID)),
|
|
|
|
mutex: &sync.Mutex{},
|
|
|
|
}
|
|
|
|
if h.conn, err = net.ListenUDP("udp", addr); err != nil {
|
|
|
|
return nil, errors.New("homebrew: " + err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
return h, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) Active() bool {
|
|
|
|
return !h.closed && h.conn != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close stops the active listeners
|
|
|
|
func (h *Homebrew) Close() error {
|
|
|
|
h.mutex.Lock()
|
|
|
|
defer h.mutex.Unlock()
|
|
|
|
|
|
|
|
if !h.Active() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Kill keepalive goroutine
|
|
|
|
if h.stop != nil {
|
|
|
|
close(h.stop)
|
|
|
|
h.stop = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Kill listening socket
|
|
|
|
h.closed = true
|
|
|
|
return h.conn.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Link establishes a new link with a peer
|
|
|
|
func (h *Homebrew) Link(peer *Peer) error {
|
|
|
|
if peer.Addr == nil {
|
|
|
|
return errors.New("homebrew: peer Addr can't be nil")
|
|
|
|
}
|
|
|
|
if peer.AuthKey == nil || len(peer.AuthKey) == 0 {
|
|
|
|
return errors.New("homebrew: peer AuthKey can't be nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
h.mutex.Lock()
|
|
|
|
defer h.mutex.Unlock()
|
|
|
|
|
|
|
|
// Reset state
|
|
|
|
peer.Last.PacketSent = time.Time{}
|
|
|
|
peer.Last.PacketReceived = time.Time{}
|
|
|
|
peer.Last.PingSent = time.Time{}
|
|
|
|
peer.Last.PongReceived = time.Time{}
|
|
|
|
|
|
|
|
// Register our peer
|
|
|
|
h.Peer[peer.Addr.String()] = peer
|
|
|
|
h.PeerID[peer.ID] = peer
|
|
|
|
|
|
|
|
return h.handleAuth(peer)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) Unlink(id uint32) error {
|
|
|
|
h.mutex.Lock()
|
|
|
|
defer h.mutex.Unlock()
|
|
|
|
|
|
|
|
peer, ok := h.PeerID[id]
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("homebrew: peer %d not linked", id)
|
|
|
|
}
|
|
|
|
|
|
|
|
delete(h.Peer, peer.Addr.String())
|
|
|
|
delete(h.PeerID, id)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) ListenAndServe() error {
|
|
|
|
var data = make([]byte, 53)
|
|
|
|
|
|
|
|
h.stop = make(chan bool)
|
|
|
|
go h.keepalive(h.stop)
|
|
|
|
|
|
|
|
h.closed = false
|
|
|
|
for !h.closed {
|
|
|
|
n, peer, err := h.conn.ReadFromUDP(data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := h.handle(peer, data[:n]); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) WritePacketToPeer(p *dmr.Packet, peer *Peer) error {
|
|
|
|
return h.WriteToPeer(h.parsePacket(p), peer)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) WriteToPeer(b []byte, peer *Peer) error {
|
|
|
|
if peer == nil {
|
|
|
|
return errors.New("homebrew: can't write to nil peer")
|
|
|
|
}
|
|
|
|
|
|
|
|
peer.Last.PacketSent = time.Now()
|
|
|
|
_, err := h.conn.WriteTo(b, peer.Addr)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) WriteToPeerWithID(b []byte, id uint32) error {
|
|
|
|
return h.WriteToPeer(b, h.getPeer(id))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) getPeer(id uint32) *Peer {
|
|
|
|
h.mutex.Lock()
|
|
|
|
defer h.mutex.Unlock()
|
|
|
|
|
|
|
|
if peer, ok := h.PeerID[id]; ok {
|
|
|
|
return peer
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) getPeerByAddr(addr *net.UDPAddr) *Peer {
|
|
|
|
h.mutex.Lock()
|
|
|
|
defer h.mutex.Unlock()
|
|
|
|
|
|
|
|
if peer, ok := h.Peer[addr.String()]; ok {
|
|
|
|
return peer
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) getPeers() []*Peer {
|
|
|
|
h.mutex.Lock()
|
|
|
|
defer h.mutex.Unlock()
|
|
|
|
|
|
|
|
var peers = make([]*Peer, 0)
|
|
|
|
for _, peer := range h.Peer {
|
|
|
|
peers = append(peers, peer)
|
|
|
|
}
|
|
|
|
|
|
|
|
return peers
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) handle(remote *net.UDPAddr, data []byte) error {
|
|
|
|
peer := h.getPeerByAddr(remote)
|
|
|
|
if peer == nil {
|
|
|
|
logger.Printf("ignored packet from unknown peer %s\n", remote)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ignore packet that are clearly invalid, this is the minimum packet length for any Homebrew protocol frame
|
|
|
|
if len(data) < 14 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if peer.Status != AuthDone {
|
|
|
|
// Ignore DMR data at this stage
|
|
|
|
if bytes.Equal(data[:4], DMRData) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if peer.Incoming {
|
|
|
|
// Verify we have a matching peer ID
|
|
|
|
id, err := h.parseRepeaterID(data[4:])
|
|
|
|
if err != nil {
|
|
|
|
logger.Printf("peer %d@%s sent invalid repeater ID (ignored)\n", peer.ID, remote)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
var ok = id == peer.ID
|
|
|
|
if !ok {
|
|
|
|
logger.Printf("peer %d@%s sent unexpected repeater ID %d\n", peer.ID, remote, id)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch peer.Status {
|
|
|
|
case AuthNone:
|
|
|
|
switch {
|
|
|
|
case bytes.Equal(data[:4], RepeaterLogin):
|
|
|
|
if !ok {
|
|
|
|
return h.WriteToPeer(append(MasterNAK, h.id...), peer)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Peer is verified, generate a nonce
|
|
|
|
nonce := make([]byte, 4)
|
|
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
|
|
logger.Printf("peer %d@%s nonce generation failed: %v\n", peer.ID, remote, err)
|
|
|
|
return h.WriteToPeer(append(MasterNAK, h.id...), peer)
|
|
|
|
}
|
|
|
|
|
|
|
|
peer.UpdateToken(nonce)
|
|
|
|
peer.Status = AuthBegin
|
|
|
|
return h.WriteToPeer(append(append(MasterACK, h.id...), nonce...), peer)
|
|
|
|
|
|
|
|
default:
|
|
|
|
// Ignore unauthenticated repeater, we're not going to reply unless it's
|
|
|
|
// an actual login request; if it was indeed a valid repeater and we missed
|
|
|
|
// anything, we rely on the remote end to retry to reconnect if it doesn't
|
|
|
|
// get an answer in a timely manner.
|
|
|
|
break
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case AuthBegin:
|
|
|
|
switch {
|
|
|
|
case bytes.Equal(data[:4], RepeaterKey):
|
|
|
|
if ok && len(data) != 76 {
|
|
|
|
logger.Printf("peer %d@%s sent invalid key challenge length of %d\n", peer.ID, remote, len(data))
|
|
|
|
ok = false
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
peer.Status = AuthNone
|
|
|
|
return h.WriteToPeer(append(MasterNAK, h.id...), peer)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !bytes.Equal(data[12:], peer.Token) {
|
|
|
|
logger.Printf("peer %d@%s sent invalid key challenge token\n", peer.ID, remote)
|
|
|
|
peer.Status = AuthNone
|
|
|
|
return h.WriteToPeer(append(MasterNAK, h.id...), peer)
|
|
|
|
}
|
|
|
|
|
|
|
|
peer.Status = AuthDone
|
|
|
|
return h.WriteToPeer(append(MasterACK, h.id...), peer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Verify we have a matching peer ID
|
|
|
|
id, err := h.parseRepeaterID(data[6:14])
|
|
|
|
if err != nil {
|
|
|
|
logger.Printf("peer %d@%s sent invalid repeater ID (ignored)\n", peer.ID, remote)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if id != peer.ID {
|
|
|
|
logger.Printf("peer %d@%s sent unexpected repeater ID %d (ignored)\n", peer.ID, remote, id)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
switch peer.Status {
|
|
|
|
case AuthNone:
|
|
|
|
switch {
|
|
|
|
case bytes.Equal(data[:6], MasterACK):
|
|
|
|
logger.Printf("peer %d@%s sent nonce\n", peer.ID, remote)
|
|
|
|
peer.Status = AuthBegin
|
|
|
|
peer.UpdateToken(data[14:])
|
|
|
|
return h.handleAuth(peer)
|
|
|
|
|
|
|
|
case bytes.Equal(data[:6], MasterNAK):
|
|
|
|
logger.Printf("peer %d@%s refused login\n", peer.ID, remote)
|
|
|
|
peer.Status = AuthFailed
|
|
|
|
if peer.UnlinkOnAuthFailure {
|
|
|
|
h.Unlink(peer.ID)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
default:
|
|
|
|
logger.Printf("peer %d@%s sent unexpected login reply (ignored)\n", peer.ID, remote)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
case AuthBegin:
|
|
|
|
switch {
|
|
|
|
case bytes.Equal(data[:6], MasterACK):
|
|
|
|
logger.Printf("peer %d@%s accepted login\n", peer.ID, remote)
|
|
|
|
peer.Status = AuthDone
|
|
|
|
peer.Last.RepeaterConfigurationSent = time.Now()
|
|
|
|
return h.WriteToPeer(h.Config.Bytes(), peer)
|
|
|
|
|
|
|
|
case bytes.Equal(data[:6], MasterNAK):
|
|
|
|
logger.Printf("peer %d@%s refused login\n", peer.ID, remote)
|
|
|
|
peer.Status = AuthFailed
|
|
|
|
if peer.UnlinkOnAuthFailure {
|
|
|
|
h.Unlink(peer.ID)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
default:
|
|
|
|
logger.Printf("peer %d@%s sent unexpected login reply (ignored)\n", peer.ID, remote)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Authentication is done
|
|
|
|
switch {
|
|
|
|
case bytes.Equal(data[:4], DMRData):
|
|
|
|
p, err := h.parseData(data[4:])
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return h.handlePacket(p, peer)
|
|
|
|
|
|
|
|
case peer.Incoming && len(data) == 15 && bytes.Equal(data[:7], MasterPing):
|
|
|
|
// Verify we have a matching peer ID
|
|
|
|
id, err := h.parseRepeaterID(data[7:])
|
|
|
|
if err != nil {
|
|
|
|
logger.Printf("peer %d@%s sent invalid repeater ID (ignored)\n", peer.ID, remote)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if id != peer.ID {
|
|
|
|
logger.Printf("peer %d@%s sent unexpected repeater ID %d (ignored)\n", peer.ID, remote, id)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return h.WriteToPeer(append(RepeaterPong, h.id...), peer)
|
|
|
|
|
|
|
|
default:
|
|
|
|
logger.Printf("peer %d@%s sent unexpected packet\n", peer.ID, remote)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) handleAuth(peer *Peer) error {
|
|
|
|
if !peer.Incoming {
|
|
|
|
switch peer.Status {
|
|
|
|
case AuthNone:
|
|
|
|
// Send login packet
|
|
|
|
return h.WriteToPeer(append(RepeaterLogin, h.id...), peer)
|
|
|
|
|
|
|
|
case AuthBegin:
|
|
|
|
// Send repeater key exchange packet
|
|
|
|
return h.WriteToPeer(append(append(RepeaterKey, h.id...), peer.Token...), peer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) handlePacket(p *dmr.Packet, peer *Peer) error {
|
|
|
|
if peer.PacketReceived != nil {
|
|
|
|
return peer.PacketReceived(h, p)
|
|
|
|
}
|
|
|
|
if h.PacketReceived == nil {
|
|
|
|
return errors.New("homebrew: no PacketReceived func defined to handle DMR packet")
|
|
|
|
}
|
|
|
|
return h.PacketReceived(h, p)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) keepalive(stop <-chan bool) {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
for _, peer := range h.getPeers() {
|
|
|
|
// Ping protocol only applies to outgoing links, and also the auth retries
|
|
|
|
// are entirely up to the peer.
|
|
|
|
if peer.Incoming {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
switch peer.Status {
|
|
|
|
case AuthNone, AuthBegin:
|
|
|
|
switch {
|
|
|
|
case now.Sub(peer.Last.PacketReceived) > AuthTimeout:
|
|
|
|
logger.Printf("peer %d@%s not responding to login; retrying\n", peer.ID, peer.Addr)
|
|
|
|
if err := h.handleAuth(peer); err != nil {
|
|
|
|
logger.Printf("peer %d@%s retry failed: %v\n", peer.ID, peer.Addr, err)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
case AuthDone:
|
|
|
|
switch {
|
|
|
|
case now.Sub(peer.Last.PongReceived) > PingTimeout:
|
|
|
|
peer.Status = AuthNone
|
|
|
|
logger.Printf("peer %d@%s not responding to ping; trying to re-establish connection", peer.ID, peer.Addr)
|
|
|
|
if err := h.handleAuth(peer); err != nil {
|
|
|
|
logger.Printf("peer %d@%s retry failed: %v\n", peer.ID, peer.Addr, err)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case now.Sub(peer.Last.PingSent) > PingInterval:
|
|
|
|
peer.Last.PingSent = now
|
|
|
|
if err := h.WriteToPeer(append(MasterPing, h.id...), peer); err != nil {
|
|
|
|
logger.Printf("peer %d@%s ping failed: %v\n", peer.ID, peer.Addr, err)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case now.Sub(peer.Last.RepeaterConfigurationSent) > RepeaterConfigurationInterval:
|
|
|
|
peer.Last.RepeaterConfigurationSent = time.Now()
|
|
|
|
if err := h.WriteToPeer(h.Config.Bytes(), peer); err != nil {
|
|
|
|
logger.Printf("peer %d@%s repeater configuration failed: %v\n", peer.ID, peer.Addr, err)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
case <-stop:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseData converts Homebrew packet format to DMR packet format
|
|
|
|
func (h *Homebrew) parseData(data []byte) (*dmr.Packet, error) {
|
|
|
|
if len(data) != 53 {
|
|
|
|
return nil, fmt.Errorf("homebrew: expected 53 data bytes, got %d", len(data))
|
|
|
|
}
|
|
|
|
|
|
|
|
var p = &dmr.Packet{
|
|
|
|
Sequence: data[4],
|
|
|
|
SrcID: uint32(data[5])<<16 | uint32(data[6])<<8 | uint32(data[7]),
|
|
|
|
DstID: uint32(data[8])<<16 | uint32(data[9])<<8 | uint32(data[10]),
|
|
|
|
}
|
|
|
|
p.SetData(data[20:])
|
|
|
|
|
|
|
|
return p, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// parsePacket converts DMR packet format to Homebrew packet format suitable for sending on the wire
|
|
|
|
func (h *Homebrew) parsePacket(p *dmr.Packet) []byte {
|
|
|
|
var d = make([]byte, 53)
|
|
|
|
|
|
|
|
// Signature, 4 bytes, "DMRD"
|
|
|
|
copy(d[0:], DMRData)
|
|
|
|
|
|
|
|
// Seq No, 1 byte
|
|
|
|
d[4] = p.Sequence
|
|
|
|
|
|
|
|
// Src ID, 3 bytes
|
|
|
|
d[5] = uint8((p.SrcID >> 16) & 0xff)
|
|
|
|
d[6] = uint8((p.SrcID >> 8) & 0xff)
|
|
|
|
d[7] = uint8((p.SrcID & 0xff))
|
|
|
|
|
|
|
|
// Dst ID, 3 bytes
|
|
|
|
d[8] = uint8((p.DstID >> 16) & 0xff)
|
|
|
|
d[9] = uint8((p.DstID >> 8) & 0xff)
|
|
|
|
d[10] = uint8((p.DstID & 0xff))
|
|
|
|
|
|
|
|
// RptrID, 4 bytes
|
|
|
|
binary.LittleEndian.PutUint32(d[11:], p.RepeaterID)
|
|
|
|
|
|
|
|
var s byte
|
|
|
|
s |= (p.Timeslot & 0x01) // Slot no, 1 bit
|
|
|
|
s |= (p.CallType & 0x01) << 1 // Call Type, 1 bit
|
|
|
|
switch p.DataType { // Frame Type, 2 bits and Data Type or Voice Seq, 4 bits
|
|
|
|
case dmr.VoiceBurstA:
|
|
|
|
s |= 0x01 << 2 // Voice sync
|
|
|
|
s |= (p.DataType - dmr.VoiceBurstA) << 4
|
|
|
|
case dmr.VoiceBurstB, dmr.VoiceBurstC, dmr.VoiceBurstD, dmr.VoiceBurstE, dmr.VoiceBurstF:
|
|
|
|
s |= 0x00 << 2 // Voice (no-op)
|
|
|
|
s |= (p.DataType - dmr.VoiceBurstA) << 4
|
|
|
|
default:
|
|
|
|
s |= 0x02 << 2 // Data
|
|
|
|
s |= p.DataType << 4
|
|
|
|
}
|
|
|
|
|
|
|
|
// StreamID, 4 bytes
|
|
|
|
binary.BigEndian.PutUint32(d[16:], p.StreamID)
|
|
|
|
|
|
|
|
// DMR Data, 33 bytes
|
|
|
|
copy(d[20:], p.Data)
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Homebrew) parseRepeaterID(data []byte) (uint32, error) {
|
|
|
|
id, err := strconv.ParseUint(string(data), 10, 32)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return uint32(id), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Interface compliance check
|
|
|
|
var _ dmr.Repeater = (*Homebrew)(nil)
|
|
|
|
|
|
|
|
// UpdateLogger replaces the package logger.
|
|
|
|
func UpdateLogger(l *log.Logger) {
|
|
|
|
logger = l
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
UpdateLogger(log.New(os.Stderr, "dmr/homebrew: ", log.LstdFlags))
|
|
|
|
}
|