diff --git a/bits.go b/bits.go index cc4dd92..98eee24 100644 --- a/bits.go +++ b/bits.go @@ -1,10 +1,11 @@ package dmr +// Various sizes of information chunks. const ( PayloadBits = 98 + 10 + 48 + 10 + 98 - PayloadSize = 33 InfoHalfBits = 98 InfoBits = 2 * InfoHalfBits + InfoSize = 12 // After BPTC(196, 96) decoding SlotTypeHalfBits = 10 SlotTypeBits = 2 * SlotTypeHalfBits SignalBits = 48 diff --git a/bptc/bptc.go b/bptc/bptc.go index 25c1ea2..4d5d32e 100644 --- a/bptc/bptc.go +++ b/bptc/bptc.go @@ -3,11 +3,11 @@ package bptc import ( "fmt" - "github.com/tehmaze/go-dmr" - "github.com/tehmaze/go-dmr/fec" + "github.com/pd0mz/go-dmr" + "github.com/pd0mz/go-dmr/fec" ) -func Process(info []byte, payload []byte) error { +func Decode(info []byte, payload []byte) error { if len(info) < 196 { return fmt.Errorf("bptc: info size %d too small, need at least 196 bits", len(info)) } diff --git a/controlblock.go b/controlblock.go new file mode 100644 index 0000000..7244bcc --- /dev/null +++ b/controlblock.go @@ -0,0 +1,206 @@ +package dmr + +import ( + "errors" + "fmt" +) + +// Control Block Options +const ( + CBSKOOutboundActivation = B00111000 + CBSKOUnitToUnitVoiceServiceRequest = B00000100 + CBSKOUnitToUnitVoiceServiceAnswerResponse = B00000101 + CBSKONegativeAcknowledgeResponse = B00100100 + CBSKOPreamble = B00111101 +) + +type ControlBlock struct { + Last bool + CBSKO uint8 + SrcID, DstID uint32 + Data ControlBlockData +} + +type ControlBlockData interface { + Write([]byte) error + Parse([]byte) error +} + +type OutboundActivation struct{} + +func (d *OutboundActivation) Parse(data []byte) error { + if len(data) != InfoSize { + return fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + return nil +} + +func (d *OutboundActivation) Write(data []byte) error { + if len(data) != InfoSize { + return fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + data[0] |= B00111000 + return nil +} + +type UnitToUnitVoiceServiceRequest struct { + Options uint8 +} + +func (d *UnitToUnitVoiceServiceRequest) Parse(data []byte) error { + if len(data) != InfoSize { + return fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + d.Options = data[2] + return nil +} + +func (d *UnitToUnitVoiceServiceRequest) Write(data []byte) error { + if len(data) != InfoSize { + return fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + data[0] |= B00000100 + data[2] = d.Options + return nil +} + +var _ (ControlBlockData) = (*UnitToUnitVoiceServiceRequest)(nil) + +type UnitToUnitVoiceServiceAnswerResponse struct { + Options uint8 + Response uint8 +} + +func (d *UnitToUnitVoiceServiceAnswerResponse) Parse(data []byte) error { + if len(data) != InfoSize { + return fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + d.Options = data[2] + d.Response = data[3] + return nil +} + +func (d *UnitToUnitVoiceServiceAnswerResponse) Write(data []byte) error { + if len(data) != InfoSize { + return fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + data[0] |= B00000101 + data[2] = d.Options + data[3] = d.Response + return nil +} + +var _ (ControlBlockData) = (*UnitToUnitVoiceServiceAnswerResponse)(nil) + +type NegativeAcknowledgeResponse struct { + SourceType bool + ServiceType uint8 + Reason uint8 +} + +func (d *NegativeAcknowledgeResponse) Parse(data []byte) error { + if len(data) != InfoSize { + return fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + d.SourceType = (data[2] & B01000000) > 0 + d.ServiceType = (data[2] & B00011111) + d.Reason = data[3] + return nil +} + +func (d *NegativeAcknowledgeResponse) Write(data []byte) error { + if len(data) != InfoSize { + return fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + data[0] |= B00100110 + data[2] = d.ServiceType + if d.SourceType { + data[2] |= B01000000 + } + data[3] = d.Reason + return nil +} + +var _ (ControlBlockData) = (*NegativeAcknowledgeResponse)(nil) + +type ControlBlockPreamble struct { + DataFollows bool + DstIsGroup bool + Blocks uint8 +} + +func (d *ControlBlockPreamble) Parse(data []byte) error { + if len(data) != InfoSize { + return fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + d.DataFollows = (data[2] & B10000000) > 0 + d.DstIsGroup = (data[2] & B01000000) > 0 + d.Blocks = data[3] + return nil +} + +func (d *ControlBlockPreamble) Write(data []byte) error { + if len(data) != InfoSize { + return fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + data[0] |= B00100110 + if d.DataFollows { + data[2] |= B10000000 + } + if d.DstIsGroup { + data[2] |= B01000000 + } + data[3] = d.Blocks + return nil +} + +var _ (ControlBlockData) = (*ControlBlockPreamble)(nil) + +func ParseControlBlock(data []byte) (*ControlBlock, error) { + if len(data) != InfoSize { + return nil, fmt.Errorf("dmr: expected %d info bytes, got %d", InfoSize, len(data)) + } + + // Calculate CRC16 + + // Check packet + if data[0]&B01000000 > 0 { + return nil, errors.New("dmr: CBSK protect flag is set") + } + if data[1] != 0 { + return nil, errors.New("dmr: CBSK feature set ID is set") + } + + cb := &ControlBlock{ + Last: (data[0] & B10000000) > 0, + CBSKO: (data[0] & B00111111), + DstID: uint32(data[4])<<16 | uint32(data[5])<<8 | uint32(data[6]), + SrcID: uint32(data[7])<<16 | uint32(data[8])<<8 | uint32(data[9]), + } + + switch cb.CBSKO { + case CBSKOOutboundActivation: + cb.Data = &OutboundActivation{} + break + case CBSKOUnitToUnitVoiceServiceRequest: + cb.Data = &UnitToUnitVoiceServiceRequest{} + break + case CBSKOUnitToUnitVoiceServiceAnswerResponse: + cb.Data = &UnitToUnitVoiceServiceAnswerResponse{} + break + case CBSKONegativeAcknowledgeResponse: + cb.Data = &NegativeAcknowledgeResponse{} + break + case CBSKOPreamble: + cb.Data = &ControlBlockPreamble{} + break + default: + return nil, fmt.Errorf("dmr: unknown CBSKO %#02x (%#06b)", cb.CBSKO, cb.CBSKO) + } + + if err := cb.Data.Parse(data); err != nil { + return nil, err + } + + return cb, nil +} diff --git a/data.go b/data.go new file mode 100644 index 0000000..db45e53 --- /dev/null +++ b/data.go @@ -0,0 +1,9 @@ +package dmr + +type DataBlock struct { + Serial uint8 + CRC uint16 + OK bool + Data [24]byte + Length uint8 +} diff --git a/dataheader.go b/dataheader.go index f2ff841..33a37ca 100644 --- a/dataheader.go +++ b/dataheader.go @@ -2,25 +2,6 @@ package dmr import "fmt" -var DataTypeName = [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 -} - // Data Header Packet Format const ( PacketFormatUDT uint8 = iota // 0b0000 diff --git a/homebrew/homebrew.go b/homebrew/homebrew.go index 448958f..66ea8dd 100644 --- a/homebrew/homebrew.go +++ b/homebrew/homebrew.go @@ -9,20 +9,35 @@ import ( "encoding/hex" "errors" "fmt" - "log" "net" - "os" "strconv" + "strings" "sync" "time" - "github.com/tehmaze/go-dmr" + "github.com/op/go-logging" + "github.com/pd0mz/go-dmr" ) -var logger *log.Logger +var log = logging.MustGetLogger("dmr/homebrew") type AuthStatus uint8 +func (a *AuthStatus) String() string { + switch *a { + case AuthNone: + return "none" + case AuthBegin: + return "begin" + case AuthDone: + return "none" + case AuthFailed: + return "failed" + default: + return "invalid" + } +} + const ( AuthNone AuthStatus = iota AuthBegin @@ -45,10 +60,9 @@ var ( // We ping the peers every minute var ( - AuthTimeout = time.Second * 90 - PingInterval = time.Minute - PingTimeout = time.Second * 150 - RepeaterConfigurationInterval = time.Minute * 5 + AuthTimeout = time.Second * 5 + PingInterval = time.Second * 5 + PingTimeout = time.Second * 15 ) // Peer is a remote repeater that also speaks the Homebrew protocol @@ -63,12 +77,19 @@ type Peer struct { UnlinkOnAuthFailure bool PacketReceived dmr.PacketFunc Last struct { - PacketSent time.Time - PacketReceived time.Time - PingSent time.Time - PongReceived time.Time - RepeaterConfigurationSent time.Time + PacketSent time.Time + PacketReceived time.Time + PingSent time.Time + PingReceived time.Time + PongReceived time.Time } + + // Packed repeater ID + id []byte +} + +func (p *Peer) CheckRepeaterID(id []byte) bool { + return id != nil && p.id != nil && bytes.Equal(id, p.id) } func (p *Peer) UpdateToken(nonce []byte) { @@ -81,11 +102,11 @@ func (p *Peer) UpdateToken(nonce []byte) { // 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 + Config *RepeaterConfiguration + Peer map[string]*Peer + PeerID map[uint32]*Peer + pf dmr.PacketFunc conn *net.UDPConn closed bool id []byte @@ -100,12 +121,15 @@ func New(config *RepeaterConfiguration, addr *net.UDPAddr) (*Homebrew, error) { if config == nil { return nil, errors.New("homebrew: RepeaterConfiguration can't be nil") } + if addr == nil { + return nil, errors.New("homebrew: addr 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)), + id: packRepeaterID(config.ID), mutex: &sync.Mutex{}, } if h.conn, err = net.ListenUDP("udp", addr); err != nil { @@ -128,6 +152,18 @@ func (h *Homebrew) Close() error { return nil } + log.Info("closing") + + // Tell peers we're closing +closing: + for _, peer := range h.Peer { + if peer.Status == AuthDone { + if err := h.WriteToPeer(append(RepeaterClosing, h.id...), peer); err != nil { + break closing + } + } + } + // Kill keepalive goroutine if h.stop != nil { close(h.stop) @@ -141,6 +177,9 @@ func (h *Homebrew) Close() error { // Link establishes a new link with a peer func (h *Homebrew) Link(peer *Peer) error { + if peer == nil { + return errors.New("homebrew: peer can't be nil") + } if peer.Addr == nil { return errors.New("homebrew: peer Addr can't be nil") } @@ -158,6 +197,7 @@ func (h *Homebrew) Link(peer *Peer) error { peer.Last.PongReceived = time.Time{} // Register our peer + peer.id = packRepeaterID(peer.ID) h.Peer[peer.Addr.String()] = peer h.PeerID[peer.ID] = peer @@ -191,13 +231,25 @@ func (h *Homebrew) ListenAndServe() error { return err } if err := h.handle(peer, data[:n]); err != nil { + if h.closed && strings.HasSuffix(err.Error(), "use of closed network connection") { + break + } return err } } + log.Info("listener closed") return nil } +func (h *Homebrew) GetPacketFunc() dmr.PacketFunc { + return h.pf +} + +func (h *Homebrew) SetPacketFunc(f dmr.PacketFunc) { + h.pf = f +} + func (h *Homebrew) WritePacketToPeer(p *dmr.Packet, peer *Peer) error { return h.WriteToPeer(h.parsePacket(p), peer) } @@ -216,6 +268,10 @@ func (h *Homebrew) WriteToPeerWithID(b []byte, id uint32) error { return h.WriteToPeer(b, h.getPeer(id)) } +func (h *Homebrew) checkRepeaterID(id []byte) bool { + return id != nil && bytes.Equal(id, h.id) +} + func (h *Homebrew) getPeer(id uint32) *Peer { h.mutex.Lock() defer h.mutex.Unlock() @@ -253,7 +309,7 @@ func (h *Homebrew) getPeers() []*Peer { 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) + log.Debugf("ignored packet from unknown peer %s\n", remote) return nil } @@ -269,29 +325,19 @@ func (h *Homebrew) handle(remote *net.UDPAddr, data []byte) error { } 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 { + if !peer.CheckRepeaterID(data[4:]) { + log.Warningf("peer %d@%s sent invalid repeater ID %q (ignored)\n", peer.ID, remote, string(data[4:])) 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) + log.Errorf("peer %d@%s nonce generation failed: %v\n", peer.ID, remote, err) return h.WriteToPeer(append(MasterNAK, h.id...), peer) } @@ -311,34 +357,30 @@ func (h *Homebrew) handle(remote *net.UDPAddr, data []byte) error { 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 !peer.CheckRepeaterID(data[4:]) { + log.Warningf("peer %d@%s sent invalid repeater ID %q (ignored)\n", peer.ID, remote, string(data[4:])) + return h.WriteToPeer(append(MasterNAK, h.id...), peer) } - if !ok { + if len(data) != 76 { 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) + log.Errorf("peer %d@%s sent invalid key challenge token\n", peer.ID, remote) peer.Status = AuthNone return h.WriteToPeer(append(MasterNAK, h.id...), peer) } + peer.Last.PingSent = time.Now() + peer.Last.PongReceived = time.Now() 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) + if !h.checkRepeaterID(data[6:14]) { + log.Warningf("peer %d@%s sent invalid repeater ID %q (ignored)\n", peer.ID, remote, string(data[6:14])) return nil } @@ -346,13 +388,13 @@ func (h *Homebrew) handle(remote *net.UDPAddr, data []byte) error { case AuthNone: switch { case bytes.Equal(data[:6], MasterACK): - logger.Printf("peer %d@%s sent nonce\n", peer.ID, remote) + log.Debugf("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) + log.Errorf("peer %d@%s refused login\n", peer.ID, remote) peer.Status = AuthFailed if peer.UnlinkOnAuthFailure { h.Unlink(peer.ID) @@ -360,20 +402,21 @@ func (h *Homebrew) handle(remote *net.UDPAddr, data []byte) error { break default: - logger.Printf("peer %d@%s sent unexpected login reply (ignored)\n", peer.ID, remote) + log.Warningf("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) + log.Infof("peer %d@%s accepted login\n", peer.ID, remote) peer.Status = AuthDone - peer.Last.RepeaterConfigurationSent = time.Now() + peer.Last.PingSent = time.Now() + peer.Last.PongReceived = 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) + log.Errorf("peer %d@%s refused login\n", peer.ID, remote) peer.Status = AuthFailed if peer.UnlinkOnAuthFailure { h.Unlink(peer.ID) @@ -381,37 +424,82 @@ func (h *Homebrew) handle(remote *net.UDPAddr, data []byte) error { break default: - logger.Printf("peer %d@%s sent unexpected login reply (ignored)\n", peer.ID, remote) + log.Warningf("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) + if peer.Incoming { + switch { + case bytes.Equal(data[:4], DMRData): + p, err := h.parseData(data) + 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 + case bytes.Equal(data[:6], MasterACK): + break + + case len(data) == 15 && bytes.Equal(data[:7], MasterPing): + return h.WriteToPeer(append(RepeaterPong, data[7:]...), peer) + + default: + log.Warningf("peer %d@%s sent unexpected packet (status=%s):\n", peer.ID, remote, peer.Status.String()) + log.Debug(hex.Dump(data)) + break } - return h.WriteToPeer(append(RepeaterPong, h.id...), peer) + } else { + switch { + case bytes.Equal(data[:4], DMRData): + p, err := h.parseData(data) + if err != nil { + return err + } + return h.handlePacket(p, peer) + + case bytes.Equal(data[:6], MasterACK): + if !h.checkRepeaterID(data[6:]) { + log.Warningf("peer %d@%s sent invalid repeater ID %q (ignored)\n", peer.ID, remote, string(data[6:14])) + return nil + } + peer.Last.PingSent = time.Now() + return h.WriteToPeer(append(MasterPing, h.id...), peer) + + case bytes.Equal(data[:6], MasterNAK): + if !h.checkRepeaterID(data[6:]) { + log.Warningf("peer %d@%s sent invalid repeater ID %q (ignored)\n", peer.ID, remote, string(data[6:14])) + return nil + } + + log.Errorf("peer %d@%s deauthenticated us; re-authenticating\n", peer.ID, remote) + peer.Status = AuthNone + return h.handleAuth(peer) + + case len(data) == 15 && bytes.Equal(data[:7], RepeaterPong): + if !h.checkRepeaterID(data[7:]) { + log.Warningf("peer %d@%s sent invalid repeater ID %q (ignored)\n", peer.ID, remote, string(data[6:14])) + return nil + } + peer.Last.PongReceived = time.Now() + break - default: - logger.Printf("peer %d@%s sent unexpected packet\n", peer.ID, remote) - break + case len(data) == 10 && bytes.Equal(data[:6], MasterNAK): + if !h.checkRepeaterID(data[6:]) { + log.Warningf("peer %d@%s sent invalid repeater ID %q (ignored)\n", peer.ID, remote, string(data[6:14])) + return nil + } + log.Errorf("peer %d@%s sent NAK; re-establishing link\n", peer.ID, remote) + peer.Status = AuthNone + return h.handleAuth(peer) + + default: + log.Warningf("peer %d@%s sent unexpected packet (status=%s):\n", peer.ID, remote, peer.Status.String()) + log.Debug(hex.Dump(data)) + break + } } } @@ -437,10 +525,10 @@ func (h *Homebrew) handlePacket(p *dmr.Packet, peer *Peer) error { if peer.PacketReceived != nil { return peer.PacketReceived(h, p) } - if h.PacketReceived == nil { + if h.pf == nil { return errors.New("homebrew: no PacketReceived func defined to handle DMR packet") } - return h.PacketReceived(h, p) + return h.pf(h, p) } func (h *Homebrew) keepalive(stop <-chan bool) { @@ -453,43 +541,51 @@ func (h *Homebrew) keepalive(stop <-chan bool) { // 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) + switch peer.Status { + case AuthDone: + switch { + case now.Sub(peer.Last.PingReceived) > PingTimeout: + peer.Status = AuthNone + log.Errorf("peer %d@%s not requesting to ping; dropping connection", peer.ID, peer.Addr) + if err := h.WriteToPeer(append(MasterClosing, h.id...), peer); err != nil { + log.Errorf("peer %d@%s close failed: %v\n", peer.ID, peer.Addr, err) + } + break } 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) + } else { + switch peer.Status { + case AuthNone, AuthBegin: + switch { + case now.Sub(peer.Last.PacketReceived) > AuthTimeout: + log.Errorf("peer %d@%s not responding to login; retrying\n", peer.ID, peer.Addr) + if err := h.handleAuth(peer); err != nil { + log.Errorf("peer %d@%s retry failed: %v\n", peer.ID, peer.Addr, err) + } + break } - 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) + case AuthDone: + switch { + case now.Sub(peer.Last.PongReceived) > PingTimeout: + peer.Status = AuthNone + log.Errorf("peer %d@%s not responding to ping; trying to re-establish connection", peer.ID, peer.Addr) + if err := h.WriteToPeer(append(RepeaterClosing, h.id...), peer); err != nil { + log.Errorf("peer %d@%s close failed: %v\n", peer.ID, peer.Addr, err) + } + if err := h.handleAuth(peer); err != nil { + log.Errorf("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 { + log.Errorf("peer %d@%s ping failed: %v\n", peer.ID, peer.Addr, err) + } + break } - 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 } } } @@ -502,18 +598,11 @@ func (h *Homebrew) keepalive(stop <-chan bool) { // 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)) + p, err := ParseData(data) + if err == nil { + p.RepeaterID = h.Config.ID } - - 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 + return p, err } // parsePacket converts DMR packet format to Homebrew packet format suitable for sending on the wire @@ -563,7 +652,7 @@ func (h *Homebrew) parsePacket(p *dmr.Packet) []byte { } func (h *Homebrew) parseRepeaterID(data []byte) (uint32, error) { - id, err := strconv.ParseUint(string(data), 10, 32) + id, err := strconv.ParseUint(string(data), 16, 32) if err != nil { return 0, err } @@ -573,11 +662,33 @@ func (h *Homebrew) parseRepeaterID(data []byte) (uint32, error) { // Interface compliance check var _ dmr.Repeater = (*Homebrew)(nil) -// UpdateLogger replaces the package logger. -func UpdateLogger(l *log.Logger) { - logger = l +func packRepeaterID(id uint32) []byte { + return []byte(fmt.Sprintf("%08X", id)) } -func init() { - UpdateLogger(log.New(os.Stderr, "dmr/homebrew: ", log.LstdFlags)) +// ParseData converts Homebrew packet format to DMR packet format. +func 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]), + RepeaterID: uint32(data[11])<<24 | uint32(data[12])<<16 | uint32(data[13])<<8 | uint32(data[14]), + Timeslot: (data[15] >> 0) & 0x01, + CallType: (data[15] >> 1) & 0x01, + StreamID: uint32(data[16])<<24 | uint32(data[17])<<16 | uint32(data[18])<<8 | uint32(data[19]), + } + p.SetData(data[20:]) + + switch (data[15] >> 2) & 0x03 { + case 0x00, 0x01: // voice (B-F), voice sync (A) + p.DataType = dmr.VoiceBurstA + (data[15] >> 4) + case 0x02: // data sync + p.DataType = (data[15] >> 4) + } + + return p, nil } diff --git a/homebrew/repeaterconfiguration.go b/homebrew/repeaterconfiguration.go index 6d67e5b..e002e08 100644 --- a/homebrew/repeaterconfiguration.go +++ b/homebrew/repeaterconfiguration.go @@ -3,7 +3,7 @@ package homebrew import ( "fmt" - "github.com/tehmaze/go-dmr" + "github.com/pd0mz/go-dmr" ) // RepeaterConfiguration holds information about the current repeater. It @@ -22,6 +22,8 @@ type RepeaterConfiguration struct { Location string Description string URL string + SoftwareID string + PackageID string } // Bytes returns the configuration as bytes. @@ -40,6 +42,12 @@ func (r *RepeaterConfiguration) String() string { if r.TXPower > 99 { r.TXPower = 99 } + if r.SoftwareID == "" { + r.SoftwareID = dmr.SoftwareID + } + if r.PackageID == "" { + r.PackageID = dmr.PackageID + } var lat = fmt.Sprintf("%-08f", r.Latitude) if len(lat) > 8 { @@ -63,8 +71,8 @@ func (r *RepeaterConfiguration) String() string { 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) + b += fmt.Sprintf("%-40s", r.SoftwareID) + b += fmt.Sprintf("%-40s", r.PackageID) return b } diff --git a/ipsc/ipsc.go b/ipsc/ipsc.go index d3c5c4b..f291577 100644 --- a/ipsc/ipsc.go +++ b/ipsc/ipsc.go @@ -1,3 +1,4 @@ +// Package ipsc implements the Motorola IP Site Connect protocol. package ipsc import ( diff --git a/ipsc/packet.go b/ipsc/packet.go index b1e7f54..83781a2 100644 --- a/ipsc/packet.go +++ b/ipsc/packet.go @@ -4,7 +4,7 @@ import ( "encoding/hex" "fmt" - "github.com/tehmaze/go-dmr/dmr" + "github.com/pd0mz/go-dmr" ) const ( @@ -63,15 +63,15 @@ var ( ) type Packet struct { - Timeslot uint8 // 0=ts1, 1=ts2 - FrameType uint8 - SlotType uint16 - CallType uint8 // 0=private, 1=group - SrcID uint32 - DstID uint32 - Payload []byte // 34 bytes - PayloadBits []byte // 264 bits - Sequence uint8 + Timeslot uint8 // 0=ts1, 1=ts2 + FrameType uint8 + SlotType uint16 + CallType uint8 // 0=private, 1=group + SrcID uint32 + DstID uint32 + Payload []byte // 34 bytes + Bits []byte // 264 bits + Sequence uint8 } func (p *Packet) Dump() string { @@ -84,21 +84,21 @@ func (p *Packet) Dump() string { s += fmt.Sprintf("target....: %d\n", p.DstID) s += fmt.Sprintf("payload...: %d bytes (swapped):\n", len(p.Payload)) s += hex.Dump(p.Payload) - s += fmt.Sprintf("payload...: %d bits:\n", len(p.PayloadBits)) - s += p.PayloadBits.Dump() + s += fmt.Sprintf("payload...: %d bits:\n", len(p.Bits)) + s += hex.Dump(p.Bits) return s } func (p *Packet) InfoBits() []byte { var b = make([]byte, dmr.InfoBits) - copy(b[0:dmr.InfoHalfBits], p.PayloadBits[0:dmr.InfoHalfBits]) - copy(b[dmr.InfoHalfBits:], p.PayloadBits[dmr.InfoHalfBits+dmr.SlotTypeBits+dmr.SignalBits:]) + copy(b[0:dmr.InfoHalfBits], p.Bits[0:dmr.InfoHalfBits]) + copy(b[dmr.InfoHalfBits:], p.Bits[dmr.InfoHalfBits+dmr.SlotTypeBits+dmr.SignalBits:]) return b } func (p *Packet) VoiceBits() []byte { var b = make([]byte, dmr.VoiceBits) - copy(b[:dmr.VoiceHalfBits], p.PayloadBits[:dmr.VoiceHalfBits]) - copy(b[dmr.VoiceHalfBits:], p.PayloadBits[dmr.VoiceHalfBits+dmr.SignalBits:]) + copy(b[:dmr.VoiceHalfBits], p.Bits[:dmr.VoiceHalfBits]) + copy(b[dmr.VoiceHalfBits:], p.Bits[dmr.VoiceHalfBits+dmr.SignalBits:]) return b } diff --git a/packet.go b/packet.go index 4b01d3f..b1126a2 100644 --- a/packet.go +++ b/packet.go @@ -5,7 +5,7 @@ const ( PrivacyIndicator uint8 = iota // Privacy Indicator information in a standalone burst VoiceLC // Indicates the beginning of voice transmission, carries addressing information TerminatorWithLC // Indicates the end of transmission, carries LC information - ControlBlock // Carries a control block + CBSK // Carries a control block MultiBlockControl // Header for multi-block control MultiBlockControlContinuation // Follow-on blocks for multi-block control Data // Carries addressing and numbering of packet data blocks @@ -22,6 +22,27 @@ const ( UnknownSlotType ) +var DataTypeName = map[uint8]string{ + PrivacyIndicator: "privacy indicator", + VoiceLC: "voice LC", + TerminatorWithLC: "terminator with LC", + CBSK: "control block", + MultiBlockControl: "multi-block control", + MultiBlockControlContinuation: "multi-block control follow-on", + Data: "data", + Rate12Data: "rate 1/2 packet data", + Rate34Data: "rate 3/4 packet data", + Idle: "idle", + VoiceBurstA: "voice (burst A)", + VoiceBurstB: "voice (burst B)", + VoiceBurstC: "voice (burst C)", + VoiceBurstD: "voice (burst D)", + VoiceBurstE: "voice (burst E)", + VoiceBurstF: "voice (burst F)", + IPSCSync: "IPSC sync", + UnknownSlotType: "uknown", +} + // Call Type const ( CallTypePrivate uint8 = iota diff --git a/repeater.go b/repeater.go index bb54843..91660af 100644 --- a/repeater.go +++ b/repeater.go @@ -5,4 +5,7 @@ type Repeater interface { Active() bool Close() error ListenAndServe() error + + GetPacketFunc() PacketFunc + SetPacketFunc(PacketFunc) } diff --git a/terminal/terminal.go b/terminal/terminal.go new file mode 100644 index 0000000..190cdc7 --- /dev/null +++ b/terminal/terminal.go @@ -0,0 +1,214 @@ +package terminal + +import ( + "encoding/hex" + "fmt" + "time" + + "github.com/op/go-logging" + "github.com/pd0mz/go-dmr" + "github.com/pd0mz/go-dmr/bptc" + "github.com/pd0mz/go-dmr/trellis" +) + +var log = logging.MustGetLogger("dmr/terminal") + +const ( + idle uint8 = iota + dataCallActive + voideCallActive +) + +type Slot struct { + call struct { + start time.Time + end time.Time + } + dstID, srcID uint32 + dataType uint8 + data struct { + packetHeaderValid bool + blocks [64]dmr.DataBlock + blocksExpected uint8 + blocksReceived uint8 + } + selectiveAckRequestsSent int + rxSequence int + fullMessageBlocks uint8 + last struct { + packetReceived time.Time + } +} + +func NewSlot() Slot { + return Slot{} +} + +type Terminal struct { + ID uint32 + Call string + Repeater dmr.Repeater + slot []Slot + state uint8 +} + +func New(id uint32, call string, r dmr.Repeater) *Terminal { + t := &Terminal{ + ID: id, + Call: call, + Repeater: r, + slot: []Slot{NewSlot(), NewSlot()}, + } + r.SetPacketFunc(t.handlePacket) + return t +} + +func (t *Terminal) dataCallEnd(p *dmr.Packet) error { + slot := t.slot[p.Timeslot] + + if t.state != dataCallActive { + return nil + } + + log.Debugf("[%d->%d] data call ended", slot.srcID, slot.dstID) + + return nil +} + +func (t *Terminal) dataCallStart(p *dmr.Packet) error { + slot := t.slot[p.Timeslot] + + if slot.dstID != p.DstID || slot.srcID != p.SrcID || slot.dataType != p.DataType { + if err := t.dataCallEnd(p); err != nil { + return err + } + } + + slot.data.packetHeaderValid = false + slot.call.start = time.Now() + slot.call.end = time.Time{} + slot.dstID = p.DstID + slot.srcID = p.SrcID + t.state = dataCallActive + + log.Debugf("[%d->%d] data call started", slot.srcID, slot.dstID) + + return nil +} + +func (t *Terminal) handlePacket(r dmr.Repeater, p *dmr.Packet) error { + var err error + if p.DstID != t.ID { + //log.Debugf("[%d->%d] (%s, %#04b): ignored, not sent to me", p.SrcID, p.DstID, dmr.DataTypeName[p.DataType], p.DataType) + return nil + } + + switch p.DataType { + case dmr.VoiceBurstA, dmr.VoiceBurstB, dmr.VoiceBurstC, dmr.VoiceBurstD, dmr.VoiceBurstE, dmr.VoiceBurstF: + return nil + case dmr.CBSK: + return nil + } + + log.Debugf("[%d->%d] (%s, %#04b): sent to me", p.SrcID, p.DstID, dmr.DataTypeName[p.DataType], p.DataType) + + switch p.DataType { + case dmr.CBSK: + err = t.handleControlBlock(p) + break + case dmr.Data: + err = t.handleData(p) + break + case dmr.Rate34Data: + err = t.handleRate34Data(p) + default: + log.Debug(hex.Dump(p.Data)) + } + + if err != nil { + log.Errorf("handle packet error: %v", err) + } + + return err +} + +func (t *Terminal) handleControlBlock(p *dmr.Packet) error { + slot := t.slot[p.Timeslot] + slot.last.packetReceived = time.Now() + + var ( + bits = p.InfoBits() + data = make([]byte, 12) + ) + + if err := bptc.Decode(bits, data); err != nil { + return err + } + cb, err := dmr.ParseControlBlock(data) + if err != nil { + return err + } + + log.Debugf("[%d->%d] control block %T", cb.SrcID, cb.DstID, cb.Data) + + return nil +} + +func (t *Terminal) handleData(p *dmr.Packet) error { + slot := t.slot[p.Timeslot] + slot.last.packetReceived = time.Now() + + var ( + bits = p.InfoBits() + data = make([]byte, 12) + ) + + if err := bptc.Decode(bits, data); err != nil { + return err + } + + h, err := dmr.ParseDataHeader(data, false) + if err != nil { + return err + } + + slot.data.packetHeaderValid = false + slot.data.blocksReceived = 0 + slot.selectiveAckRequestsSent = 0 + slot.rxSequence = 0 + + c := h.CommonHeader() + log.Debugf("[%d->%d] data header %T", c.SrcID, c.DstID, h) + + switch ht := h.(type) { + case dmr.ShortDataDefinedDataHeader: + if ht.FullMessage { + slot.data.blocks = [64]dmr.DataBlock{} + slot.fullMessageBlocks = ht.AppendedBlocks + log.Debugf("[%d->%d] expecting %d data blocks", c.SrcID, c.DstID, slot.fullMessageBlocks) + } + slot.data.blocksExpected = ht.AppendedBlocks + return t.dataCallStart(p) + default: + log.Warningf("unhandled data header %T", h) + } + + return nil +} + +func (t *Terminal) handleRate34Data(p *dmr.Packet) error { + slot := t.slot[p.Timeslot] + slot.last.packetReceived = time.Now() + + var ( + bits = p.InfoBits() + data = make([]byte, 18) + ) + + if err := trellis.Decode(bits, data); err != nil { + return err + } + fmt.Print(hex.Dump(data)) + + return nil +} diff --git a/voice_emb.go b/voice_emb.go index f23e02d..aba5fe8 100644 --- a/voice_emb.go +++ b/voice_emb.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "github.com/tehmaze/go-dmr/crc/quadres_16_7" + "github.com/pd0mz/go-dmr/crc/quadres_16_7" ) // EMB LCSS fragments