Module pycti.messages

Expand source code
import struct
import logging
import re
from copy import deepcopy
from abc import ABC

logger = logging.getLogger(__name__)


class MessageABC(ABC):

    # The length of the message. Should be overwritten in child class
    msg_length = 0

    # The message command code. Should be overwritten in child class
    command_code = 0x00

    # Template that is specific to each message type. Should be overwritten in child class
    msg_specific_template = {}

    # Base message template that is common for all messages
    base_template = {
        'header': {
            'format': '<Q',
            'start_byte': 0,
            'value': 0x11DDDDDDDDDDDDDD
        },
        'msg_length': {
            'format': '<L',
            'start_byte': 8,
            'value': 0
        },
        'command_code': {
            'format': '<L',
            'start_byte': 12,
            'value': 0x00000000
        },
        'extended_command_code': {
            'format': '<L',
            'start_byte': 16,
            'value': 0x00000000
        },
    }

    @classmethod
    def unpack(cls, msg_bin: bytearray) -> dict:
        """
        Parses the passed message and decodes it with the msg_encoding dict.
        Each key in the output message will have name of the key from the 
        msg_encoding dictionary.

        Parameters
        ----------
        msg_bin : bytearry
            The message to unpack.

        Returns
        -------
        decoded_msg_dict : dict
            The message items decoded into a dictionary.
        """
        decoded_msg_dict = {}

        # Create a template to unpack message with
        template = {**deepcopy(cls.base_template),
                   **deepcopy(cls.msg_specific_template)}

        for item_name, item in template.items():
            start_idx = item['start_byte']
            end_idx = item['start_byte'] + struct.calcsize(item['format'])
            decoded_msg_dict[item_name] = struct.unpack(
                item['format'], msg_bin[start_idx:end_idx])[0]

            # Decode and strip trailing 0x00s from strings.
            if item['format'].endswith('s'):
                decoded_msg_dict[item_name] = decoded_msg_dict[item_name].decode(
                    item['text_encoding']).rstrip('\x00')

        if decoded_msg_dict['command_code'] != cls.command_code:
            logger.warning(
                f'Decoded command code {decoded_msg_dict["command_code"]} does not match what was expected!')

        if decoded_msg_dict['msg_length'] != cls.msg_length:
            logger.warning(
                f'Decoded message length {decoded_msg_dict["msg_length"]} does not match what was expected!')

        return decoded_msg_dict

    @classmethod
    def pack(cls, msg_values={}) -> bytearray:
        """
        Packs a message based on the message encoding given in the msg_specific_template
        dictionary. Values can be substituted for default values if they are included 
        in the `msg_values` argument.

        Parameters
        ----------
        msg_values : dict
            A dictionary detailing which default values in the message temple should be 
            updated.

        Returns
        -------
        msg_bin : bytearray
            Packed response message.
        """
        # Create a template to build messages from
        template = {**deepcopy(cls.base_template),
                   **deepcopy(cls.msg_specific_template)}

        # Update the template with message specific length and command code
        template['msg_length']['value'] = cls.msg_length
        template['command_code']['value'] = cls.command_code

        # Create a message bytearray that will be loaded with message contents
        msg_bin = bytearray(template['msg_length']['value'])

        # Update default message values with those in the passed msg_values dict
        for key in msg_values.keys():
            if key in template.keys():
                template[key]['value'] = msg_values[key]
            else:
                logger.warning(
                    f'Key name {key} was not found in msg_encoding!')

        # Pack each item in template. If packing any item fails then abort packing.
        for item_name, item in template.items():
            logger.debug(f'Packing item {item_name}')
            try:
                if item['format'].endswith('s') or item['format'].endswith('c'):
                    packed_item = struct.pack(
                        item['format'],
                        item['value'].encode(item['text_encoding']))
                else:
                    packed_item = struct.pack(
                        item['format'], item['value'])
            except struct.error as e:
                logger.error(
                    f'Error packing {item_name} with fields {item}!')
                logger.error(e)
                msg_bin = bytearray([])
                break

            start_idx = item['start_byte']
            end_idx = item['start_byte'] + struct.calcsize(item['format'])
            msg_bin[start_idx:end_idx] = packed_item

        # Append a checksum to the end of the message
        if msg_bin:
            msg_bin += struct.pack('<H', sum(msg_bin))

        return msg_bin


class Msg:
    class Login:
        '''
        Message for logging into Arbin cycler. See
        CTI_REQUEST_LOGIN/CTI_REQUEST_LOGIN_FEEDBACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 74
            command_code = 0xEEAB0001

            msg_specific_template = {
                'username': {
                    'format': '32s',
                    'start_byte': 20,
                    'text_encoding': 'utf-8',
                    'value': 'not a username'
                },
                'password': {
                    'format': '32s',
                    'start_byte': 52,
                    'text_encoding': 'utf-8',
                    'value': 'not a password'
                },
            }

        class Server(MessageABC):
            msg_length = 8678
            command_code = 0xEEBA0001

            msg_specific_template = {
                'result': {
                    'format': 'I',
                    'start_byte': 20,
                    'value': 1
                },
                'ip_address': {
                    'format': '4s',
                    'start_byte': 24,
                    'value': "0000",
                    'text_encoding': 'utf-8',
                },
                'cycler_sn': {
                    'format': '16s',
                    'start_byte': 28,
                    'value': '00000000',
                    'text_encoding': 'ascii',
                },
                'note': {
                    'format': '256s',
                    'start_byte': 44,
                    'value': '00000000',
                    'text_encoding': 'ascii',
                },
                'nick_name': {
                    # Stored as wchar_t[1024]. Each wchar_t is 2 bytes, twice as big as standard char in Python
                    'format': '2048s',
                    'start_byte': 300,
                    'value': 'our nickname',
                    'text_encoding': 'utf-16-le',
                },
                'location': {
                    'format': '2048s',
                    'start_byte': 2348,
                    'value': 'our location',
                    'text_encoding': 'utf-16-le',
                },
                'emergency_contact': {
                    'format': '2048s',
                    'start_byte': 4396,
                    'value': 'our location',
                    'text_encoding': 'utf-16-le',
                },
                'other_comments': {
                    'format': '2048s',
                    'start_byte': 6444,
                    'value': 'our location',
                    'text_encoding': 'utf-16-le',
                },
                'email': {
                    'format': '128s',
                    'start_byte': 8492,
                    'value': 'our location',
                    'text_encoding': 'utf-16-le',
                },
                'call': {
                    'format': '32s',
                    'start_byte': 8620,
                    'value': 'our location',
                    'text_encoding': 'utf-16-le',
                },
                'itac': {
                    'format': '<I',
                    'start_byte': 8652,
                    'value': 0
                },
                'version': {
                    'format': '<I',
                    'start_byte': 8656,
                    'value': 0
                },
                'allow_control': {
                    'format': '<I',
                    'start_byte': 8660,
                    'value': 0
                },
                'num_channels': {
                    'format': '<I',
                    'start_byte': 8664,
                    'value': 0
                },
                'user_type': {
                    'format': '<I',
                    'start_byte': 8668,
                    'value': 1,
                    'text_encoding': 'utf-16-le',
                },
                'picture_length': {
                    'format': '<I',
                    'start_byte': 8672,
                    'value': 0,
                    'text_encoding': 'utf-8',
                },
            }

            login_result_dict = {
                0: "should not see this",
                1: "success",
                2: "fail",
                3: "already logged in"
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but converts the result based on the
                login_result_dict.

                Parameters
                ----------
                msg_bin : bytearray
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)
                msg_dict['result'] = cls.login_result_dict[msg_dict['result']]
                return msg_dict

    class ChannelInfo:
        '''
        Message for getting channel info from cycler. See
        CTI_REQUEST_GET_CHANNELS_INFO/CTI_REQUEST_GET_CHANNELS_INFO_FEED_BACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 50
            command_code = 0xEEAB0003

            msg_specific_template = {
                'channel': {
                    'format': '<h',
                    'start_byte': 20,
                    'value': 0
                },
                'channel_selection': {
                    'format': '<h',
                    'start_byte': 22,
                    'value': 1
                },
                'aux_options': {
                    'format': '<I',
                    'start_byte': 24,
                    'value': 0x00
                },
                'reserved': {
                    'format': '32s',
                    'start_byte': 28,
                    'value': ''.join(['\0' for i in range(32)]),
                    'text_encoding': 'utf-8',
                },
            }

        class Server(MessageABC):

            # Default message length for 1 channel with no aux readings. Will be larger as those grow.
            msg_length = 1779
            command_code = 0xEEBA0003

            msg_specific_template = {
                'number_of_channels': {
                    'format': '<I',
                    'start_byte': 20,
                    'value': 1
                },
                'channel': {
                    'format': '<I',
                    'start_byte': 24,
                    'value': 0
                },
                'status': {
                    'format': '<h',
                    'start_byte': 28,
                    'value': 0x00
                },
                'comm_failure': {
                    'format': '<B',
                    'start_byte': 30,
                    'value': 0
                },
                'schedule': {
                    # Stored as wchar_t[200]. Each wchar_t is 2 bytes, twice as big as standard char in Python
                    'format': '400s',
                    'start_byte': 31,
                    'value': 'fake_schedule',
                    'text_encoding': 'utf-16-le',
                },
                'testname': {
                    # Stored as wchar_t[72]
                    'format': '144s',
                    'start_byte': 431,
                    'value': 'fake_testname',
                    'text_encoding': 'utf-16-le',
                },
                'exit_condition': {
                    'format': '100s',
                    'start_byte': 575,
                    'value': 'none',
                    'text_encoding': 'utf-8',
                },
                'step_and_cycle_format': {
                    'format': '64s',
                    'start_byte': 675,
                    'value': 'none',
                    'text_encoding': 'utf-8',
                },
                # Stored as wchar_t[72]
                'barcode': {
                    'format': '144s',
                    'start_byte': 739,
                    'value': 'none',
                    'text_encoding': 'utf-16',
                },
                # Stored as wchar_t[72]
                'can_config_name': {
                    'format': '400s',
                    'start_byte': 883,
                    'value': 'none',
                    'text_encoding': 'utf-16',
                },
                # Stored as wchar_t[72]
                'smb_config_name': {
                    'format': '400s',
                    'start_byte': 1283,
                    'value': 'none',
                    'text_encoding': 'utf-16',
                },
                'master_channel': {
                    'format': '<H',
                    'start_byte': 1683,
                    'value': 0,
                },
                'test_time_s': {
                    'format': '<d',
                    'start_byte': 1685,
                    'value': 0,
                },
                'step_time_s': {
                    'format': '<d',
                    'start_byte': 1693,
                    'value': 0,
                },
                'voltage_v': {
                    'format': '<f',
                    'start_byte': 1701,
                    'value': 0,
                },
                'current_a': {
                    'format': '<f',
                    'start_byte': 1705,
                    'value': 0,
                },
                'power_w': {
                    'format': '<f',
                    'start_byte': 1709,
                    'value': 0,
                },
                'charge_capacity_ah': {
                    'format': '<f',
                    'start_byte': 1713,
                    'value': 0,
                },
                'discharge_capacity_ah': {
                    'format': '<f',
                    'start_byte': 1717,
                    'value': 0,
                },
                'charge_energy_wh': {
                    'format': '<f',
                    'start_byte': 1721,
                    'value': 0,
                },
                'discharge_energy_wh': {
                    'format': '<f',
                    'start_byte': 1725,
                    'value': 0,
                },
                'internal_resistance_ohm': {
                    'format': '<f',
                    'start_byte': 1729,
                    'value': 0,
                },
                'dvdt_vbys': {
                    'format': '<f',
                    'start_byte': 1733,
                    'value': 0,
                },
                'acr_ohm': {
                    'format': '<f',
                    'start_byte': 1737,
                    'value': 0,
                },
                'aci_ohm': {
                    'format': '<f',
                    'start_byte': 1741,
                    'value': 0,
                },
                'aci_phase_degrees': {
                    'format': '<f',
                    'start_byte': 1745,
                    'value': 0,
                },
                'aux_voltage_count': {
                    'format': '<H',
                    'start_byte': 1749,
                    'value': 0,
                },
                'aux_temperature_count': {
                    'format': '<H',
                    'start_byte': 1751,
                    'value': 0,
                },
                'aux_pressure_count': {
                    'format': '<H',
                    'start_byte': 1753,
                    'value': 0,
                },
                'aux_external_count': {
                    'format': '<H',
                    'start_byte': 1755,
                    'value': 0,
                },
                'aux_flow_count': {
                    'format': '<H',
                    'start_byte': 1757,
                    'value': 0,
                },
                'aux_ao_count': {
                    'format': '<H',
                    'start_byte': 1759,
                    'value': 0,
                },
                'aux_di_count': {
                    'format': '<H',
                    'start_byte': 1761,
                    'value': 0,
                },
                'aux_do_count': {
                    'format': '<H',
                    'start_byte': 1763,
                    'value': 0,
                },
                'aux_humidity_count': {
                    'format': '<H',
                    'start_byte': 1765,
                    'value': 0,
                },
                'aux_safety_count': {
                    'format': '<H',
                    'start_byte': 1767,
                    'value': 0,
                },
                'aux_ph_count': {
                    'format': '<H',
                    'start_byte': 1769,
                    'value': 0,
                },
                'aux_density_count': {
                    'format': '<H',
                    'start_byte': 1771,
                    'value': 0,
                },
                'bms_count': {
                    'format': '<H',
                    'start_byte': 1773,
                    'value': 0,
                },
                'smb_count': {
                    'format': '<H',
                    'start_byte': 1775,
                    'value': 0,
                },
            }

            # List of staus codes. Each index in the corresponding status code.
            status_code_dict = {
                0: 'Idle',
                1: 'Transition',
                2: 'Charge',
                3: 'Disharge',
                4: 'Rest',
                5: 'Wait',
                6: 'External Charge',
                7: 'Calibration',
                8: 'Unsafe',
                9: 'Pulse',
                10: 'Internal Resistance',
                11: 'AC Impedance',
                12: 'ACI Cell',
                13: 'Test Settings',
                14: 'Error',
                15: 'Finished',
                16: 'Volt Meter',
                17: 'Waiting for ACS',
                18: 'Pause',
                19: 'Empty',
                20: 'Idle from MCU',
                21: 'Start',
                22: 'Running',
                23: 'Step Transfer',
                24: 'Resume',
                25: 'Go Pause',
                26: 'Go Stop',
                27: 'Go Next Step',
                28: 'Online Update',
                29: 'DAQ Memory Unsafe',
                30: 'ACR'
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but uses aux counts to unpack aux readings

                Parameters
                ----------
                msg_bin : bytearry
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)
                msg_dict = cls.aux_readings_parser(
                    msg_dict, msg_bin, starting_aux_idx=1777)
                msg_dict['status'] = cls.status_code_dict[msg_dict['status']]
                return msg_dict

            @classmethod
            def pack(cls, msg_values={}) -> bytearray:
                """
                Same as parent method, but handles packing aux measurements.

                Parameters
                ----------
                msg_values : dict
                    A dictionary detailing which default values in the message temple should be 
                    updated.

                Returns
                -------
                msg : bytearray
                    Packed response message.
                """
                # TODO : Modify so that we can aux values can be packed.
                msg_bin = super().pack(msg_values)
                return msg_bin

            @classmethod
            def aux_readings_parser(cls, msg_dict: dict, msg_bin: bytearray, starting_aux_idx=1777):
                """
                Parses the auxiliary readings in msg_bin based on the aux readings
                counts in msg_dict. Aux readings are then added as items to the msg_dict. 

                Parameters
                ----------
                msg_dict : dict
                    A dictionary containing the aux readings counts (aux_voltage_count, aux_voltage_count, etc)
                msg_bin : bytearray
                    The message to unpack as a byte array.
                starting_aux_idx : int
                    The starting index in the msg_bin for aux readings. 1777 in single channel messages

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                aux_lists = []

                aux_count_name_list = [
                    'aux_voltage_count',
                    'aux_temperature_count',
                    'aux_pressure_count',
                    'aux_external_count',
                    'aux_flow_count',
                    'aux_ao_count',
                    'aux_di_count',
                    'aux_do_count',
                    'aux_humidity_count',
                    'aux_safety_count',
                    'aux_ph_count',
                    'aux_density_count'
                ]

                # Generate a list of readings for each aux reading.
                # If count is non-zero then genreate a aux_reading and aux_reading_dt list of that length
                # Else, generate empty lists for the aux_reading and aux_reading_dt
                for aux_count_name in aux_count_name_list:
                    aux_reading_name = re.split('_count', aux_count_name)[0]
                    aux_dt_name = aux_reading_name + '_dt'
                    if msg_dict[aux_count_name]:
                        msg_dict[aux_reading_name] = [0 for x in range(
                            msg_dict[aux_count_name])]
                        msg_dict[aux_dt_name] = [0 for x in range(
                            msg_dict[aux_count_name])]
                        aux_lists.append(
                            [msg_dict[aux_reading_name], msg_dict[aux_dt_name]])
                    else:
                        msg_dict[aux_reading_name] = []
                        msg_dict[aux_dt_name] = []

                # For aux readings that have a measurements, add them to the respective reading list.
                current_aux_idx = starting_aux_idx
                for readings_list in aux_lists:
                    for i in range(0, len(readings_list[0])):
                        # The first list in reading list is reading itself
                        readings_list[0][i] = struct.unpack(
                            '<f', msg_bin[current_aux_idx:current_aux_idx+4])[0]
                        # The second reading in the list is the dt value.
                        readings_list[1][i] = struct.unpack(
                            '<f', msg_bin[current_aux_idx+4:current_aux_idx+8])[0]
                        current_aux_idx += 8

                return msg_dict

    class AssignSchedule:
        '''
        Message for assiging a schedule to a specific channel. See
        THIRD_PARTY_ASSIGN_SCHEDULE/THIRD_PARTY_ASSIGN_SCHEDULE_FEEDBACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 659
            command_code = 0xBB210001

            msg_specific_template = {
                'channel': {
                    'format': 'i',
                    'start_byte': 20,
                    'value': 0
                },
                # Always 0x00 for PyCTI since we only work with single channels
                'assign_all_channels': {
                    'format': '1s',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'schedule': {
                    # Stored as wchar_t[200]. Each wchar_t is 2 bytes, twice as big as standard char in Python
                    'format': '400s',
                    'start_byte': 25,
                    'value': 'fake_schedule',
                    'text_encoding': 'utf-16-le',
                },
                'test_capacity_ah': {
                    'format': '<f',
                    'start_byte': 425,
                    'value': 1.0,
                },
                'barcode': {
                    'format': '144s',
                    'start_byte': 429,
                    'value': '',
                    'text_encoding': 'utf-16-le',
                },
                'user_variable_1': {
                    'format': '<f',
                    'start_byte': 573,
                    'value': 1.0,
                },
                'user_variable_2': {
                    'format': '<f',
                    'start_byte': 577,
                    'value': 1.0,
                },
                'user_variable_3': {
                    'format': '<f',
                    'start_byte': 581,
                    'value': 1.0,
                },
                'user_variable_4': {
                    'format': '<f',
                    'start_byte': 585,
                    'value': 1.0,
                },
                'user_variable_5': {
                    'format': '<f',
                    'start_byte': 589,
                    'value': 1.0,
                },
                'user_variable_6': {
                    'format': '<f',
                    'start_byte': 593,
                    'value': 1.0,
                },
                'user_variable_7': {
                    'format': '<f',
                    'start_byte': 597,
                    'value': 1.0,
                },
                'user_variable_8': {
                    'format': '<f',
                    'start_byte': 601,
                    'value': 1.0,
                },
                'user_variable_9': {
                    'format': '<f',
                    'start_byte': 605,
                    'value': 1.0,
                },
                'user_variable_10': {
                    'format': '<f',
                    'start_byte': 609,
                    'value': 1.0,
                },
                'user_variable_11': {
                    'format': '<f',
                    'start_byte': 613,
                    'value': 1.0,
                },
                'user_variable_12': {
                    'format': '<f',
                    'start_byte': 617,
                    'value': 1.0,
                },
                'user_variable_13': {
                    'format': '<f',
                    'start_byte': 621,
                    'value': 1.0,
                },
                'user_variable_14': {
                    'format': '<f',
                    'start_byte': 625,
                    'value': 1.0,
                },
                'user_variable_15': {
                    'format': '<f',
                    'start_byte': 629,
                    'value': 1.0,
                },
                'user_variable_16': {
                    'format': '<f',
                    'start_byte': 633,
                    'value': 1.0,
                },
                'reserved': {
                    'format': '32s',
                    'start_byte': 637,
                    'value': ''.join(['\0' for i in range(32)]),
                    'text_encoding': 'utf-8',
                },
            }

        class Server(MessageABC):
            msg_length = 128
            command_code = 0xBB120001

            msg_specific_template = {
                'channel': {
                    'format': 'i',
                    'start_byte': 20,
                    'value': 0
                },
                'result': {
                    'format': 'c',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'reserved': {
                    'format': '101s',
                    'start_byte': 25,
                    'value': ''.join(['\0' for i in range(101)]),
                    'text_encoding': 'utf-8',
                },
            }

            assign_schedule_feedback_codes = {
                0: 'success',
                16: 'channel does not exist',
                17: 'Monitor window in use at the moment',
                18: 'Schedule name cannot be empty',
                19: 'Schedule name not found',
                20: 'Channel is running',
                21: 'Channel is downloading another schedule currently',
                22: 'Cannot assign schedule when batch file is open',
                23: 'Assign failed',
                24: 'Not used: User should never see this',
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but converts the result based on the
                assign_schedule_feedback_codes.

                Parameters
                ----------
                msg_bin : bytearray
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)
                msg_dict['result'] = cls.assign_schedule_feedback_codes[
                    ord(msg_dict['result'])]
                return msg_dict

    class StartSchedule:
        '''
        Message for assigning a schedule to a specific channel. See
        THIRD_PARTY_START_SCHEDULE/THIRD_PARTY_START_SCHEDULE_FEEDBACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 160
            command_code = 0xBB320004

            msg_specific_template = {
                'test_name': {
                    # Read as wchar_t which has length of 2 bytes each.
                    'format': '144s',
                    'start_byte': 20,
                    'value': 'pycti test name',
                    'text_encoding': 'utf-16-le',
                },
                'num_channels_to_start': {
                    'format': '<I',
                    'start_byte': 164,
                    'value': 1
                },
                'channel': {
                    'format': '<H',
                    'start_byte': 168,
                    'value': 0
                },
            }

        class Server(MessageABC):
            msg_length = 128
            command_code = 0XBB230004

            msg_specific_template = {
                'channel': {
                    'format': 'I',
                    'start_byte': 20,
                    'value': 0
                },
                'result': {
                    'format': 'c',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'reserved': {
                    'format': '101s',
                    'start_byte': 25,
                    'value': ''.join(['\0' for i in range(101)]),
                    'text_encoding': 'utf-8',
                },
            }

            start_test_feedback_codes = {
                0: 'success',
                16: 'Invalid channel index',
                17: 'There is a user controlling the monitor window (Start/Resume channel window is open)',
                18: 'Requested channel is running or unsafe',
                19: 'Channel not connected to DAQ',
                20: 'Schedule not compatible with current system configuration',
                21: 'No schedule assigned to channel',
                22: 'Schedule version does not match current version of MITS',
                23: 'Not used: User should never see this',
                24: 'Not used: User should never see this',
                25: 'Invalid step number',
                26: 'Not used: User should never see this',
                27: 'Invalid auxiliary count in schedule',
                28: 'Invalid build in auxiliary count',
                29: 'Not used: User should never see this',
                30: 'Check Aux Test Setting tab',
                31: 'No selected channels',
                32: 'Not used: User should never see this',
                33: 'DAQ still downloading schedule',
                34: 'Error querying database (database connection closed most likely)',
                35: 'Testname cannot be empty',
                36: 'Invalid step number',
                37: 'Invalid parallel channel number',
                38: 'Schedule safety precheck failed',
                39: 'Not used: User should never see this',
                40: 'Battery simulation error',
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but converts the result based on the
                start_test_feedback_codes.

                Parameters
                ----------
                msg_bin : bytearray
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)
                msg_dict['result'] = cls.start_test_feedback_codes[
                    ord(msg_dict['result'])]
                return msg_dict

    class StopSchedule:
        '''
        Message for stopping a test on a specific channel. See
        THIRD_PARTY_STOP_SCHEDULE/THIRD_PARTY_STOP_SCHEDULE_FEEDBACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 116
            command_code = 0xBB310001

            msg_specific_template = {
                'channel': {
                    'format': 'I',
                    'start_byte': 20,
                    'value': 0
                },
                # Always 0x00, others all channels are stopped.
                'stop_all_channels': {
                    'format': '1s',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'reserved': {
                    'format': '101s',
                    'start_byte': 25,
                    'value': ''.join(['\0' for i in range(101)]),
                    'text_encoding': 'utf-8',
                },
            }

        class Server(MessageABC):
            msg_length = 128
            command_code = 0XBB130001

            msg_specific_template = {
                'channel': {
                    'format': 'I',
                    'start_byte': 20,
                    'value': 0
                },
                'result': {
                    'format': 'c',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'reserved': {
                    'format': '101s',
                    'start_byte': 25,
                    'value': ''.join(['\0' for i in range(101)]),
                    'text_encoding': 'utf-8',
                },
            }

            stop_test_feedback_codes = {
                0: 'success',
                16: 'Channel index does not exist',
                17: 'Someone else is controlling monitor window at the moment',
                18: 'Not used: User should never see this',
                19: 'Not used: User should never see this',
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but converts the result based on the
                stop_test_feedback_codes.

                Parameters
                ----------
                msg_bin : bytearray
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)
                msg_dict['result'] = cls.stop_test_feedback_codes[
                    ord(msg_dict['result'])]
                return msg_dict

    class SetMetaVariable:
        '''
        Message for setting meta variables. 
        THIRD_PARTY_SET_MV_VALUE/THIRD_PARTY_SET_MV_VALUE_FEEDBACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 62
            command_code = 0xBB150001

            msg_specific_template = {
                'channel': {
                    'format': '<I',
                    'start_byte': 20,
                    'value': 0
                },
                # The only mv_type allowed for CTI is 1
                'mv_type': {
                    'format': '<i',
                    'start_byte': 24,
                    'value': 1
                },
                # This determines which meta variable is set. Defaults to MV 1.
                'mv_meta_code': {
                    'format': '<i',
                    'start_byte': 28,
                    'value': 52,
                },
                'reserved_1': {
                    'format': '16s',
                    'start_byte': 32,
                    'value': ''.join(['\0' for i in range(16)]),
                    'text_encoding': 'utf-8',
                },
                # The only value type allowed for CTI is 1, float.
                'mv_value_type': {
                    'format': '<i',
                    'start_byte': 48,
                    'value': 1
                },
                'mv_data': {
                    'format': '<f',
                    'start_byte': 52,
                    'value': 1
                },
                'reserved_2': {
                    'format': '16s',
                    'start_byte': 56,
                    'value': ''.join(['\0' for i in range(16)]),
                    'text_encoding': 'utf-8',
                },
            }

            # Specifies the code for each variable. E.g. MV_UD1 has a code of 52
            mv_channel_codes = {
                1: 52,
                2: 53,
                3: 54,
                4: 55,
                5: 105,
                6: 106,
                7: 107,
                8: 108,
                9: 109,
                10: 110,
                11: 111,
                12: 112,
                13: 113,
                14: 114,
                15: 115,
                16: 116,
            }

        class Server(MessageABC):
            msg_length = 128
            command_code = 0XBB510001

            msg_specific_template = {
                'channel': {
                    'format': '<I',
                    'start_byte': 20,
                    'value': 0
                },
                'result': {
                    'format': 'c',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'reserved': {
                    'format': '101s',
                    'start_byte': 25,
                    'value': ''.join(['\0' for i in range(101)]),
                    'text_encoding': 'utf-8',
                },
            }

            mv_result_decoder = {
                0: 'success',
                16: 'Set MV Failure',
                17: 'Channel is not running',
                18: 'Meta code does not exist'
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but converts the result based on the
                stop_test_feedback_codes.

                Parameters
                ----------
                msg_bin : bytearry
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)

                msg_dict['result'] = cls.mv_result_decoder[
                    ord(msg_dict['result'])]

                return msg_dict

Classes

class MessageABC

Helper class that provides a standard way to create an ABC using inheritance.

Expand source code
class MessageABC(ABC):

    # The length of the message. Should be overwritten in child class
    msg_length = 0

    # The message command code. Should be overwritten in child class
    command_code = 0x00

    # Template that is specific to each message type. Should be overwritten in child class
    msg_specific_template = {}

    # Base message template that is common for all messages
    base_template = {
        'header': {
            'format': '<Q',
            'start_byte': 0,
            'value': 0x11DDDDDDDDDDDDDD
        },
        'msg_length': {
            'format': '<L',
            'start_byte': 8,
            'value': 0
        },
        'command_code': {
            'format': '<L',
            'start_byte': 12,
            'value': 0x00000000
        },
        'extended_command_code': {
            'format': '<L',
            'start_byte': 16,
            'value': 0x00000000
        },
    }

    @classmethod
    def unpack(cls, msg_bin: bytearray) -> dict:
        """
        Parses the passed message and decodes it with the msg_encoding dict.
        Each key in the output message will have name of the key from the 
        msg_encoding dictionary.

        Parameters
        ----------
        msg_bin : bytearry
            The message to unpack.

        Returns
        -------
        decoded_msg_dict : dict
            The message items decoded into a dictionary.
        """
        decoded_msg_dict = {}

        # Create a template to unpack message with
        template = {**deepcopy(cls.base_template),
                   **deepcopy(cls.msg_specific_template)}

        for item_name, item in template.items():
            start_idx = item['start_byte']
            end_idx = item['start_byte'] + struct.calcsize(item['format'])
            decoded_msg_dict[item_name] = struct.unpack(
                item['format'], msg_bin[start_idx:end_idx])[0]

            # Decode and strip trailing 0x00s from strings.
            if item['format'].endswith('s'):
                decoded_msg_dict[item_name] = decoded_msg_dict[item_name].decode(
                    item['text_encoding']).rstrip('\x00')

        if decoded_msg_dict['command_code'] != cls.command_code:
            logger.warning(
                f'Decoded command code {decoded_msg_dict["command_code"]} does not match what was expected!')

        if decoded_msg_dict['msg_length'] != cls.msg_length:
            logger.warning(
                f'Decoded message length {decoded_msg_dict["msg_length"]} does not match what was expected!')

        return decoded_msg_dict

    @classmethod
    def pack(cls, msg_values={}) -> bytearray:
        """
        Packs a message based on the message encoding given in the msg_specific_template
        dictionary. Values can be substituted for default values if they are included 
        in the `msg_values` argument.

        Parameters
        ----------
        msg_values : dict
            A dictionary detailing which default values in the message temple should be 
            updated.

        Returns
        -------
        msg_bin : bytearray
            Packed response message.
        """
        # Create a template to build messages from
        template = {**deepcopy(cls.base_template),
                   **deepcopy(cls.msg_specific_template)}

        # Update the template with message specific length and command code
        template['msg_length']['value'] = cls.msg_length
        template['command_code']['value'] = cls.command_code

        # Create a message bytearray that will be loaded with message contents
        msg_bin = bytearray(template['msg_length']['value'])

        # Update default message values with those in the passed msg_values dict
        for key in msg_values.keys():
            if key in template.keys():
                template[key]['value'] = msg_values[key]
            else:
                logger.warning(
                    f'Key name {key} was not found in msg_encoding!')

        # Pack each item in template. If packing any item fails then abort packing.
        for item_name, item in template.items():
            logger.debug(f'Packing item {item_name}')
            try:
                if item['format'].endswith('s') or item['format'].endswith('c'):
                    packed_item = struct.pack(
                        item['format'],
                        item['value'].encode(item['text_encoding']))
                else:
                    packed_item = struct.pack(
                        item['format'], item['value'])
            except struct.error as e:
                logger.error(
                    f'Error packing {item_name} with fields {item}!')
                logger.error(e)
                msg_bin = bytearray([])
                break

            start_idx = item['start_byte']
            end_idx = item['start_byte'] + struct.calcsize(item['format'])
            msg_bin[start_idx:end_idx] = packed_item

        # Append a checksum to the end of the message
        if msg_bin:
            msg_bin += struct.pack('<H', sum(msg_bin))

        return msg_bin

Ancestors

  • abc.ABC

Subclasses

  • pycti.messages.Msg.AssignSchedule.Client
  • pycti.messages.Msg.AssignSchedule.Server
  • pycti.messages.Msg.ChannelInfo.Client
  • pycti.messages.Msg.ChannelInfo.Server
  • pycti.messages.Msg.Login.Client
  • pycti.messages.Msg.Login.Server
  • pycti.messages.Msg.SetMetaVariable.Client
  • pycti.messages.Msg.SetMetaVariable.Server
  • pycti.messages.Msg.StartSchedule.Client
  • pycti.messages.Msg.StartSchedule.Server
  • pycti.messages.Msg.StopSchedule.Client
  • pycti.messages.Msg.StopSchedule.Server

Class variables

var base_template
var command_code
var msg_length
var msg_specific_template

Static methods

def pack(msg_values={}) ‑> bytearray

Packs a message based on the message encoding given in the msg_specific_template dictionary. Values can be substituted for default values if they are included in the msg_values argument.

Parameters

msg_values : dict
A dictionary detailing which default values in the message temple should be updated.

Returns

msg_bin : bytearray
Packed response message.
Expand source code
@classmethod
def pack(cls, msg_values={}) -> bytearray:
    """
    Packs a message based on the message encoding given in the msg_specific_template
    dictionary. Values can be substituted for default values if they are included 
    in the `msg_values` argument.

    Parameters
    ----------
    msg_values : dict
        A dictionary detailing which default values in the message temple should be 
        updated.

    Returns
    -------
    msg_bin : bytearray
        Packed response message.
    """
    # Create a template to build messages from
    template = {**deepcopy(cls.base_template),
               **deepcopy(cls.msg_specific_template)}

    # Update the template with message specific length and command code
    template['msg_length']['value'] = cls.msg_length
    template['command_code']['value'] = cls.command_code

    # Create a message bytearray that will be loaded with message contents
    msg_bin = bytearray(template['msg_length']['value'])

    # Update default message values with those in the passed msg_values dict
    for key in msg_values.keys():
        if key in template.keys():
            template[key]['value'] = msg_values[key]
        else:
            logger.warning(
                f'Key name {key} was not found in msg_encoding!')

    # Pack each item in template. If packing any item fails then abort packing.
    for item_name, item in template.items():
        logger.debug(f'Packing item {item_name}')
        try:
            if item['format'].endswith('s') or item['format'].endswith('c'):
                packed_item = struct.pack(
                    item['format'],
                    item['value'].encode(item['text_encoding']))
            else:
                packed_item = struct.pack(
                    item['format'], item['value'])
        except struct.error as e:
            logger.error(
                f'Error packing {item_name} with fields {item}!')
            logger.error(e)
            msg_bin = bytearray([])
            break

        start_idx = item['start_byte']
        end_idx = item['start_byte'] + struct.calcsize(item['format'])
        msg_bin[start_idx:end_idx] = packed_item

    # Append a checksum to the end of the message
    if msg_bin:
        msg_bin += struct.pack('<H', sum(msg_bin))

    return msg_bin
def unpack(msg_bin: bytearray) ‑> dict

Parses the passed message and decodes it with the msg_encoding dict. Each key in the output message will have name of the key from the msg_encoding dictionary.

Parameters

msg_bin : bytearry
The message to unpack.

Returns

decoded_msg_dict : dict
The message items decoded into a dictionary.
Expand source code
@classmethod
def unpack(cls, msg_bin: bytearray) -> dict:
    """
    Parses the passed message and decodes it with the msg_encoding dict.
    Each key in the output message will have name of the key from the 
    msg_encoding dictionary.

    Parameters
    ----------
    msg_bin : bytearry
        The message to unpack.

    Returns
    -------
    decoded_msg_dict : dict
        The message items decoded into a dictionary.
    """
    decoded_msg_dict = {}

    # Create a template to unpack message with
    template = {**deepcopy(cls.base_template),
               **deepcopy(cls.msg_specific_template)}

    for item_name, item in template.items():
        start_idx = item['start_byte']
        end_idx = item['start_byte'] + struct.calcsize(item['format'])
        decoded_msg_dict[item_name] = struct.unpack(
            item['format'], msg_bin[start_idx:end_idx])[0]

        # Decode and strip trailing 0x00s from strings.
        if item['format'].endswith('s'):
            decoded_msg_dict[item_name] = decoded_msg_dict[item_name].decode(
                item['text_encoding']).rstrip('\x00')

    if decoded_msg_dict['command_code'] != cls.command_code:
        logger.warning(
            f'Decoded command code {decoded_msg_dict["command_code"]} does not match what was expected!')

    if decoded_msg_dict['msg_length'] != cls.msg_length:
        logger.warning(
            f'Decoded message length {decoded_msg_dict["msg_length"]} does not match what was expected!')

    return decoded_msg_dict
class Msg
Expand source code
class Msg:
    class Login:
        '''
        Message for logging into Arbin cycler. See
        CTI_REQUEST_LOGIN/CTI_REQUEST_LOGIN_FEEDBACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 74
            command_code = 0xEEAB0001

            msg_specific_template = {
                'username': {
                    'format': '32s',
                    'start_byte': 20,
                    'text_encoding': 'utf-8',
                    'value': 'not a username'
                },
                'password': {
                    'format': '32s',
                    'start_byte': 52,
                    'text_encoding': 'utf-8',
                    'value': 'not a password'
                },
            }

        class Server(MessageABC):
            msg_length = 8678
            command_code = 0xEEBA0001

            msg_specific_template = {
                'result': {
                    'format': 'I',
                    'start_byte': 20,
                    'value': 1
                },
                'ip_address': {
                    'format': '4s',
                    'start_byte': 24,
                    'value': "0000",
                    'text_encoding': 'utf-8',
                },
                'cycler_sn': {
                    'format': '16s',
                    'start_byte': 28,
                    'value': '00000000',
                    'text_encoding': 'ascii',
                },
                'note': {
                    'format': '256s',
                    'start_byte': 44,
                    'value': '00000000',
                    'text_encoding': 'ascii',
                },
                'nick_name': {
                    # Stored as wchar_t[1024]. Each wchar_t is 2 bytes, twice as big as standard char in Python
                    'format': '2048s',
                    'start_byte': 300,
                    'value': 'our nickname',
                    'text_encoding': 'utf-16-le',
                },
                'location': {
                    'format': '2048s',
                    'start_byte': 2348,
                    'value': 'our location',
                    'text_encoding': 'utf-16-le',
                },
                'emergency_contact': {
                    'format': '2048s',
                    'start_byte': 4396,
                    'value': 'our location',
                    'text_encoding': 'utf-16-le',
                },
                'other_comments': {
                    'format': '2048s',
                    'start_byte': 6444,
                    'value': 'our location',
                    'text_encoding': 'utf-16-le',
                },
                'email': {
                    'format': '128s',
                    'start_byte': 8492,
                    'value': 'our location',
                    'text_encoding': 'utf-16-le',
                },
                'call': {
                    'format': '32s',
                    'start_byte': 8620,
                    'value': 'our location',
                    'text_encoding': 'utf-16-le',
                },
                'itac': {
                    'format': '<I',
                    'start_byte': 8652,
                    'value': 0
                },
                'version': {
                    'format': '<I',
                    'start_byte': 8656,
                    'value': 0
                },
                'allow_control': {
                    'format': '<I',
                    'start_byte': 8660,
                    'value': 0
                },
                'num_channels': {
                    'format': '<I',
                    'start_byte': 8664,
                    'value': 0
                },
                'user_type': {
                    'format': '<I',
                    'start_byte': 8668,
                    'value': 1,
                    'text_encoding': 'utf-16-le',
                },
                'picture_length': {
                    'format': '<I',
                    'start_byte': 8672,
                    'value': 0,
                    'text_encoding': 'utf-8',
                },
            }

            login_result_dict = {
                0: "should not see this",
                1: "success",
                2: "fail",
                3: "already logged in"
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but converts the result based on the
                login_result_dict.

                Parameters
                ----------
                msg_bin : bytearray
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)
                msg_dict['result'] = cls.login_result_dict[msg_dict['result']]
                return msg_dict

    class ChannelInfo:
        '''
        Message for getting channel info from cycler. See
        CTI_REQUEST_GET_CHANNELS_INFO/CTI_REQUEST_GET_CHANNELS_INFO_FEED_BACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 50
            command_code = 0xEEAB0003

            msg_specific_template = {
                'channel': {
                    'format': '<h',
                    'start_byte': 20,
                    'value': 0
                },
                'channel_selection': {
                    'format': '<h',
                    'start_byte': 22,
                    'value': 1
                },
                'aux_options': {
                    'format': '<I',
                    'start_byte': 24,
                    'value': 0x00
                },
                'reserved': {
                    'format': '32s',
                    'start_byte': 28,
                    'value': ''.join(['\0' for i in range(32)]),
                    'text_encoding': 'utf-8',
                },
            }

        class Server(MessageABC):

            # Default message length for 1 channel with no aux readings. Will be larger as those grow.
            msg_length = 1779
            command_code = 0xEEBA0003

            msg_specific_template = {
                'number_of_channels': {
                    'format': '<I',
                    'start_byte': 20,
                    'value': 1
                },
                'channel': {
                    'format': '<I',
                    'start_byte': 24,
                    'value': 0
                },
                'status': {
                    'format': '<h',
                    'start_byte': 28,
                    'value': 0x00
                },
                'comm_failure': {
                    'format': '<B',
                    'start_byte': 30,
                    'value': 0
                },
                'schedule': {
                    # Stored as wchar_t[200]. Each wchar_t is 2 bytes, twice as big as standard char in Python
                    'format': '400s',
                    'start_byte': 31,
                    'value': 'fake_schedule',
                    'text_encoding': 'utf-16-le',
                },
                'testname': {
                    # Stored as wchar_t[72]
                    'format': '144s',
                    'start_byte': 431,
                    'value': 'fake_testname',
                    'text_encoding': 'utf-16-le',
                },
                'exit_condition': {
                    'format': '100s',
                    'start_byte': 575,
                    'value': 'none',
                    'text_encoding': 'utf-8',
                },
                'step_and_cycle_format': {
                    'format': '64s',
                    'start_byte': 675,
                    'value': 'none',
                    'text_encoding': 'utf-8',
                },
                # Stored as wchar_t[72]
                'barcode': {
                    'format': '144s',
                    'start_byte': 739,
                    'value': 'none',
                    'text_encoding': 'utf-16',
                },
                # Stored as wchar_t[72]
                'can_config_name': {
                    'format': '400s',
                    'start_byte': 883,
                    'value': 'none',
                    'text_encoding': 'utf-16',
                },
                # Stored as wchar_t[72]
                'smb_config_name': {
                    'format': '400s',
                    'start_byte': 1283,
                    'value': 'none',
                    'text_encoding': 'utf-16',
                },
                'master_channel': {
                    'format': '<H',
                    'start_byte': 1683,
                    'value': 0,
                },
                'test_time_s': {
                    'format': '<d',
                    'start_byte': 1685,
                    'value': 0,
                },
                'step_time_s': {
                    'format': '<d',
                    'start_byte': 1693,
                    'value': 0,
                },
                'voltage_v': {
                    'format': '<f',
                    'start_byte': 1701,
                    'value': 0,
                },
                'current_a': {
                    'format': '<f',
                    'start_byte': 1705,
                    'value': 0,
                },
                'power_w': {
                    'format': '<f',
                    'start_byte': 1709,
                    'value': 0,
                },
                'charge_capacity_ah': {
                    'format': '<f',
                    'start_byte': 1713,
                    'value': 0,
                },
                'discharge_capacity_ah': {
                    'format': '<f',
                    'start_byte': 1717,
                    'value': 0,
                },
                'charge_energy_wh': {
                    'format': '<f',
                    'start_byte': 1721,
                    'value': 0,
                },
                'discharge_energy_wh': {
                    'format': '<f',
                    'start_byte': 1725,
                    'value': 0,
                },
                'internal_resistance_ohm': {
                    'format': '<f',
                    'start_byte': 1729,
                    'value': 0,
                },
                'dvdt_vbys': {
                    'format': '<f',
                    'start_byte': 1733,
                    'value': 0,
                },
                'acr_ohm': {
                    'format': '<f',
                    'start_byte': 1737,
                    'value': 0,
                },
                'aci_ohm': {
                    'format': '<f',
                    'start_byte': 1741,
                    'value': 0,
                },
                'aci_phase_degrees': {
                    'format': '<f',
                    'start_byte': 1745,
                    'value': 0,
                },
                'aux_voltage_count': {
                    'format': '<H',
                    'start_byte': 1749,
                    'value': 0,
                },
                'aux_temperature_count': {
                    'format': '<H',
                    'start_byte': 1751,
                    'value': 0,
                },
                'aux_pressure_count': {
                    'format': '<H',
                    'start_byte': 1753,
                    'value': 0,
                },
                'aux_external_count': {
                    'format': '<H',
                    'start_byte': 1755,
                    'value': 0,
                },
                'aux_flow_count': {
                    'format': '<H',
                    'start_byte': 1757,
                    'value': 0,
                },
                'aux_ao_count': {
                    'format': '<H',
                    'start_byte': 1759,
                    'value': 0,
                },
                'aux_di_count': {
                    'format': '<H',
                    'start_byte': 1761,
                    'value': 0,
                },
                'aux_do_count': {
                    'format': '<H',
                    'start_byte': 1763,
                    'value': 0,
                },
                'aux_humidity_count': {
                    'format': '<H',
                    'start_byte': 1765,
                    'value': 0,
                },
                'aux_safety_count': {
                    'format': '<H',
                    'start_byte': 1767,
                    'value': 0,
                },
                'aux_ph_count': {
                    'format': '<H',
                    'start_byte': 1769,
                    'value': 0,
                },
                'aux_density_count': {
                    'format': '<H',
                    'start_byte': 1771,
                    'value': 0,
                },
                'bms_count': {
                    'format': '<H',
                    'start_byte': 1773,
                    'value': 0,
                },
                'smb_count': {
                    'format': '<H',
                    'start_byte': 1775,
                    'value': 0,
                },
            }

            # List of staus codes. Each index in the corresponding status code.
            status_code_dict = {
                0: 'Idle',
                1: 'Transition',
                2: 'Charge',
                3: 'Disharge',
                4: 'Rest',
                5: 'Wait',
                6: 'External Charge',
                7: 'Calibration',
                8: 'Unsafe',
                9: 'Pulse',
                10: 'Internal Resistance',
                11: 'AC Impedance',
                12: 'ACI Cell',
                13: 'Test Settings',
                14: 'Error',
                15: 'Finished',
                16: 'Volt Meter',
                17: 'Waiting for ACS',
                18: 'Pause',
                19: 'Empty',
                20: 'Idle from MCU',
                21: 'Start',
                22: 'Running',
                23: 'Step Transfer',
                24: 'Resume',
                25: 'Go Pause',
                26: 'Go Stop',
                27: 'Go Next Step',
                28: 'Online Update',
                29: 'DAQ Memory Unsafe',
                30: 'ACR'
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but uses aux counts to unpack aux readings

                Parameters
                ----------
                msg_bin : bytearry
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)
                msg_dict = cls.aux_readings_parser(
                    msg_dict, msg_bin, starting_aux_idx=1777)
                msg_dict['status'] = cls.status_code_dict[msg_dict['status']]
                return msg_dict

            @classmethod
            def pack(cls, msg_values={}) -> bytearray:
                """
                Same as parent method, but handles packing aux measurements.

                Parameters
                ----------
                msg_values : dict
                    A dictionary detailing which default values in the message temple should be 
                    updated.

                Returns
                -------
                msg : bytearray
                    Packed response message.
                """
                # TODO : Modify so that we can aux values can be packed.
                msg_bin = super().pack(msg_values)
                return msg_bin

            @classmethod
            def aux_readings_parser(cls, msg_dict: dict, msg_bin: bytearray, starting_aux_idx=1777):
                """
                Parses the auxiliary readings in msg_bin based on the aux readings
                counts in msg_dict. Aux readings are then added as items to the msg_dict. 

                Parameters
                ----------
                msg_dict : dict
                    A dictionary containing the aux readings counts (aux_voltage_count, aux_voltage_count, etc)
                msg_bin : bytearray
                    The message to unpack as a byte array.
                starting_aux_idx : int
                    The starting index in the msg_bin for aux readings. 1777 in single channel messages

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                aux_lists = []

                aux_count_name_list = [
                    'aux_voltage_count',
                    'aux_temperature_count',
                    'aux_pressure_count',
                    'aux_external_count',
                    'aux_flow_count',
                    'aux_ao_count',
                    'aux_di_count',
                    'aux_do_count',
                    'aux_humidity_count',
                    'aux_safety_count',
                    'aux_ph_count',
                    'aux_density_count'
                ]

                # Generate a list of readings for each aux reading.
                # If count is non-zero then genreate a aux_reading and aux_reading_dt list of that length
                # Else, generate empty lists for the aux_reading and aux_reading_dt
                for aux_count_name in aux_count_name_list:
                    aux_reading_name = re.split('_count', aux_count_name)[0]
                    aux_dt_name = aux_reading_name + '_dt'
                    if msg_dict[aux_count_name]:
                        msg_dict[aux_reading_name] = [0 for x in range(
                            msg_dict[aux_count_name])]
                        msg_dict[aux_dt_name] = [0 for x in range(
                            msg_dict[aux_count_name])]
                        aux_lists.append(
                            [msg_dict[aux_reading_name], msg_dict[aux_dt_name]])
                    else:
                        msg_dict[aux_reading_name] = []
                        msg_dict[aux_dt_name] = []

                # For aux readings that have a measurements, add them to the respective reading list.
                current_aux_idx = starting_aux_idx
                for readings_list in aux_lists:
                    for i in range(0, len(readings_list[0])):
                        # The first list in reading list is reading itself
                        readings_list[0][i] = struct.unpack(
                            '<f', msg_bin[current_aux_idx:current_aux_idx+4])[0]
                        # The second reading in the list is the dt value.
                        readings_list[1][i] = struct.unpack(
                            '<f', msg_bin[current_aux_idx+4:current_aux_idx+8])[0]
                        current_aux_idx += 8

                return msg_dict

    class AssignSchedule:
        '''
        Message for assiging a schedule to a specific channel. See
        THIRD_PARTY_ASSIGN_SCHEDULE/THIRD_PARTY_ASSIGN_SCHEDULE_FEEDBACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 659
            command_code = 0xBB210001

            msg_specific_template = {
                'channel': {
                    'format': 'i',
                    'start_byte': 20,
                    'value': 0
                },
                # Always 0x00 for PyCTI since we only work with single channels
                'assign_all_channels': {
                    'format': '1s',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'schedule': {
                    # Stored as wchar_t[200]. Each wchar_t is 2 bytes, twice as big as standard char in Python
                    'format': '400s',
                    'start_byte': 25,
                    'value': 'fake_schedule',
                    'text_encoding': 'utf-16-le',
                },
                'test_capacity_ah': {
                    'format': '<f',
                    'start_byte': 425,
                    'value': 1.0,
                },
                'barcode': {
                    'format': '144s',
                    'start_byte': 429,
                    'value': '',
                    'text_encoding': 'utf-16-le',
                },
                'user_variable_1': {
                    'format': '<f',
                    'start_byte': 573,
                    'value': 1.0,
                },
                'user_variable_2': {
                    'format': '<f',
                    'start_byte': 577,
                    'value': 1.0,
                },
                'user_variable_3': {
                    'format': '<f',
                    'start_byte': 581,
                    'value': 1.0,
                },
                'user_variable_4': {
                    'format': '<f',
                    'start_byte': 585,
                    'value': 1.0,
                },
                'user_variable_5': {
                    'format': '<f',
                    'start_byte': 589,
                    'value': 1.0,
                },
                'user_variable_6': {
                    'format': '<f',
                    'start_byte': 593,
                    'value': 1.0,
                },
                'user_variable_7': {
                    'format': '<f',
                    'start_byte': 597,
                    'value': 1.0,
                },
                'user_variable_8': {
                    'format': '<f',
                    'start_byte': 601,
                    'value': 1.0,
                },
                'user_variable_9': {
                    'format': '<f',
                    'start_byte': 605,
                    'value': 1.0,
                },
                'user_variable_10': {
                    'format': '<f',
                    'start_byte': 609,
                    'value': 1.0,
                },
                'user_variable_11': {
                    'format': '<f',
                    'start_byte': 613,
                    'value': 1.0,
                },
                'user_variable_12': {
                    'format': '<f',
                    'start_byte': 617,
                    'value': 1.0,
                },
                'user_variable_13': {
                    'format': '<f',
                    'start_byte': 621,
                    'value': 1.0,
                },
                'user_variable_14': {
                    'format': '<f',
                    'start_byte': 625,
                    'value': 1.0,
                },
                'user_variable_15': {
                    'format': '<f',
                    'start_byte': 629,
                    'value': 1.0,
                },
                'user_variable_16': {
                    'format': '<f',
                    'start_byte': 633,
                    'value': 1.0,
                },
                'reserved': {
                    'format': '32s',
                    'start_byte': 637,
                    'value': ''.join(['\0' for i in range(32)]),
                    'text_encoding': 'utf-8',
                },
            }

        class Server(MessageABC):
            msg_length = 128
            command_code = 0xBB120001

            msg_specific_template = {
                'channel': {
                    'format': 'i',
                    'start_byte': 20,
                    'value': 0
                },
                'result': {
                    'format': 'c',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'reserved': {
                    'format': '101s',
                    'start_byte': 25,
                    'value': ''.join(['\0' for i in range(101)]),
                    'text_encoding': 'utf-8',
                },
            }

            assign_schedule_feedback_codes = {
                0: 'success',
                16: 'channel does not exist',
                17: 'Monitor window in use at the moment',
                18: 'Schedule name cannot be empty',
                19: 'Schedule name not found',
                20: 'Channel is running',
                21: 'Channel is downloading another schedule currently',
                22: 'Cannot assign schedule when batch file is open',
                23: 'Assign failed',
                24: 'Not used: User should never see this',
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but converts the result based on the
                assign_schedule_feedback_codes.

                Parameters
                ----------
                msg_bin : bytearray
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)
                msg_dict['result'] = cls.assign_schedule_feedback_codes[
                    ord(msg_dict['result'])]
                return msg_dict

    class StartSchedule:
        '''
        Message for assigning a schedule to a specific channel. See
        THIRD_PARTY_START_SCHEDULE/THIRD_PARTY_START_SCHEDULE_FEEDBACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 160
            command_code = 0xBB320004

            msg_specific_template = {
                'test_name': {
                    # Read as wchar_t which has length of 2 bytes each.
                    'format': '144s',
                    'start_byte': 20,
                    'value': 'pycti test name',
                    'text_encoding': 'utf-16-le',
                },
                'num_channels_to_start': {
                    'format': '<I',
                    'start_byte': 164,
                    'value': 1
                },
                'channel': {
                    'format': '<H',
                    'start_byte': 168,
                    'value': 0
                },
            }

        class Server(MessageABC):
            msg_length = 128
            command_code = 0XBB230004

            msg_specific_template = {
                'channel': {
                    'format': 'I',
                    'start_byte': 20,
                    'value': 0
                },
                'result': {
                    'format': 'c',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'reserved': {
                    'format': '101s',
                    'start_byte': 25,
                    'value': ''.join(['\0' for i in range(101)]),
                    'text_encoding': 'utf-8',
                },
            }

            start_test_feedback_codes = {
                0: 'success',
                16: 'Invalid channel index',
                17: 'There is a user controlling the monitor window (Start/Resume channel window is open)',
                18: 'Requested channel is running or unsafe',
                19: 'Channel not connected to DAQ',
                20: 'Schedule not compatible with current system configuration',
                21: 'No schedule assigned to channel',
                22: 'Schedule version does not match current version of MITS',
                23: 'Not used: User should never see this',
                24: 'Not used: User should never see this',
                25: 'Invalid step number',
                26: 'Not used: User should never see this',
                27: 'Invalid auxiliary count in schedule',
                28: 'Invalid build in auxiliary count',
                29: 'Not used: User should never see this',
                30: 'Check Aux Test Setting tab',
                31: 'No selected channels',
                32: 'Not used: User should never see this',
                33: 'DAQ still downloading schedule',
                34: 'Error querying database (database connection closed most likely)',
                35: 'Testname cannot be empty',
                36: 'Invalid step number',
                37: 'Invalid parallel channel number',
                38: 'Schedule safety precheck failed',
                39: 'Not used: User should never see this',
                40: 'Battery simulation error',
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but converts the result based on the
                start_test_feedback_codes.

                Parameters
                ----------
                msg_bin : bytearray
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)
                msg_dict['result'] = cls.start_test_feedback_codes[
                    ord(msg_dict['result'])]
                return msg_dict

    class StopSchedule:
        '''
        Message for stopping a test on a specific channel. See
        THIRD_PARTY_STOP_SCHEDULE/THIRD_PARTY_STOP_SCHEDULE_FEEDBACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 116
            command_code = 0xBB310001

            msg_specific_template = {
                'channel': {
                    'format': 'I',
                    'start_byte': 20,
                    'value': 0
                },
                # Always 0x00, others all channels are stopped.
                'stop_all_channels': {
                    'format': '1s',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'reserved': {
                    'format': '101s',
                    'start_byte': 25,
                    'value': ''.join(['\0' for i in range(101)]),
                    'text_encoding': 'utf-8',
                },
            }

        class Server(MessageABC):
            msg_length = 128
            command_code = 0XBB130001

            msg_specific_template = {
                'channel': {
                    'format': 'I',
                    'start_byte': 20,
                    'value': 0
                },
                'result': {
                    'format': 'c',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'reserved': {
                    'format': '101s',
                    'start_byte': 25,
                    'value': ''.join(['\0' for i in range(101)]),
                    'text_encoding': 'utf-8',
                },
            }

            stop_test_feedback_codes = {
                0: 'success',
                16: 'Channel index does not exist',
                17: 'Someone else is controlling monitor window at the moment',
                18: 'Not used: User should never see this',
                19: 'Not used: User should never see this',
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but converts the result based on the
                stop_test_feedback_codes.

                Parameters
                ----------
                msg_bin : bytearray
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)
                msg_dict['result'] = cls.stop_test_feedback_codes[
                    ord(msg_dict['result'])]
                return msg_dict

    class SetMetaVariable:
        '''
        Message for setting meta variables. 
        THIRD_PARTY_SET_MV_VALUE/THIRD_PARTY_SET_MV_VALUE_FEEDBACK 
        in Arbin docs for more info.
        '''
        class Client(MessageABC):
            msg_length = 62
            command_code = 0xBB150001

            msg_specific_template = {
                'channel': {
                    'format': '<I',
                    'start_byte': 20,
                    'value': 0
                },
                # The only mv_type allowed for CTI is 1
                'mv_type': {
                    'format': '<i',
                    'start_byte': 24,
                    'value': 1
                },
                # This determines which meta variable is set. Defaults to MV 1.
                'mv_meta_code': {
                    'format': '<i',
                    'start_byte': 28,
                    'value': 52,
                },
                'reserved_1': {
                    'format': '16s',
                    'start_byte': 32,
                    'value': ''.join(['\0' for i in range(16)]),
                    'text_encoding': 'utf-8',
                },
                # The only value type allowed for CTI is 1, float.
                'mv_value_type': {
                    'format': '<i',
                    'start_byte': 48,
                    'value': 1
                },
                'mv_data': {
                    'format': '<f',
                    'start_byte': 52,
                    'value': 1
                },
                'reserved_2': {
                    'format': '16s',
                    'start_byte': 56,
                    'value': ''.join(['\0' for i in range(16)]),
                    'text_encoding': 'utf-8',
                },
            }

            # Specifies the code for each variable. E.g. MV_UD1 has a code of 52
            mv_channel_codes = {
                1: 52,
                2: 53,
                3: 54,
                4: 55,
                5: 105,
                6: 106,
                7: 107,
                8: 108,
                9: 109,
                10: 110,
                11: 111,
                12: 112,
                13: 113,
                14: 114,
                15: 115,
                16: 116,
            }

        class Server(MessageABC):
            msg_length = 128
            command_code = 0XBB510001

            msg_specific_template = {
                'channel': {
                    'format': '<I',
                    'start_byte': 20,
                    'value': 0
                },
                'result': {
                    'format': 'c',
                    'start_byte': 24,
                    'value': '\0',
                    'text_encoding': 'utf-8',
                },
                'reserved': {
                    'format': '101s',
                    'start_byte': 25,
                    'value': ''.join(['\0' for i in range(101)]),
                    'text_encoding': 'utf-8',
                },
            }

            mv_result_decoder = {
                0: 'success',
                16: 'Set MV Failure',
                17: 'Channel is not running',
                18: 'Meta code does not exist'
            }

            @classmethod
            def unpack(cls, msg_bin: bytearray) -> dict:
                """
                Same as the parent method, but converts the result based on the
                stop_test_feedback_codes.

                Parameters
                ----------
                msg_bin : bytearry
                    The message to unpack.

                Returns
                -------
                msg_dict : dict
                    The message with items decoded into a dictionary
                """
                msg_dict = super().unpack(msg_bin)

                msg_dict['result'] = cls.mv_result_decoder[
                    ord(msg_dict['result'])]

                return msg_dict

Class variables

var AssignSchedule

Message for assiging a schedule to a specific channel. See THIRD_PARTY_ASSIGN_SCHEDULE/THIRD_PARTY_ASSIGN_SCHEDULE_FEEDBACK in Arbin docs for more info.

var ChannelInfo

Message for getting channel info from cycler. See CTI_REQUEST_GET_CHANNELS_INFO/CTI_REQUEST_GET_CHANNELS_INFO_FEED_BACK in Arbin docs for more info.

var Login

Message for logging into Arbin cycler. See CTI_REQUEST_LOGIN/CTI_REQUEST_LOGIN_FEEDBACK in Arbin docs for more info.

var SetMetaVariable

Message for setting meta variables. THIRD_PARTY_SET_MV_VALUE/THIRD_PARTY_SET_MV_VALUE_FEEDBACK in Arbin docs for more info.

var StartSchedule

Message for assigning a schedule to a specific channel. See THIRD_PARTY_START_SCHEDULE/THIRD_PARTY_START_SCHEDULE_FEEDBACK in Arbin docs for more info.

var StopSchedule

Message for stopping a test on a specific channel. See THIRD_PARTY_STOP_SCHEDULE/THIRD_PARTY_STOP_SCHEDULE_FEEDBACK in Arbin docs for more info.