From 94e2184d919ef43adee68df4acff4ab6c6ef39db Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman <jeffrey.freeman@syncleus.com> Date: Sat, 24 Sep 2016 09:06:20 -0400 Subject: [PATCH] Split up the kiss class into multiple implementations and refactored the rest of the code to match it. Issue #11 --- setup.py | 3 +- src/apex/aprs/__init__.py | 2 +- src/apex/aprs/{aprs_kiss.py => aprs.py} | 29 +++--- src/apex/cli.py | 13 +-- src/apex/kiss/__init__.py | 2 + src/apex/kiss/kiss.py | 109 +++++------------------ src/apex/kiss/kiss_serial.py | 113 ++++++++++++++++++++++++ src/apex/kiss/kiss_tcp.py | 89 +++++++++++++++++++ tox.ini | 1 - 9 files changed, 254 insertions(+), 107 deletions(-) rename src/apex/aprs/{aprs_kiss.py => aprs.py} (83%) create mode 100644 src/apex/kiss/kiss_serial.py create mode 100644 src/apex/kiss/kiss_tcp.py diff --git a/setup.py b/setup.py index c6e13b6..eea8e0e 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,8 @@ setup( 'Ham Radio', 'APEX', 'APRS' ], install_requires=[ - 'click', + 'click >= 6.6', + 'six >= 1.10.0', 'pynmea2 >= 1.4.2', 'pyserial >= 2.7', 'requests >= 2.7.0', diff --git a/src/apex/aprs/__init__.py b/src/apex/aprs/__init__.py index ec82aa9..9047faa 100644 --- a/src/apex/aprs/__init__.py +++ b/src/apex/aprs/__init__.py @@ -22,8 +22,8 @@ from __future__ import print_function import logging +from .aprs import Aprs # noqa: F401 from .aprs_internet_service import AprsInternetService # noqa: F401 -from .aprs_kiss import AprsKiss # noqa: F401 __author__ = 'Jeffrey Phillips Freeman (WI2ARD)' __maintainer__ = 'Jeffrey Phillips Freeman (WI2ARD)' diff --git a/src/apex/aprs/aprs_kiss.py b/src/apex/aprs/aprs.py similarity index 83% rename from src/apex/aprs/aprs_kiss.py rename to src/apex/aprs/aprs.py index 660effa..1c02f03 100644 --- a/src/apex/aprs/aprs_kiss.py +++ b/src/apex/aprs/aprs.py @@ -20,9 +20,12 @@ __copyright__ = 'Copyright 2016, Syncleus, Inc. and contributors' __credits__ = [] -class AprsKiss(apex.kiss.Kiss): +class Aprs(object): - """APRS interface for KISS serial devices.""" + """APRS interface.""" + + def __init__(self, data_stream): + self.data_Stream = data_stream @staticmethod def __decode_frame(raw_frame): @@ -48,9 +51,9 @@ class AprsKiss(apex.kiss.Kiss): if 1 < i < 11: if (raw_frame[raw_slice + 1] & 0x03 == 0x03 and raw_frame[raw_slice + 2] in [0xf0, 0xcf]): frame['text'] = ''.join(map(chr, raw_frame[raw_slice + 3:])) - frame['destination'] = AprsKiss.__identity_as_string(AprsKiss.__extract_callsign(raw_frame)) - frame['source'] = AprsKiss.__identity_as_string(AprsKiss.__extract_callsign(raw_frame[7:])) - frame['path'] = AprsKiss.__extract_path(int(i), raw_frame) + frame['destination'] = Aprs.__identity_as_string(Aprs.__extract_callsign(raw_frame)) + frame['source'] = Aprs.__identity_as_string(Aprs.__extract_callsign(raw_frame[7:])) + frame['path'] = Aprs.__extract_path(int(i), raw_frame) return frame logging.debug('frame=%s', frame) @@ -69,7 +72,7 @@ class AprsKiss(apex.kiss.Kiss): full_path = [] for i in range(2, start): - path = AprsKiss.__identity_as_string(AprsKiss.__extract_callsign(raw_frame[i * 7:])) + path = Aprs.__identity_as_string(Aprs.__extract_callsign(raw_frame[i * 7:])) if path: if raw_frame[i * 7 + 6] & 0x80: full_path.append(''.join([path, '*'])) @@ -117,10 +120,10 @@ class AprsKiss(apex.kiss.Kiss): :return: KISS-encoded APRS frame. :rtype: list """ - enc_frame = AprsKiss.__encode_callsign(AprsKiss.__parse_identity_string(frame['destination'])) +\ - AprsKiss.__encode_callsign(AprsKiss.__parse_identity_string(frame['source'])) + enc_frame = Aprs.__encode_callsign(Aprs.__parse_identity_string(frame['destination'])) + \ + Aprs.__encode_callsign(Aprs.__parse_identity_string(frame['source'])) for p in frame['path']: - enc_frame += AprsKiss.__encode_callsign(AprsKiss.__parse_identity_string(p)) + enc_frame += Aprs.__encode_callsign(Aprs.__parse_identity_string(p)) return enc_frame[:-1] + [enc_frame[-1] | 0x01] + [apex.kiss.constants.SLOT_TIME] + [0xf0] + frame['text'] @@ -179,14 +182,14 @@ class AprsKiss(apex.kiss.Kiss): :param frame: APRS frame to write to KISS device. :type frame: dict """ - encoded_frame = AprsKiss.__encode_frame(frame) - super(AprsKiss, self).write(encoded_frame, port) + encoded_frame = Aprs.__encode_frame(frame) + self.data_Stream.write(encoded_frame, port) def read(self): """Reads APRS-encoded frame from KISS device. """ - frame = super(AprsKiss, self).read() + frame = self.data_Stream.read() if frame is not None and len(frame): - return AprsKiss.__decode_frame(frame) + return Aprs.__decode_frame(frame) else: return None diff --git a/src/apex/cli.py b/src/apex/cli.py index c5261c3..dc4f181 100644 --- a/src/apex/cli.py +++ b/src/apex/cli.py @@ -97,16 +97,17 @@ def main(verbose, configfile): if config.has_option(section, 'com_port') and config.has_option(section, 'baud'): com_port = config.get(section, 'com_port') baud = config.get(section, 'baud') - kiss_tnc = apex.aprs.AprsKiss(com_port=com_port, baud=baud) + kiss_tnc = apex.kiss.KissSerial(com_port=com_port, baud=baud) elif config.has_option(section, 'tcp_host') and config.has_option(section, 'tcp_port'): tcp_host = config.get(section, 'tcp_host') tcp_port = config.get(section, 'tcp_port') - kiss_tnc = apex.aprs.AprsKiss(host=tcp_host, tcp_port=tcp_port) + kiss_tnc = apex.kiss.KissTcp(host=tcp_host, tcp_port=tcp_port) else: click.echo(click.style('Error: ', fg='red', bold=True, blink=True) + click.style("""Invalid configuration, must have both com_port and baud set or tcp_host and tcp_port set in TNC sections of configuration file""", bold=True)) return + aprs_tnc = apex.aprs.Aprs(data_stream=kiss_tnc) if not config.has_option(section, 'kiss_init'): click.echo(click.style('Error: ', fg='red', bold=True, blink=True) + @@ -115,11 +116,11 @@ def main(verbose, configfile): return kiss_init_string = config.get(section, 'kiss_init') if kiss_init_string == 'MODE_INIT_W8DED': - kiss_tnc.start(kissConstants.MODE_INIT_W8DED) + aprs_tnc.start(kissConstants.MODE_INIT_W8DED) elif kiss_init_string == 'MODE_INIT_KENWOOD_D710': - kiss_tnc.start(kissConstants.MODE_INIT_KENWOOD_D710) + aprs_tnc.start(kissConstants.MODE_INIT_KENWOOD_D710) elif kiss_init_string == 'NONE': - kiss_tnc.start() + aprs_tnc.start() else: click.echo(click.style('Error: ', fg='red', bold=True, blink=True) + click.style('Invalid configuration, value assigned to kiss_init was not recognized: %s' @@ -131,7 +132,7 @@ def main(verbose, configfile): port_identifier = config.get(port_section, 'identifier') port_net = config.get(port_section, 'net') tnc_port = int(config.get(port_section, 'tnc_port')) - port_map[port_name] = {'identifier': port_identifier, 'net': port_net, 'tnc': kiss_tnc, + port_map[port_name] = {'identifier': port_identifier, 'net': port_net, 'tnc': aprs_tnc, 'tnc_port': tnc_port} if config.has_section('APRS-IS'): aprsis_callsign = config.get('APRS-IS', 'callsign') diff --git a/src/apex/kiss/__init__.py b/src/apex/kiss/__init__.py index 9db9ef0..256a7a5 100644 --- a/src/apex/kiss/__init__.py +++ b/src/apex/kiss/__init__.py @@ -22,6 +22,8 @@ from __future__ import print_function import logging from .kiss import Kiss # noqa: F401 +from .kiss_serial import KissSerial # noqa: F401 +from .kiss_tcp import KissTcp # noqa: F401 __author__ = 'Jeffrey Phillips Freeman (WI2ARD)' __maintainer__ = 'Jeffrey Phillips Freeman (WI2ARD)' diff --git a/src/apex/kiss/kiss.py b/src/apex/kiss/kiss.py index 0b66c43..6751e62 100644 --- a/src/apex/kiss/kiss.py +++ b/src/apex/kiss/kiss.py @@ -9,8 +9,9 @@ from __future__ import division from __future__ import print_function import logging -import socket -import serial +from abc import ABCMeta +from abc import abstractmethod +from six import with_metaclass from apex.kiss import constants as kissConstants @@ -22,9 +23,9 @@ __copyright__ = 'Copyright 2016, Syncleus, Inc. and contributors' __credits__ = [] -class Kiss(object): +class Kiss(with_metaclass(ABCMeta, object)): - """KISS Object Class.""" + """Abstract KISS Object Class.""" logger = logging.getLogger(__name__) logger.setLevel(kissConstants.LOG_LEVEL) @@ -37,58 +38,10 @@ class Kiss(object): frame_buffer = [] - def __init__(self, com_port=None, - baud=38400, - parity=serial.PARITY_NONE, - stop_bits=serial.STOPBITS_ONE, - byte_size=serial.EIGHTBITS, - host=None, - tcp_port=8000, - strip_df_start=True): - self.com_port = com_port - self.baud = baud - self.parity = parity - self.stop_bits = stop_bits - self.byte_size = byte_size - self.host = host - self.tcp_port = tcp_port - self.interface = None - self.interface_mode = None + def __init__(self, strip_df_start=True): self.strip_df_start = strip_df_start self.exit_kiss = False - if self.com_port is not None: - self.interface_mode = 'serial' - elif self.host is not None: - self.interface_mode = 'tcp' - if self.interface_mode is None: - raise Exception('Must set port/speed or host/tcp_port.') - - self.logger.info('Using interface_mode=%s', self.interface_mode) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if 'tcp' in self.interface_mode: - self.interface.shutdown() - elif self.interface and self.interface.isOpen(): - self.interface.close() - - def __del__(self): - if self.interface and self.interface.isOpen(): - self.interface.close() - - def __read_interface(self): - if 'tcp' in self.interface_mode: - return self.interface.recv(kissConstants.READ_BYTES) - elif 'serial' in self.interface_mode: - read_data = self.interface.read(kissConstants.READ_BYTES) - waiting_data = self.interface.inWaiting() - if waiting_data: - read_data += self.interface.read(waiting_data) - return map(ord, read_data) - @staticmethod def __strip_df_start(frame): """ @@ -143,42 +96,31 @@ class Kiss(object): raise Exception('command_Code out of range') return (port << 4) & command_code + @abstractmethod + def _read_interface(self): + pass + + @abstractmethod + def _write_interface(self, data): + pass + + @abstractmethod def start(self, mode_init=None, **kwargs): """ Initializes the KISS device and commits configuration. + This method is abstract and must be implemented by a concrete class. + See http://en.wikipedia.org/wiki/KISS_(TNC)#Command_codes for configuration names. :param **kwargs: name/value pairs to use as initial config values. """ - self.logger.debug('kwargs=%s', kwargs) - - if 'tcp' in self.interface_mode: - address = (self.host, self.tcp_port) - self.interface = socket.create_connection(address) - elif 'serial' in self.interface_mode: - self.interface = serial.Serial(port=self.com_port, baudrate=self.baud, parity=self.parity, - stopbits=self.stop_bits, bytesize=self.byte_size) - self.interface.timeout = kissConstants.SERIAL_TIMEOUT - if mode_init is not None: - self.interface.write(mode_init) - self.exit_kiss = True - - # Previous verious defaulted to Xastir-friendly configs. Unfortunately - # those don't work with Bluetooth TNCs, so we're reverting to None. - if 'serial' in self.interface_mode and kwargs: - for name, value in kwargs.items(): - self.write_setting(name, value) - - # If no settings specified, default to config values similar - # to those that ship with Xastir. - # if not kwargs: - # kwargs = kiss.constants.DEFAULT_KISS_CONFIG_VALUES + pass def close(self): if self.exit_kiss is True: - self.interface.write(kissConstants.MODE_END) + self._write_interface(kissConstants.MODE_END) def write_setting(self, name, value): """ @@ -195,7 +137,7 @@ class Kiss(object): if isinstance(value, int): value = chr(value) - return self.interface.write( + return self._write_interface( kissConstants.FEND + getattr(kissConstants, name.upper()) + Kiss.__escape_special_codes(value) + @@ -209,7 +151,7 @@ class Kiss(object): new_frames = [] read_buffer = [] - read_data = self.__read_interface() + read_data = self._read_interface() while read_data is not None and len(read_data): split_data = [[]] for read_byte in read_data: @@ -243,7 +185,7 @@ class Kiss(object): if split_data[len_fend - 1]: read_buffer = split_data[len_fend - 1] # Get anymore data that is waiting - read_data = self.__read_interface() + read_data = self._read_interface() for new_frame in new_frames: if len(new_frame) and new_frame[0] == 0: @@ -268,10 +210,7 @@ class Kiss(object): :param frame: Frame to write. """ - kiss_packet = [kissConstants.FEND] + [Kiss.__command_byte_combine(port, kissConstants.DATA_FRAME)] +\ + kiss_packet = [kissConstants.FEND] + [Kiss.__command_byte_combine(port, kissConstants.DATA_FRAME)] + \ Kiss.__escape_special_codes(frame_bytes) + [kissConstants.FEND] - if 'tcp' in self.interface_mode: - return self.interface.send(bytearray(kiss_packet)) - elif 'serial' in self.interface_mode: - return self.interface.write(kiss_packet) + return self._write_interface(kiss_packet) diff --git a/src/apex/kiss/kiss_serial.py b/src/apex/kiss/kiss_serial.py new file mode 100644 index 0000000..5ffc3d2 --- /dev/null +++ b/src/apex/kiss/kiss_serial.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""KISS Core Classes.""" + +# These imports are for python3 compatability inside python2 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import logging +import serial + +from apex.kiss import constants as kissConstants + +from .kiss import Kiss + +__author__ = 'Jeffrey Phillips Freeman (WI2ARD)' +__maintainer__ = 'Jeffrey Phillips Freeman (WI2ARD)' +__email__ = 'jeffrey.freeman@syncleus.com' +__license__ = 'Apache License, Version 2.0' +__copyright__ = 'Copyright 2016, Syncleus, Inc. and contributors' +__credits__ = [] + + +class KissSerial(Kiss): + + """KISS Serial Object Class.""" + + logger = logging.getLogger(__name__) + logger.setLevel(kissConstants.LOG_LEVEL) + console_handler = logging.StreamHandler() + console_handler.setLevel(kissConstants.LOG_LEVEL) + formatter = logging.Formatter(kissConstants.LOG_FORMAT) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + logger.propagate = False + + frame_buffer = [] + + def __init__(self, strip_df_start=True, + com_port=None, + baud=38400, + parity=serial.PARITY_NONE, + stop_bits=serial.STOPBITS_ONE, + byte_size=serial.EIGHTBITS): + super(KissSerial, self).__init__(strip_df_start) + + self.com_port = com_port + self.baud = baud + self.parity = parity + self.stop_bits = stop_bits + self.byte_size = byte_size + self.serial = None + self.strip_df_start = strip_df_start + self.exit_kiss = False + + self.logger.info('Using interface_mode=Serial') + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.serial.close() + + def __del__(self): + if self.serial and self.serial.isOpen(): + self.serial.close() + + def _read_interface(self): + read_data = self.serial.read(kissConstants.READ_BYTES) + waiting_data = self.serial.inWaiting() + if waiting_data: + read_data += self.serial.read(waiting_data) + return map(ord, read_data) + + def _write_interface(self, data): + self.serial.write(data) + + def start(self, mode_init=None, **kwargs): + """ + Initializes the KISS device and commits configuration. + + See http://en.wikipedia.org/wiki/KISS_(TNC)#Command_codes + for configuration names. + + :param **kwargs: name/value pairs to use as initial config values. + """ + self.logger.debug('kwargs=%s', kwargs) + + self.serial = serial.Serial(port=self.com_port, baudrate=self.baud, parity=self.parity, + stopbits=self.stop_bits, bytesize=self.byte_size) + self.serial.timeout = kissConstants.SERIAL_TIMEOUT + if mode_init is not None: + self.serial.write(mode_init) + self.exit_kiss = True + + # Previous verious defaulted to Xastir-friendly configs. Unfortunately + # those don't work with Bluetooth TNCs, so we're reverting to None. + if kwargs: + for name, value in kwargs.items(): + super(KissSerial, self).write_setting(name, value) + + # If no settings specified, default to config values similar + # to those that ship with Xastir. + # if not kwargs: + # kwargs = kiss.constants.DEFAULT_KISS_CONFIG_VALUES + + def close(self): + if not self.serial: + raise RuntimeError('Attempting to close before the class has been started.') + elif self.serial.isOpen(): + self.serial.close() diff --git a/src/apex/kiss/kiss_tcp.py b/src/apex/kiss/kiss_tcp.py new file mode 100644 index 0000000..f9df262 --- /dev/null +++ b/src/apex/kiss/kiss_tcp.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""KISS Core Classes.""" + +# These imports are for python3 compatability inside python2 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import logging +import socket + +from apex.kiss import constants as kissConstants +from .kiss import Kiss + +__author__ = 'Jeffrey Phillips Freeman (WI2ARD)' +__maintainer__ = 'Jeffrey Phillips Freeman (WI2ARD)' +__email__ = 'jeffrey.freeman@syncleus.com' +__license__ = 'Apache License, Version 2.0' +__copyright__ = 'Copyright 2016, Syncleus, Inc. and contributors' +__credits__ = [] + + +class KissTcp(Kiss): + + """KISS TCP Object Class.""" + + logger = logging.getLogger(__name__) + logger.setLevel(kissConstants.LOG_LEVEL) + console_handler = logging.StreamHandler() + console_handler.setLevel(kissConstants.LOG_LEVEL) + formatter = logging.Formatter(kissConstants.LOG_FORMAT) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + logger.propagate = False + + frame_buffer = [] + + def __init__(self, + strip_df_start=True, + host=None, + tcp_port=8000): + super(KissTcp, self).__init__(strip_df_start) + + self.host = host + self.tcp_port = tcp_port + self.socket = None + self.strip_df_start = strip_df_start + self.exit_kiss = False + + self.logger.info('Using interface_mode=TCP') + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.socket.close() + + def _read_interface(self): + return self.socket.recv(kissConstants.READ_BYTES) + + def _write_interface(self, data): + self.socket.write(data) + + def start(self, mode_init=None, **kwargs): + """ + Initializes the KISS device and commits configuration. + + See http://en.wikipedia.org/wiki/KISS_(TNC)#Command_codes + for configuration names. + + :param **kwargs: name/value pairs to use as initial config values. + """ + self.logger.debug('kwargs=%s', kwargs) + + address = (self.host, self.tcp_port) + self.socket = socket.create_connection(address) + + def close(self): + if not self.socket: + raise RuntimeError('Attempting to close before the class has been started.') + + super(KissTcp, self).close() + self.socket.shutdown() + self.socket.close() + + def shutdown(self): + self.socket.shutdown() diff --git a/tox.ini b/tox.ini index 2301f01..df1fa4e 100644 --- a/tox.ini +++ b/tox.ini @@ -75,7 +75,6 @@ commands = check-manifest {toxinidir} flake8 src tests setup.py isort --verbose --check-only --diff --recursive src tests setup.py -; pylint -r n src/apex tests setup.py [testenv:coveralls] deps = -- GitLab