Repository: EricssonResearch/arms

Issue:
Communication with the ABB robot (IRC5)
A class that handles the communication with the IRC5. Requirements:
- Creating/ closing the socket.
- Sending/ receiving of messages in a readable way.
- Implementing of some example cases.

In addition, tests shall show that the class works as intended.

Comments:

Patch:
diff --git a/arms/units/__init__.py b/arms/units/__init__.py
new file mode 100644
index 0000000..631db19
--- /dev/null
+++ b/arms/units/__init__.py
@@ -0,0 +1,1 @@
+"""Modules related to units"""
diff --git a/arms/units/abb.py b/arms/units/abb.py
new file mode 100644
index 0000000..5d39fa1
--- /dev/null
+++ b/arms/units/abb.py
@@ -0,0 +1,137 @@
+"""Utilities related to the communication with the ABB robotic arm."""
+
+import ast
+import json
+from arms.misc.socket import SocketClient
+from arms.utils import log
+
+
+class ABB:
+    """Handles the communication with the ABB robot with respect to
+        - sending command requests to and
+        - receiving/evaluating the responds from the ABB robot.
+
+    Attributes:
+        client: The socket interface.
+    """
+    def __init__(self):
+        self.client = SocketClient("abb")
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.close()
+
+    def close(self):
+        """Close connection."""
+        case = 0
+        self._send(case)
+        self.client.close()
+
+    def get_system_information(self):
+        """Get system information."""
+        case = 1
+        ok, data = self._exchange(case)
+        return ok, data
+
+    def get_joint_coordinates(self):
+        """Get joint coordinates."""
+        case = 10
+        ok, data = self._exchange(case)
+        return ok, data
+
+    def _exchange(self, case, data=None):
+        """Exchanges messages with the ABB robot in two steps:
+            1. step: Send a command to the ABB robot.
+            2. step: Receive the answer from the ABB robot.
+
+        Moreover, the message to expect from the ABB robot is of JSON format
+        and is sent as a string; e.g.:
+            "{'id':1,'ok':1,'data':'[1, 2, 3]'}"
+        Hence, the 3. step is to convert this string into a dictionary.
+
+        Args:
+         case:  The case to address in the RAPID code running on the IRC5
+                (ABB robot).
+         data:  The data to send to the IRC5 (ABB robot), e.g. coordinates.
+         """
+
+        # 1. Send a command to the ABB robot.
+        ok = self._send(case, data)
+        if not ok:
+            return [False, []]
+
+        # 2. Receive the answer from the ABB robot.
+        [ok_client, recv_msg] = self.client.recv()
+        if not ok_client:
+            return [False, []]
+
+        # 3. Convert the received message into a dictionary.
+        recv_msg = recv_msg.replace("'", '\"')
+        recv_msg = recv_msg.replace("|", "'")
+        try:
+            recv_msg = json.loads(recv_msg)
+            recv_id = recv_msg['id']
+            recv_ok = bool(recv_msg['ok'])
+            recv_data = ast.literal_eval(recv_msg['data'])
+        except (OSError, ValueError, TypeError, KeyError) as e:
+            log.abb.error("The received message is of the wrong "
+                          "format. The received message is {}; reason for "
+                          "error: {}.".format(recv_msg, e))
+            return [False, []]
+
+        # Check if the stated case in the received message equals the
+        # given (expected) case.
+        if recv_id != case:
+            log.abb.critical("The case in the received message {} does not "
+                             "equal the given (expected) case.")
+            return [False, []]
+        else:
+            return [recv_ok, recv_data]
+
+    def _send(self, case, data=None):
+        """Sends a command to the ABB robot in the expected format:
+            <length of data in bytes><delimiter = \n><data>
+        , where
+             <length of data in bytes><delimiter = \n>: Handled by the
+                                                        class SocketClient.
+             <data>: array/list of 10 elements; first element is the case.
+
+        Args:
+            case:   The case to address in the RAPID code running on the IRC5
+                    (ABB robot).
+            data:   The data to send to the IRC5 (ABB robot), e.g. coordinates.
+
+        Return:
+            - True, if the message could be send to the ABB robot successfully.
+            - False, if not.
+        """
+        if not isinstance(case, int):
+            log.abb.error('The case has to be provided as an integer. The '
+                          'given case was: '.format(case))
+            return False
+
+        if data is None:
+            data = []
+
+        if len(str(data)) > 80:
+            log.abb.error('The data list {} is bigger than 80 bytes and '
+                          'therefore can not be send to the ABB robot.'
+                          .format(data))
+            return False
+
+        msg = [case]
+        msg = msg + data
+        if len(msg) <= 10:
+            msg = msg + (10-len(msg))*[0]
+        else:
+            log.abb.error('The data list {} has more than 9 elements and '
+                          'therefore can not be send to the ABB robot.'
+                          .format(data))
+            return False
+
+        log.abb.info('An attempt to send a command request to the ABB robot '
+                     'is made. The message is {}.'.format(msg))
+        ok = self.client.send(msg)
+        return ok
diff --git a/arms/utils/log.py b/arms/utils/log.py
index 672e910..68f1f0b 100644
--- a/arms/utils/log.py
+++ b/arms/utils/log.py
@@ -70,6 +70,7 @@ def get_logger(name):
 
 
 # The different loggers used (alphabetical order).
+abb = get_logger('abb')
 app = get_logger('app')
 ard_log = get_logger('ard_log')
 camera = get_logger('camera')


Test patch:
diff --git a/tests/unit/test_abb.py b/tests/unit/test_abb.py
new file mode 100644
index 0000000..eed3285
--- /dev/null
+++ b/tests/unit/test_abb.py
@@ -0,0 +1,118 @@
+"""Tests for arms.units.abb"""
+
+import pytest
+from unittest import mock
+from arms.units.abb import ABB
+
+
+def test_init():
+    """The instance attribute 'client' of the class 'ABB' shall be an instance
+    of the class 'SocketClient' with the attribute value 'abb'.
+    """
+    with mock.patch('arms.units.abb.SocketClient') as mock_client:
+        ABB()
+        mock_client.assert_called_with("abb")
+
+
+def test_with():
+    """The class 'ABB' shall be able to work with Python with statements and
+    by leaving its scope the connection to the ABB robot shall be closed.
+    """
+    with mock.patch('arms.units.abb.ABB.close') as mock_close:
+        with ABB():
+            pass
+        mock_close.assert_called()
+
+
+@pytest.mark.parametrize("case, msg, expected", [
+    (1, "{'id':1,'ok':1,'data':'[]'}", [True, []]),
+    (1, "{'id':1,'ok':1,'data':'[1,2,3]'}", [True, [1, 2, 3]]),
+])
+def test_exchange(case, msg, expected):
+    """WHEN the function '_exchange' is called, a command request to the ABB
+    robot shall be send by calling the function 'send(case, data)', and the
+    received message (answer) from the ABB robot shall be converted into a
+    dictionary.
+
+    Comment:
+        This requires a working connection to the ABB robot and in addition a
+        received message from the ABB robot of the format:
+            "{'id':<id number>,
+            'ok':<bool as number (1 = True, 0 = False)>,
+            'data':<array of variable size>}"
+    """
+    abb = ABB()
+
+    mock_send = mock.Mock()
+    abb._send = mock_send
+    mock_send.return_value = True
+
+    mock_client = mock.Mock()
+    abb.client = mock_client
+    mock_client.recv.return_value = [True, msg]
+
+    received = abb._exchange(case)
+    abb._send.assert_called_with(case, None)
+    assert received == expected
+
+
+@pytest.mark.parametrize("ok_send, case, ok_recv, msg, expected", [
+    (False, 1, True, "", [False, []]),
+    (True, 1, False, "{'id':1,'ok':1,'data':'[1,2,3]'}", [False, []]),
+    (True, 1, True, "Not expected.", [False, []]),
+    (True, 2, True, "{'id':1,'ok':1,'data':'[1,2,3]'}", [False, []]),
+])
+def test_exchange_error(ok_send, case, ok_recv, msg, expected):
+    """WHEN the function '_exchange' is called, IF there is no working
+    connection to the ABB robot or the received message from the ABB robot is
+    not of the expected form, THEN the boolean value False and an empty
+    list shall be returned.
+
+    Comment:
+        The format of the message to expect from the ABB robot is explained in
+        the test 'test_exchange'.
+    """
+    abb = ABB()
+
+    mock_send = mock.Mock()
+    abb._send = mock_send
+    mock_send.return_value = ok_send
+
+    mock_client = mock.Mock()
+    abb.client = mock_client
+    mock_client.recv.return_value = [ok_recv, msg]
+
+    received = abb._exchange(case)
+    assert received == expected
+
+
+@pytest.mark.parametrize("case, data, expected", [
+    (0, None, [0] + 9*[0]),
+    (1, [1, 2, 3], [1] + [1, 2, 3] + 6*[0]),
+])
+def test_send(case, data, expected):
+    """WHEN the function '_send' is called, a command request to the ABB robot
+    of the expected form for the data entry shall be made.
+    """
+    abb = ABB()
+
+    mock_send = mock.Mock()
+    abb.client = mock_send
+
+    abb._send(case, data)
+    abb.client.send.assert_called_with(expected)
+
+
+@pytest.mark.parametrize("case, data", [
+    ("a", []),
+    (1, None),
+    (1, 81*"a"),
+    (1, 10*[0]),
+])
+def test_send_error(case, data):
+    """WHEN the function '_send' is called, IF there is no working connection to
+    the ABB robot, THEN the boolean value False shall be returned.
+    """
+    abb = ABB()
+    ok = abb._send(case, data)
+    assert ok is False