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