diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..14b55f0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.7-slim-stretch + +RUN apt update && \ + apt install -y git && \ + cd /usr/src/ && \ + git clone https://github.com/n0mjs710/dmr_utils3 && \ + cd /usr/src/dmr_utils3 && \ + ./install.sh && \ + rm -rf /var/lib/apt/lists/* && \ + cd /opt && \ + rm -rf /usr/src/dmr_utils3 && \ + git clone https://github.com/n0mjs710/hblink3 + +RUN cd /opt/hblink3/ && \ + sed -i s/.*python.*//g requirements.txt && \ + pip install --no-cache-dir -r requirements.txt + + +ADD entrypoint /entrypoint + +RUN adduser -u 54000 radio && \ + adduser radio radio && \ + chmod 755 /entrypoint && \ + chown radio:radio /entrypoint && \ + chown radio /opt/hblink3 + +RUN chmod 755 /entrypoint + +USER radio +EXPOSE 54000 + +ENTRYPOINT [ "/entrypoint" ] diff --git a/README.md b/README.md index bb9c0f9..32e57c9 100755 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ DVSwitch@groups.io **UPDATES:** -**PURPOSE:** Thanks to the work of Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT we have an open protocol for internetworking DMR repeaters. Unfortunately, there's no generic client and/or master stacks. This project is to build an open-source, python-based implementation. This is a non-commercial license. Atribution is *required* if you use it. +**PURPOSE:** Thanks to the work of Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT we have an open protocol for internetworking DMR repeaters. Unfortunately, there's no generic client and/or master stacks. This project is to build an open-source, python-based implementation. You are free to use this software however you want, however we ask that you provide attribution in some public venue (such as project, club, organization web site). This helps us see where the software is in use and track how it is used. For those who will ask: This is a piece of software that implements an open-source, amateur radio networking protocol. It is not a network. It is not indended to be a network. It is not intended to replace or circumvent a network. People do those things, code doesn't. @@ -24,11 +24,37 @@ None. The owners of this work make absolutely no warranty, express or implied. U **PRE-REQUISITE KNOWLEDGE:** This document assumes the reader is familiar with Linux/UNIX, the Python programming language and DMR. +**Using docker version** + +To work with provided docker setup you will need: +* A private repository with your configuration files (all .cfg files in repo will be copyed to the application root directory on start up) +* A service user able to read your private repository (or be brave and publish your configuration, or be really brave and give your username and password to the docker) +* A server with docker installed +* Follow this simple steps: + +Build your own image from source + +```bash + +docker build . -t millaguie/hblink:3.0.0 + +``` + +Or user a prebuilt one in docker hub: millaguie/hblink:3.0.0 + +Wake up your container + +```bash +touch /var/log/hblink.log +chown 65000 /var/log/hblink.log + run -v /var/log/hblink.log:/var/log/hblink.log -e GIT_USER=$USER -e GIT_PASSWORD=$PASSWORD -e GIT_REPO=$URL_TO_REPO_WITHOUT_HTTPS:// -p 54000:54000 millaguie/hblink:3.0.0 + ``` + **MORE DOCUMENTATION TO COME** ***0x49 DE N0MJS*** -Copyright (C) 2016-2017 Cortney T. Buffington, N0MJS n0mjs@me.com +Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS n0mjs@me.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. diff --git a/config.py b/config.py index 5de0d69..3efe39f 100755 --- a/config.py +++ b/config.py @@ -198,7 +198,53 @@ def build_config(_config_file): 'LAST_PING_TX_TIME': 0, 'LAST_PING_ACK_TIME': 0, }}) - + + if config.get(section, 'MODE') == 'XLXPEER': + CONFIG['SYSTEMS'].update({section: { + 'MODE': config.get(section, 'MODE'), + 'ENABLED': config.getboolean(section, 'ENABLED'), + 'LOOSE': config.getboolean(section, 'LOOSE'), + 'SOCK_ADDR': (gethostbyname(config.get(section, 'IP')), config.getint(section, 'PORT')), + 'IP': gethostbyname(config.get(section, 'IP')), + 'PORT': config.getint(section, 'PORT'), + 'MASTER_SOCKADDR': (gethostbyname(config.get(section, 'MASTER_IP')), config.getint(section, 'MASTER_PORT')), + 'MASTER_IP': gethostbyname(config.get(section, 'MASTER_IP')), + 'MASTER_PORT': config.getint(section, 'MASTER_PORT'), + 'PASSPHRASE': bytes(config.get(section, 'PASSPHRASE'), 'utf-8'), + 'CALLSIGN': bytes(config.get(section, 'CALLSIGN').ljust(8)[:8], 'utf-8'), + 'RADIO_ID': config.getint(section, 'RADIO_ID').to_bytes(4, 'big'), + 'RX_FREQ': bytes(config.get(section, 'RX_FREQ').ljust(9)[:9], 'utf-8'), + 'TX_FREQ': bytes(config.get(section, 'TX_FREQ').ljust(9)[:9], 'utf-8'), + 'TX_POWER': bytes(config.get(section, 'TX_POWER').rjust(2,'0'), 'utf-8'), + 'COLORCODE': bytes(config.get(section, 'COLORCODE').rjust(2,'0'), 'utf-8'), + 'LATITUDE': bytes(config.get(section, 'LATITUDE').ljust(8)[:8], 'utf-8'), + 'LONGITUDE': bytes(config.get(section, 'LONGITUDE').ljust(9)[:9], 'utf-8'), + 'HEIGHT': bytes(config.get(section, 'HEIGHT').rjust(3,'0'), 'utf-8'), + 'LOCATION': bytes(config.get(section, 'LOCATION').ljust(20)[:20], 'utf-8'), + 'DESCRIPTION': bytes(config.get(section, 'DESCRIPTION').ljust(19)[:19], 'utf-8'), + 'SLOTS': bytes(config.get(section, 'SLOTS'), 'utf-8'), + 'URL': bytes(config.get(section, 'URL').ljust(124)[:124], 'utf-8'), + 'SOFTWARE_ID': bytes(config.get(section, 'SOFTWARE_ID').ljust(40)[:40], 'utf-8'), + 'PACKAGE_ID': bytes(config.get(section, 'PACKAGE_ID').ljust(40)[:40], 'utf-8'), + 'GROUP_HANGTIME': config.getint(section, 'GROUP_HANGTIME'), + 'XLXMODULE': config.getint(section, 'XLXMODULE'), + 'OPTIONS': '', + 'USE_ACL': config.getboolean(section, 'USE_ACL'), + 'SUB_ACL': config.get(section, 'SUB_ACL'), + 'TG1_ACL': config.get(section, 'TGID_TS1_ACL'), + 'TG2_ACL': config.get(section, 'TGID_TS2_ACL') + }}) + CONFIG['SYSTEMS'][section].update({'XLXSTATS': { + 'CONNECTION': 'NO', # NO, RTPL_SENT, AUTHENTICATED, CONFIG-SENT, YES + 'CONNECTED': None, + 'PINGS_SENT': 0, + 'PINGS_ACKD': 0, + 'NUM_OUTSTANDING': 0, + 'PING_OUTSTANDING': False, + 'LAST_PING_TX_TIME': 0, + 'LAST_PING_ACK_TIME': 0, + }}) + elif config.get(section, 'MODE') == 'MASTER': CONFIG['SYSTEMS'].update({section: { 'MODE': config.get(section, 'MODE'), diff --git a/const.py b/const.py index 34c8ac8..09c6ff3 100755 --- a/const.py +++ b/const.py @@ -52,7 +52,7 @@ HBPF_SLT_VTERM = 0x2 # HomeBrew Protocol Commands DMRD = b'DMRD' MSTCL = b'MSTCL' -MSTNAK = b'MSTNAC' +MSTNAK = b'MSTNAK' MSTPONG = b'MSTPONG' MSTN = b'MSTN' MSTP = b'MSTP' diff --git a/entrypoint b/entrypoint new file mode 100644 index 0000000..84a03e2 --- /dev/null +++ b/entrypoint @@ -0,0 +1,10 @@ +#!/bin/sh + +mkdir -p /var/tmp/config +cd /var/tmp/config +git clone https://${GIT_USER}:${GIT_PASSWORD}@${GIT_REPO} + +DIR=$(echo ${GIT_REPO}| sed s/.git$//g | sed s#^.*/##g) + +cp -a /var/tmp/config/${DIR}/*cfg /opt/hblink3/ +python /opt/hblink3/hblink.py diff --git a/hblink-SAMPLE.cfg b/hblink-SAMPLE.cfg index 2df6fb9..a3b616c 100755 --- a/hblink-SAMPLE.cfg +++ b/hblink-SAMPLE.cfg @@ -101,8 +101,8 @@ PATH: ./ PEER_FILE: peer_ids.json SUBSCRIBER_FILE: subscriber_ids.json TGID_FILE: talkgroup_ids.json -PEER_URL: https://www.radioid.net/api/dmr/repeater/?country=united%%20states -SUBSCRIBER_URL: https://www.radioid.net/api/dmr/user/?country=united%%20states +PEER_URL: https://www.radioid.net/static/rptrs.json +SUBSCRIBER_URL: https://www.radioid.net/static/users.json STALE_DAYS: 7 # OPENBRIDGE INSTANCES - DUPLICATE SECTION FOR MULTIPLE CONNECTIONS @@ -207,3 +207,35 @@ USE_ACL: True SUB_ACL: DENY:1 TGID_TS1_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL + +[XLX-1] +MODE: XLXPEER +ENABLED: True +LOOSE: True +EXPORT_AMBE: False +IP: +PORT: 54002 +MASTER_IP: 172.16.1.1 +MASTER_PORT: 62030 +PASSPHRASE: passw0rd +CALLSIGN: W1ABC +RADIO_ID: 312000 +RX_FREQ: 449000000 +TX_FREQ: 444000000 +TX_POWER: 25 +COLORCODE: 1 +SLOTS: 1 +LATITUDE: 38.0000 +LONGITUDE: -095.0000 +HEIGHT: 75 +LOCATION: Anywhere, USA +DESCRIPTION: This is a cool repeater +URL: www.w1abc.org +SOFTWARE_ID: 20170620 +PACKAGE_ID: MMDVM_HBlink +GROUP_HANGTIME: 5 +XLXMODULE: 4004 +USE_ACL: True +SUB_ACL: DENY:1 +TGID_TS1_ACL: PERMIT:ALL +TGID_TS2_ACL: PERMIT:ALL diff --git a/hblink.py b/hblink.py index 2693981..9a61fdd 100755 --- a/hblink.py +++ b/hblink.py @@ -224,6 +224,13 @@ class HBSYSTEM(DatagramProtocol): self.datagramReceived = self.peer_datagramReceived self.dereg = self.peer_dereg + elif self._config['MODE'] == 'XLXPEER': + self._stats = self._config['XLXSTATS'] + self.send_system = self.send_master + self.maintenance_loop = self.peer_maintenance_loop + self.datagramReceived = self.peer_datagramReceived + self.dereg = self.peer_dereg + def startProtocol(self): # Set up periodic loop for tracking pings from peers. Run every 'PING_TIME' seconds self._system_maintenance = task.LoopingCall(self.maintenance_loop) @@ -283,6 +290,29 @@ class HBSYSTEM(DatagramProtocol): # KEEP THE FOLLOWING COMMENTED OUT UNLESS YOU'RE DEBUGGING DEEPLY!!!! # logger.debug('(%s) TX Packet to %s:%s -- %s', self._system, self._config['MASTER_IP'], self._config['MASTER_PORT'], ahex(_packet)) + def send_xlxmaster(self, radio, xlx, mastersock): + radio3 = int.from_bytes(radio, 'big').to_bytes(3, 'big') + radio4 = int.from_bytes(radio, 'big').to_bytes(4, 'big') + xlx3 = xlx.to_bytes(3, 'big') + streamid = randint(0,255).to_bytes(1, 'big')+randint(0,255).to_bytes(1, 'big')+randint(0,255).to_bytes(1, 'big')+randint(0,255).to_bytes(1, 'big') + # Wait for .5 secs for the XLX to log us in + for packetnr in range(5): + if packetnr < 3: + # First 3 packets, voice start, stream type e1 + strmtype = 225 + payload = bytearray.fromhex('4f2e00b501ae3a001c40a0c1cc7dff57d75df5d5065026f82880bd616f13f185890000') + else: + # Last 2 packets, voice end, stream type e2 + strmtype = 226 + payload = bytearray.fromhex('4f410061011e3a781c30a061ccbdff57d75df5d2534425c02fe0b1216713e885ba0000') + packetnr1 = packetnr.to_bytes(1, 'big') + strmtype1 = strmtype.to_bytes(1, 'big') + _packet = b''.join([DMRD, packetnr1, radio3, xlx3, radio4, strmtype1, streamid, payload]) + self.transport.write(_packet, mastersock) + # KEEP THE FOLLOWING COMMENTED OUT UNLESS YOU'RE DEBUGGING DEEPLY!!!! + #logger.debug('(%s) XLX Module Change Packet: %s', self._system, ahex(_packet)) + return + def dmrd_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data): pass @@ -631,6 +661,11 @@ class HBSYSTEM(DatagramProtocol): self._stats['CONNECTION'] = 'YES' self._stats['CONNECTED'] = time() logger.info('(%s) Connection to Master Completed', self._system) + # If we are an XLX, send the XLX module request here. + if self._config['MODE'] == 'XLXPEER': + self.send_xlxmaster(self._config['RADIO_ID'], int(4000), self._config['MASTER_SOCKADDR']) + self.send_xlxmaster(self._config['RADIO_ID'], self._config['XLXMODULE'], self._config['MASTER_SOCKADDR']) + logger.info('(%s) Sending XLX Module request', self._system) else: self._stats['CONNECTION'] = 'NO' logger.error('(%s) Master ACK Contained wrong ID - Connection Reset', self._system) diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..c47b9db --- /dev/null +++ b/install.sh @@ -0,0 +1,6 @@ +#! /bin/bash + +# Install the required support programs +apt-get install python3 python3-pip -y +pip3 install -r requirements.txt + diff --git a/requirements.txt b/requirements.txt index 7bb55eb..3d17f35 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -python>=3.5.0 bitstring>=3.1.5 bitarray>=0.8.1 Twisted>=16.3.0 dmr_utils3>=0.1.19 +configparser>=3.0.0 diff --git a/rules_SAMPLE.py b/rules_SAMPLE.py index ea36bab..4f647f0 100755 --- a/rules_SAMPLE.py +++ b/rules_SAMPLE.py @@ -15,7 +15,9 @@ configuration file. * SYSTEM - The name of the sytem as listed in the main hblink configuration file (e.g. hblink.cfg) This MUST be the exact same name as in the main config file!!! * TS - Timeslot used for matching traffic to this confernce bridge + XLX connections should *ALWAYS* use TS 2 only. * TGID - Talkgroup ID used for matching traffic to this conference bridge + XLX connections should *ALWAYS* use TG 9 only. * ON and OFF are LISTS of Talkgroup IDs used to trigger this system off and on. Even if you only want one (as shown in the ON example), it has to be in list format. None can be handled with an empty list, such as " 'ON': [] ".