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.