|
|
|
@ -91,6 +91,53 @@ def encode_utf8(text):
|
|
|
|
return bytes(buf)
|
|
|
|
return bytes(buf)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def encode_iso8(text):
|
|
|
|
|
|
|
|
"""Encode text into a 28-byte TA buffer (format 1 / ISO-8859-1).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
More widely displayed than UTF-8 on some radios (notably Hytera, which often
|
|
|
|
|
|
|
|
will not render format 2 / UTF-8 Talker Alias)."""
|
|
|
|
|
|
|
|
text = truncate_talker_alias(text)
|
|
|
|
|
|
|
|
raw = text.encode('latin-1', errors='replace')[:27]
|
|
|
|
|
|
|
|
size = len(raw)
|
|
|
|
|
|
|
|
header = (TA_FORMAT_ISO8 << 6) | (size << 1) | 0
|
|
|
|
|
|
|
|
buf = bytearray(DMRA_BUF_LEN)
|
|
|
|
|
|
|
|
buf[0] = header
|
|
|
|
|
|
|
|
buf[1:1 + size] = raw
|
|
|
|
|
|
|
|
return bytes(buf)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def encode_7bit(text):
|
|
|
|
|
|
|
|
"""Encode text into a 28-byte TA buffer (format 0 / 7-bit packed).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Bit stream is format(2) + size(5) + 7 bits per char, MSB-first; this is the
|
|
|
|
|
|
|
|
exact inverse of decode_ta()'s 7-bit path. For the oldest radios."""
|
|
|
|
|
|
|
|
text = truncate_talker_alias(text)
|
|
|
|
|
|
|
|
chars = text.encode('ascii', errors='replace')[:31]
|
|
|
|
|
|
|
|
size = len(chars)
|
|
|
|
|
|
|
|
bits = [(TA_FORMAT_7BIT >> 1) & 1, TA_FORMAT_7BIT & 1]
|
|
|
|
|
|
|
|
for b in range(4, -1, -1):
|
|
|
|
|
|
|
|
bits.append((size >> b) & 1)
|
|
|
|
|
|
|
|
for ch in chars:
|
|
|
|
|
|
|
|
for b in range(6, -1, -1):
|
|
|
|
|
|
|
|
bits.append((ch >> b) & 1)
|
|
|
|
|
|
|
|
total = DMRA_BUF_LEN * 8
|
|
|
|
|
|
|
|
bits = (bits + [0] * total)[:total]
|
|
|
|
|
|
|
|
buf = bytearray(DMRA_BUF_LEN)
|
|
|
|
|
|
|
|
for i, bit in enumerate(bits):
|
|
|
|
|
|
|
|
if bit:
|
|
|
|
|
|
|
|
buf[i // 8] |= 1 << (7 - (i % 8))
|
|
|
|
|
|
|
|
return bytes(buf)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_TA_ENCODERS = {'utf8': encode_utf8, 'iso8': encode_iso8, '7bit': encode_7bit}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def encode_ta_buffer(text, text_format='utf8'):
|
|
|
|
|
|
|
|
"""Encode text into a 28-byte TA buffer in the requested format.
|
|
|
|
|
|
|
|
text_format: 'utf8' (default), 'iso8' (ISO-8859-1, best for Hytera), '7bit'."""
|
|
|
|
|
|
|
|
return _TA_ENCODERS.get(text_format, encode_utf8)(text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def blocks_from_buffer(buf):
|
|
|
|
def blocks_from_buffer(buf):
|
|
|
|
"""Split a 28-byte encoded buffer into four 7-byte DMRA payloads."""
|
|
|
|
"""Split a 28-byte encoded buffer into four 7-byte DMRA payloads."""
|
|
|
|
buf = buf.ljust(DMRA_BUF_LEN, b'\x00')[:DMRA_BUF_LEN]
|
|
|
|
buf = buf.ljust(DMRA_BUF_LEN, b'\x00')[:DMRA_BUF_LEN]
|
|
|
|
@ -121,10 +168,10 @@ def buffer_from_blocks(blocks):
|
|
|
|
# Standalone DMRA packet builders / parser
|
|
|
|
# Standalone DMRA packet builders / parser
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def build_dmra_packets(rf_src, text):
|
|
|
|
def build_dmra_packets(rf_src, text, text_format='utf8'):
|
|
|
|
"""Build the 1-4 HBP DMRA packets needed to carry 'text' (server injection)."""
|
|
|
|
"""Build the 1-4 HBP DMRA packets needed to carry 'text' (server injection)."""
|
|
|
|
rf = rf_src[:3] if len(rf_src) >= 3 else rf_src.ljust(3, b'\x00')[:3]
|
|
|
|
rf = rf_src[:3] if len(rf_src) >= 3 else rf_src.ljust(3, b'\x00')[:3]
|
|
|
|
encoded = encode_utf8(text)
|
|
|
|
encoded = encode_ta_buffer(text, text_format)
|
|
|
|
blocks = blocks_from_buffer(encoded)
|
|
|
|
blocks = blocks_from_buffer(encoded)
|
|
|
|
count = required_ta_block_count(encoded)
|
|
|
|
count = required_ta_block_count(encoded)
|
|
|
|
packets = []
|
|
|
|
packets = []
|
|
|
|
@ -199,13 +246,20 @@ def _encode_emblc(_lc):
|
|
|
|
return {1: out[0:32], 2: out[32:64], 3: out[64:96], 4: out[96:128]}
|
|
|
|
return {1: out[0:32], 2: out[32:64], 3: out[64:96], 4: out[96:128]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def encode_talker_alias_emblc(text):
|
|
|
|
def encode_talker_alias_emblc(text, text_formats=('utf8',)):
|
|
|
|
"""Embedded-LC dicts for TA blocks 0..N-1 plus block count N (1-4)."""
|
|
|
|
"""Embedded-LC dicts for the TA blocks. `text_formats` may be a single
|
|
|
|
encoded = encode_utf8(text)
|
|
|
|
encoding or several; with several, the TA is emitted in each encoding back to
|
|
|
|
blocks = blocks_from_buffer(encoded)
|
|
|
|
back (e.g. UTF-8 then ISO-8859-1) so radios of different vendors each pick up
|
|
|
|
count = required_ta_block_count(encoded)
|
|
|
|
the format they support. Returns (list-of-emblc-dicts, total-count)."""
|
|
|
|
emblcs = [_encode_emblc(talker_alias_lc_bytes(i, blocks[i])) for i in range(count)]
|
|
|
|
if isinstance(text_formats, str):
|
|
|
|
return emblcs, count
|
|
|
|
text_formats = [text_formats]
|
|
|
|
|
|
|
|
emblcs = []
|
|
|
|
|
|
|
|
for tf in (list(text_formats) or ['utf8']):
|
|
|
|
|
|
|
|
encoded = encode_ta_buffer(text, tf)
|
|
|
|
|
|
|
|
blocks = blocks_from_buffer(encoded)
|
|
|
|
|
|
|
|
cnt = required_ta_block_count(encoded)
|
|
|
|
|
|
|
|
emblcs += [_encode_emblc(talker_alias_lc_bytes(i, blocks[i])) for i in range(cnt)]
|
|
|
|
|
|
|
|
return emblcs, len(emblcs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def encode_talker_alias_emblc_from_blocks(blocks):
|
|
|
|
def encode_talker_alias_emblc_from_blocks(blocks):
|
|
|
|
@ -263,7 +317,17 @@ def ta_settings(CONFIG, system_name=None):
|
|
|
|
fmt = sys_cfg.get('TALKER_ALIAS_FORMAT')
|
|
|
|
fmt = sys_cfg.get('TALKER_ALIAS_FORMAT')
|
|
|
|
if fmt is None:
|
|
|
|
if fmt is None:
|
|
|
|
fmt = g.get('TALKER_ALIAS_FORMAT', '{callsign} {fname}')
|
|
|
|
fmt = g.get('TALKER_ALIAS_FORMAT', '{callsign} {fname}')
|
|
|
|
return {'enabled': bool(enabled), 'mode': mode, 'format': str(fmt)}
|
|
|
|
tfmt = sys_cfg.get('TALKER_ALIAS_TEXT_FORMAT')
|
|
|
|
|
|
|
|
if tfmt is None:
|
|
|
|
|
|
|
|
tfmt = g.get('TALKER_ALIAS_TEXT_FORMAT', 'utf8')
|
|
|
|
|
|
|
|
# comma-separated list -> emit the TA in each encoding back to back, so radios
|
|
|
|
|
|
|
|
# of different vendors each pick up the format they support (e.g. 'utf8,iso8'
|
|
|
|
|
|
|
|
# for Motorola + Hytera coexistence).
|
|
|
|
|
|
|
|
formats = [f.strip().lower() for f in str(tfmt).split(',')]
|
|
|
|
|
|
|
|
formats = [f for f in formats if f in ('utf8', 'iso8', '7bit')]
|
|
|
|
|
|
|
|
if not formats:
|
|
|
|
|
|
|
|
formats = ['utf8']
|
|
|
|
|
|
|
|
return {'enabled': bool(enabled), 'mode': mode, 'format': str(fmt), 'text_formats': formats}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ta_enabled(CONFIG, system_name=None):
|
|
|
|
def ta_enabled(CONFIG, system_name=None):
|
|
|
|
@ -351,13 +415,13 @@ def resolve_ta(CONFIG, system_name, rf_src, blocks):
|
|
|
|
'text': decode_ta(buffer_from_blocks(blocks))}
|
|
|
|
'text': decode_ta(buffer_from_blocks(blocks))}
|
|
|
|
if mode == 'inject':
|
|
|
|
if mode == 'inject':
|
|
|
|
return {'source': 'inject', 'blocks': None,
|
|
|
|
return {'source': 'inject', 'blocks': None,
|
|
|
|
'text': format_ta_text(CONFIG, rf_src)}
|
|
|
|
'text': format_ta_text(CONFIG, rf_src), 'text_formats': settings['text_formats']}
|
|
|
|
# both
|
|
|
|
# both
|
|
|
|
if have_passthrough:
|
|
|
|
if have_passthrough:
|
|
|
|
return {'source': 'passthrough', 'blocks': dict(blocks),
|
|
|
|
return {'source': 'passthrough', 'blocks': dict(blocks),
|
|
|
|
'text': decode_ta(buffer_from_blocks(blocks))}
|
|
|
|
'text': decode_ta(buffer_from_blocks(blocks))}
|
|
|
|
return {'source': 'inject', 'blocks': None,
|
|
|
|
return {'source': 'inject', 'blocks': None,
|
|
|
|
'text': format_ta_text(CONFIG, rf_src)}
|
|
|
|
'text': format_ta_text(CONFIG, rf_src), 'text_formats': settings['text_formats']}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ta_dmra_packets(rf_src, resolved):
|
|
|
|
def ta_dmra_packets(rf_src, resolved):
|
|
|
|
@ -366,7 +430,7 @@ def ta_dmra_packets(rf_src, resolved):
|
|
|
|
return []
|
|
|
|
return []
|
|
|
|
if resolved['source'] == 'passthrough':
|
|
|
|
if resolved['source'] == 'passthrough':
|
|
|
|
return passthrough_packets_from_blocks(rf_src, resolved['blocks'])
|
|
|
|
return passthrough_packets_from_blocks(rf_src, resolved['blocks'])
|
|
|
|
return build_dmra_packets(rf_src, resolved['text'])
|
|
|
|
return build_dmra_packets(rf_src, resolved['text'], (resolved.get('text_formats') or ['utf8'])[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ta_emblc(resolved):
|
|
|
|
def ta_emblc(resolved):
|
|
|
|
@ -375,7 +439,7 @@ def ta_emblc(resolved):
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
if resolved['source'] == 'passthrough':
|
|
|
|
if resolved['source'] == 'passthrough':
|
|
|
|
return encode_talker_alias_emblc_from_blocks(resolved['blocks'])
|
|
|
|
return encode_talker_alias_emblc_from_blocks(resolved['blocks'])
|
|
|
|
return encode_talker_alias_emblc(resolved['text'])
|
|
|
|
return encode_talker_alias_emblc(resolved['text'], resolved.get('text_formats', ['utf8']))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|