-
Notifications
You must be signed in to change notification settings - Fork 39
/
hblink.py
executable file
·509 lines (430 loc) · 25.9 KB
/
hblink.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
#!/usr/bin/env python
#
# This work is licensed under the Creative Attribution-NonCommercial-ShareAlike
# 3.0 Unported License.To view a copy of this license, visit
# http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter to
# Creative Commons, 444 Castro Street, Suite 900, Mountain View,
# California, 94041, USA.
from __future__ import print_function
# Python modules we need
import argparse
import sys
import os
import signal
# Specifig functions from modules we need
from binascii import b2a_hex as h
from binascii import a2b_hex as a
from socket import gethostbyname
from random import randint
from hashlib import sha256
from time import time
from bitstring import BitArray
import socket
# Debugging functions
from pprint import pprint
# Twisted is pretty important, so I keep it separate
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
from twisted.internet import task
# Other files we pull from -- this is mostly for readability and segmentation
import hb_log
import hb_config
# Does anybody read this stuff? There's a PEP somewhere that says I should do this.
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2016 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Colin Durbridge, G4EML, Steve Zingman, N4IRS; Mike Zingman, N4IRR; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT'
__license__ = 'Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = '[email protected]'
__status__ = 'pre-alpha'
# Global variables used whether we are a module or __main__
masters = {}
clients = {}
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CONFIG_FILE', help='/full/path/to/config.file (usually hblink.cfg)')
parser.add_argument('-l', '--logging', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
cli_args = parser.parse_args()
# Ensure we have a path for the config file, if one wasn't specified, then use the execution directory
if not cli_args.CONFIG_FILE:
cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/hblink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = hb_config.build_config(cli_args.CONFIG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
logger = hb_log.config_logging(CONFIG['LOGGER'])
logger.debug('Logging system started, anything from here on gets logged')
# Shut ourselves down gracefully by disconnecting from the masters and clients.
def handler(_signal, _frame):
logger.info('*** HBLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for client in clients:
this_client = clients[client]
this_client.send_packet('RPTCL'+CONFIG['CLIENTS'][client]['RADIO_ID'])
logger.info('(%s) De-Registering From the Master', client)
for master in masters:
this_master = masters[master]
for client in CONFIG['MASTERS'][master]['CLIENTS']:
this_master.send_packet(client, 'MSTCL'+client)
logger.info('(%s) Sending De-Registration to Client: %s', master, CONFIG['MASTERS'][master]['CLIENTS'][client]['RADIO_ID'])
reactor.stop()
# Set signal handers so that we can gracefully exit if need be
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, handler)
#************************************************
# UTILITY FUNCTIONS
#************************************************
# Create a 3 byte hex string from an integer
def hex_str_3(_int_id):
try:
return hex(_int_id)[2:].rjust(6,'0').decode('hex')
except TypeError:
logger.error('hex_str_3: invalid integer length')
# Create a 4 byte hex string from an integer
def hex_str_4(_int_id):
try:
return hex(_int_id)[2:].rjust(8,'0').decode('hex')
except TypeError:
logger.error('hex_str_4: invalid integer length')
# Convert a hex string to an int (radio ID, etc.)
def int_id(_hex_string):
return int(h(_hex_string), 16)
#************************************************
# AMBE CLASS: Used to parse out AMBE and send to gateway
#************************************************
class AMBE:
_sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
_exp_ip = CONFIG['AMBE']['EXPORT_IP']
_exp_port = CONFIG['AMBE']['EXPORT_PORT']
def parseAMBE(self, _client, _data):
_seq = int_id(_data[4:5])
_srcID = int_id(_data[5:8])
_dstID = int_id(_data[8:11])
_rptID = int_id(_data[11:15])
_bits = int_id(_data[15:16]) # SCDV NNNN (Slot|Call type|Data|Voice|Seq or Data type)
_slot = 2 if _bits & 0x80 else 1
_callType = 1 if (_bits & 0x40) else 0
_frameType = (_bits & 0x30) >> 4
_voiceSeq = (_bits & 0x0f)
_streamID = int_id(_data[16:20])
logger.debug('(%s) seq: %d srcID: %d dstID: %d rptID: %d bits: %0X slot:%d callType: %d frameType: %d voiceSeq: %d streamID: %0X',
_client, _seq, _srcID, _dstID, _rptID, _bits, _slot, _callType, _frameType, _voiceSeq, _streamID )
#logger.debug('Frame 1:(%s)', self.ByteToHex(_data))
_dmr_frame = BitArray('0x'+h(_data[20:]))
_ambe = _dmr_frame[0:108] + _dmr_frame[156:264]
#_sock.sendto(_ambe.tobytes(), ("127.0.0.1", 31000))
ambeBytes = _ambe.tobytes()
self._sock.sendto(ambeBytes[0:9], (self._exp_ip, self._exp_port))
self._sock.sendto(ambeBytes[9:18], (self._exp_ip, self._exp_port))
self._sock.sendto(ambeBytes[18:27], (self._exp_ip, self._exp_port))
#************************************************
# HB MASTER CLASS
#************************************************
class HBMASTER(DatagramProtocol):
def __init__(self, *args, **kwargs):
if len(args) == 1:
# Define a few shortcuts to make the rest of the class more readable
self._master = args[0]
self._config = CONFIG['MASTERS'][self._master]
self._clients = CONFIG['MASTERS'][self._master]['CLIENTS']
# Configure for AMBE audio export if enabled
if self._config['EXPORT_AMBE']:
self._ambe = AMBE()
else:
# If we didn't get called correctly, log it and quit.
logger.error('(%s) HBMASTER was not called with an argument. Terminating', self._master)
sys.exit()
def startProtocol(self):
# Set up periodic loop for tracking pings from clients. Run every 'PING_TIME' seconds
self._master_maintenance = task.LoopingCall(self.master_maintenance_loop)
self._master_maintenance_loop = self._master_maintenance.start(CONFIG['GLOBAL']['PING_TIME'])
def master_maintenance_loop(self):
logger.debug('(%s) Master maintenance loop started', self._master)
for client in self._clients:
_this_client = self._clients[client]
# Check to see if any of the clients have been quiet (no ping) longer than allowed
if _this_client['LAST_PING']+CONFIG['GLOBAL']['PING_TIME']*CONFIG['GLOBAL']['MAX_MISSED'] < time():
logger.info('(%s) Client %s has timed out', self._master, _this_client['RADIO_ID'])
# Remove any timed out clients from the configuration
del CONFIG['MASTERS'][self._master]['CLIENTS'][client]
def send_clients(self, _packet):
for _client in self._clients:
self.send_packet(_client, _packet)
#logger.debug('(%s) Packet sent to client %s', self._master, self._clients[_client]['RADIO_ID'])
def send_packet(self, _client, _packet):
_ip = self._clients[_client]['IP']
_port = self._clients[_client]['PORT']
self.transport.write(_packet, (_ip, _port))
# KEEP THE FOLLOWING COMMENTED OUT UNLESS YOU'RE DEBUGGING DEEPLY!!!!
#logger.debug('(%s) TX Packet to %s on port %s: %s', self._clients[_client]['RADIO_ID'], self._clients[_client]['IP'], self._clients[_client]['PORT'], h(_packet))
def dmrd_received(self, _radio_id, _rf_src, _dst_id, _seq, _data):
pass
def datagramReceived(self, _data, (_host, _port)):
# Keep This Line Commented Unless HEAVILY Debugging!
# logger.debug('(%s) RX packet from %s:%s -- %s', self._client, _host, _port, h(_data))
# Extract the command, which is various length, all but one 4 significant characters -- RPTCL
_command = _data[:4]
if _command == 'DMRD': # DMRData -- encapsulated DMR data frame
_radio_id = _data[11:15]
if _radio_id in self._clients \
and self._clients[_radio_id]['CONNECTION'] == 'YES' \
and self._clients[_radio_id]['IP'] == _host \
and self._clients[_radio_id]['PORT'] == _port:
_seq = _data[4:5]
_rf_src = _data[5:8]
_dst_id = _data[8:11]
#logger.debug('(%s) DMRD - Seqence: %s, RF Source: %s, Destination ID: %s', self._master, int_id(_seq), int_id(_rf_src), int_id(_dst_id))
# If AMBE audio exporting is configured...
if self._config['EXPORT_AMBE']:
self._ambe.parseAMBE(self._master, _data)
# The basic purpose of a master is to repeat to the clients
if self._config['REPEAT'] == True:
for _client in self._clients:
if _client != _radio_id:
self.send_packet(_client, _data)
logger.debug('(%s) Packet repeated to client: %s', self._master, int_id(_client))
# Userland actions -- typically this is the function you subclass for an application
self.dmrd_received(_radio_id, _rf_src, _dst_id, _seq, _data)
elif _command == 'RPTL': # RPTLogin -- a repeater wants to login
_radio_id = _data[4:8]
if _radio_id: # Future check here for valid Radio ID
self._clients.update({_radio_id: { # Build the configuration data strcuture for the client
'CONNECTION': 'RPTL-RECEIVED',
'PINGS_RECEIVED': 0,
'LAST_PING': time(),
'IP': _host,
'PORT': _port,
'SALT': randint(0,0xFFFFFFFF),
'RADIO_ID': str(int(h(_radio_id), 16)),
'CALLSIGN': '',
'RX_FREQ': '',
'TX_FREQ': '',
'TX_POWER': '',
'COLORCODE': '',
'LATITUDE': '',
'LONGITUDE': '',
'HEIGHT': '',
'LOCATION': '',
'DESCRIPTION': '',
'SLOTS': '',
'URL': '',
'SOFTWARE_ID': '',
'PACKAGE_ID': '',
}})
logger.info('(%s) Repeater Logging in with Radio ID: %s, %s:%s', self._master, int_id(_radio_id), _host, _port)
_salt_str = hex_str_4(self._clients[_radio_id]['SALT'])
self.send_packet(_radio_id, 'RPTACK'+_salt_str)
self._clients[_radio_id]['CONNECTION'] = 'CHALLENGE_SENT'
logger.info('(%s) Sent Challenge Response to %s for login: %s', self._master, int_id(_radio_id), self._clients[_radio_id]['SALT'])
else:
self.transport.write('MSTNAK'+_radio_id, (_host, _port))
logger.warning('(%s) Invalid Login from Radio ID: %s', self._master, int_id(_radio_id))
elif _command == 'RPTK': # Repeater has answered our login challenge
_radio_id = _data[4:8]
if _radio_id in self._clients \
and self._clients[_radio_id]['CONNECTION'] == 'CHALLENGE_SENT' \
and self._clients[_radio_id]['IP'] == _host \
and self._clients[_radio_id]['PORT'] == _port:
_this_client = self._clients[_radio_id]
_this_client['LAST_PING'] = time()
_sent_hash = _data[8:]
_salt_str = hex_str_4(_this_client['SALT'])
_calc_hash = a(sha256(_salt_str+self._config['PASSPHRASE']).hexdigest())
if _sent_hash == _calc_hash:
_this_client['CONNECTION'] = 'WAITING_CONFIG'
self.send_packet(_radio_id, 'RPTACK'+_radio_id)
logger.info('(%s) Client %s has completed the login exchange successfully', self._master, _this_client['RADIO_ID'])
else:
logger.info('(%s) Client %s has FAILED the login exchange successfully', self._master, _this_client['RADIO_ID'])
self.transport.write('MSTNAK'+_radio_id, (_host, _port))
del self._clients[_radio_id]
else:
self.transport.write('MSTNAK'+_radio_id, (_host, _port))
logger.warning('(%s) Login challenge from Radio ID that has not logged in: %s', self._master, int_id(_radio_id))
elif _command == 'RPTC': # Repeater is sending it's configuraiton OR disconnecting
if _data[:5] == 'RPTCL': # Disconnect command
_radio_id = _data[5:9]
if _radio_id in self._clients \
and self._clients[_radio_id]['CONNECTION'] == 'YES' \
and self._clients[_radio_id]['IP'] == _host \
and self._clients[_radio_id]['PORT'] == _port:
logger.info('(%s) Client is closing down: %s', self._master, int_id(_radio_id))
self.transport.write('MSTNAK'+_radio_id, (_host, _port))
del self._clients[_radio_id]
else:
_radio_id = _data[4:8] # Configure Command
if _radio_id in self._clients \
and self._clients[_radio_id]['CONNECTION'] == 'WAITING_CONFIG' \
and self._clients[_radio_id]['IP'] == _host \
and self._clients[_radio_id]['PORT'] == _port:
_this_client = self._clients[_radio_id]
_this_client['CONNECTION'] = 'YES'
_this_client['LAST_PING'] = time()
_this_client['CALLSIGN'] = _data[8:16]
_this_client['RX_FREQ'] = _data[16:25]
_this_client['TX_FREQ'] = _data[25:34]
_this_client['TX_POWER'] = _data[34:36]
_this_client['COLORCODE'] = _data[36:38]
_this_client['LATITUDE'] = _data[38:47]
_this_client['LONGITUDE'] = _data[47:57]
_this_client['HEIGHT'] = _data[57:60]
_this_client['LOCATION'] = _data[60:80]
_this_client['DESCRIPTION'] = _data[80:99]
_this_client['SLOTS'] = _data[99:100]
_this_client['URL'] = _data[100:224]
_this_client['SOFTWARE_ID'] = _data[224:264]
_this_client['PACKAGE_ID'] = _data[264:304]
self.send_packet(_radio_id, 'RPTACK'+_radio_id)
logger.info('(%s) Client %s has sent repeater configuration', self._master, _this_client['RADIO_ID'])
else:
self.transport.write('MSTNAK'+_radio_id, (_host, _port))
logger.warning('(%s) Client info from Radio ID that has not logged in: %s', self._master, int_id(_radio_id))
elif _command == 'RPTP': # RPTPing -- client is pinging us
_radio_id = _data[7:11]
if _radio_id in self._clients \
and self._clients[_radio_id]['CONNECTION'] == "YES" \
and self._clients[_radio_id]['IP'] == _host \
and self._clients[_radio_id]['PORT'] == _port:
self._clients[_radio_id]['LAST_PING'] = time()
self.send_packet(_radio_id, 'MSTPONG'+_radio_id)
logger.debug('(%s) Received and answered RPTPING from client %s', self._master, int_id(_radio_id))
else:
self.transport.write('MSTNAK'+_radio_id, (_host, _port))
logger.warning('(%s) Client info from Radio ID that has not logged in: %s', self._master, int_id(_radio_id))
else:
logger.error('(%s) Unrecognized command from: %s. Packet: %s', self._master, int_id(_radio_id), h(_data))
#************************************************
# HB CLIENT CLASS
#************************************************
class HBCLIENT(DatagramProtocol):
def __init__(self, *args, **kwargs):
if len(args) == 1:
self._client = args[0]
self._config = CONFIG['CLIENTS'][self._client]
self._stats = self._config['STATS']
# Configure for AMBE audio export if enabled
if self._config['EXPORT_AMBE']:
self._ambe = AMBE()
else:
# If we didn't get called correctly, log it!
logger.error('(%s) HBCLIENT was not called with an argument. Terminating', self._client)
sys.exit()
def startProtocol(self):
# Set up periodic loop for sending pings to the master. Run every 'PING_TIME' seconds
self._client_maintenance = task.LoopingCall(self.client_maintenance_loop)
self._client_maintenance_loop = self._client_maintenance.start(CONFIG['GLOBAL']['PING_TIME'])
def client_maintenance_loop(self):
logger.debug('(%s) Client maintenance loop started', self._client)
# If we're not connected, zero out the stats and send a login request RPTL
if self._stats['CONNECTION'] == 'NO' or self._stats['CONNECTION'] == 'RTPL_SENT':
self._stats['PINGS_SENT'] = 0
self._stats['PINGS_ACKD'] = 0
self._stats['CONNECTION'] = 'RTPL_SENT'
self.send_packet('RPTL'+self._config['RADIO_ID'])
logger.info('(%s) Sending login request to master %s:%s', self._client, self._config['MASTER_IP'], self._config['MASTER_PORT'])
# If we are connected, sent a ping to the master and increment the counter
if self._stats['CONNECTION'] == 'YES':
self.send_packet('RPTPING'+self._config['RADIO_ID'])
self._stats['PINGS_SENT'] += 1
logger.debug('(%s) RPTPING Sent to Master. Pings Since Connected: %s', self._client, self._stats['PINGS_SENT'])
def send_packet(self, _packet):
self.transport.write(_packet, (self._config['MASTER_IP'], self._config['MASTER_PORT']))
# KEEP THE FOLLOWING COMMENTED OUT UNLESS YOU'RE DEBUGGING DEEPLY!!!!
#logger.debug('(%s) TX Packet to %s:%s -- %s', self._client, self._config['MASTER_IP'], self._config['MASTER_PORT'], h(_packet))
def dmrd_received(self, _radio_id, _rf_src, _dst_id, _seq, _data):
pass
def datagramReceived(self, _data, (_host, _port)):
# Keep This Line Commented Unless HEAVILY Debugging!
# logger.debug('(%s) RX packet from %s:%s -- %s', self._client, _host, _port, h(_data))
# Validate that we receveived this packet from the master - security check!
if self._config['MASTER_IP'] == _host and self._config['MASTER_PORT'] == _port:
# Extract the command, which is various length, but only 4 significant characters
_command = _data[:4]
if _command == 'DMRD': # DMRData -- encapsulated DMR data frame
_radio_id = _data[11:15]
if _radio_id == self._config['RADIO_ID']: # Validate the source and intended target
_seq = _data[4:5]
_rf_src = _data[5:8]
_dst_id = _data[8:11]
#logger.debug('(%s) DMRD - Seqence: %s, RF Source: %s, Destination ID: %s', self._client, h(_seq), int_id(_rf_src), int_id(_dst_id))
# If AMBE audio exporting is configured...
if self._config['EXPORT_AMBE']:
self._ambe.parseAMBE(self._client, _data)
# Userland actions -- typically this is the function you subclass for an application
self.dmrd_received(_radio_id, _rf_src, _dst_id, _seq, _data)
elif _command == 'MSTN': # Actually MSTNAK -- a NACK from the master
_radio_id = _data[4:8]
if _radio_id == self._config['RADIO_ID']: # Validate the source and intended target
logger.warning('(%s) MSTNAK Received', self._client)
self._stats['CONNECTION'] = 'NO' # Disconnect ourselves and re-register
elif _command == 'RPTA': # Actually RPTACK -- an ACK from the master
# Depending on the state, an RPTACK means different things, in each clause, we check and/or set the state
if self._stats['CONNECTION'] == 'RTPL_SENT': # If we've sent a login request...
_login_int32 = _data[6:10]
logger.info('(%s) Repeater Login ACK Received with 32bit ID: %s', self._client, int_id(_login_int32))
_pass_hash = sha256(_login_int32+self._config['PASSPHRASE']).hexdigest()
_pass_hash = a(_pass_hash)
self.send_packet('RPTK'+self._config['RADIO_ID']+_pass_hash)
self._stats['CONNECTION'] = 'AUTHENTICATED'
elif self._stats['CONNECTION'] == 'AUTHENTICATED': # If we've sent the login challenge...
if _data[6:10] == self._config['RADIO_ID']:
logger.info('(%s) Repeater Authentication Accepted', self._client)
_config_packet = self._config['RADIO_ID']+\
self._config['CALLSIGN']+\
self._config['RX_FREQ']+\
self._config['TX_FREQ']+\
self._config['TX_POWER']+\
self._config['COLORCODE']+\
self._config['LATITUDE']+\
self._config['LONGITUDE']+\
self._config['HEIGHT']+\
self._config['LOCATION']+\
self._config['DESCRIPTION']+\
self._config['SLOTS']+\
self._config['URL']+\
self._config['SOFTWARE_ID']+\
self._config['PACKAGE_ID']
self.send_packet('RPTC'+_config_packet)
self._stats['CONNECTION'] = 'CONFIG-SENT'
logger.info('(%s) Repeater Configuration Sent', self._client)
else:
self._stats['CONNECTION'] = 'NO'
logger.error('(%s) Master ACK Contained wrong ID - Connection Reset', self._client)
elif self._stats['CONNECTION'] == 'CONFIG-SENT': # If we've sent out configuration to the master
if _data[6:10] == self._config['RADIO_ID']:
logger.info('(%s) Repeater Configuration Accepted', self._client)
self._stats['CONNECTION'] = 'YES'
logger.info('(%s) Connection to Master Completed', self._client)
else:
self._stats['CONNECTION'] = 'NO'
logger.error('(%s) Master ACK Contained wrong ID - Connection Reset', self._client)
elif _command == 'MSTP': # Actually MSTPONG -- a reply to RPTPING (send by client)
if _data [7:11] == self._config['RADIO_ID']:
self._stats['PINGS_ACKD'] += 1
logger.debug('(%s) MSTPONG Received. Pongs Since Connected: %s', self._client, self._stats['PINGS_ACKD'])
elif _command == 'MSTC': # Actually MSTCL -- notify us the master is closing down
if _data[5:9] == self._config['RADIO_ID']:
self._stats['CONNECTION'] = 'NO'
logger.info('(%s) MSTCL Recieved', self._client)
else:
logger.error('(%s) Received an invalid command in packet: %s', self._client, h(_data))
#************************************************
# MAIN PROGRAM LOOP STARTS HERE
#************************************************
if __name__ == '__main__':
logger.info('HBlink \'HBlink.py\' (c) 2016 N0MJS & the K0USY Group - SYSTEM STARTING...')
# HBlink Master
for master in CONFIG['MASTERS']:
if CONFIG['MASTERS'][master]['ENABLED']:
masters[master] = HBMASTER(master)
reactor.listenUDP(CONFIG['MASTERS'][master]['PORT'], masters[master], interface=CONFIG['MASTERS'][master]['IP'])
logger.debug('MASTER instance created: %s, %s', master, masters[master])
# HBlink Client
for client in CONFIG['CLIENTS']:
if CONFIG['CLIENTS'][client]['ENABLED']:
clients[client] = HBCLIENT(client)
reactor.listenUDP(CONFIG['CLIENTS'][client]['PORT'], clients[client], interface=CONFIG['CLIENTS'][client]['IP'])
logger.debug('CLIENT instance created: %s, %s', client, clients[client])
reactor.run()