Module pycti.cycler_interface
Expand source code
import socket
import logging
import struct
import dotenv
import os
from pydantic import BaseModel
from .messages import Msg
from .messages import MessageABC
logger = logging.getLogger(__name__)
class CyclerInterface:
"""
Class for interfacing with Arbin battery cycler at a cycler level.
"""
def __init__(self, config: dict, env_path: str = os.path.join(os.getcwd(), '.env')):
"""
Creates a class instance for interfacing with Arbin battery cycler at a cycler level.
Parameters
----------
config : dict
A configuration dictionary. Must contain the following keys:
ip_address : str
The IP address of the Arbin host computer.
port : int
The TCP port to communicate through.
timeout_s : *optional* : float
How long to wait before timing out on TCP communication. Defaults to 3 seconds.
msg_buffer_size : *optional* : int
How big of a message buffer to use for sending/receiving messages.
A minimum of 1024 bytes is recommended. Defaults to 4096 bytes.
env_path : *optional* : str
The path to the `.env` file containing the Arbin CTI username,`ARBIN_CTI_USERNAME`, and password, `ARBIN_CTI_PASSWORD`.
Defaults to looking in the working directory.
"""
self.__config = CyclerInterfaceConfig(**config)
assert(self.__create_connection(
ip=self.__config.ip_address, port=self.__config.port, timeout_s=self.__config.timeout_s))
assert(self.__login(env_path))
self.__num_channels = self.get_login_feedback()['num_channels']
def get_num_channels(self):
'''
Returns the number of channels on the cycler
'''
return self.__num_channels
def get_login_feedback(self):
"""
Returns the login feedback message.
"""
return self.__login_feedback
def read_channel_status(self, channel: int) -> dict:
"""
Reads the channel status for the passed channel.
Parameters
----------
channel : int
The channel to read the status for.
Returns
-------
status : dict
A dictionary detailing the status of the channel. Returns None if there is an issue.
"""
channel_info_msg_rx_dict = {}
if (channel > self.__num_channels) or (channel < 0):
logger.error(f'Invalid channel value {channel}!')
return channel_info_msg_rx_dict
# Subtract one from the passed channel value to account for zero indexing
channel_info_msg_tx = Msg.ChannelInfo.Client.pack(
{'channel': (channel-1)})
response_msg_bin = self._send_receive_msg(
channel_info_msg_tx)
if response_msg_bin:
channel_info_msg_rx_dict = Msg.ChannelInfo.Server.unpack(
response_msg_bin)
return channel_info_msg_rx_dict
def _send_receive_msg(self, tx_msg):
"""
Sends the passed message and receives the response.
Parameters
----------
tx_msg : bytearray
Message to send.
Returns
-------
rx_msg : bytearray
Response message..
"""
rx_msg = b''
send_msg_success = False
msg_length_format = MessageABC.base_template['msg_length']['format']
msg_length_start_byte_idx = MessageABC.base_template['msg_length']['start_byte']
msg_length_end_byte_idx = msg_length_start_byte_idx + \
struct.calcsize(msg_length_format)
if self.__sock:
try:
self.__sock.sendall(tx_msg)
send_msg_success = True
except socket.timeout:
logger.error(
"Timeout on sending message from Arbin!", exc_info=True)
except socket.error:
logger.error(
"Failed to send message to Arbin!", exc_info=True)
if send_msg_success:
try:
# Receive first part of message and determine length of entire message.
rx_msg += self.__sock.recv(self.__config.msg_buffer_size)
expected_rx_msg_len = struct.unpack(
msg_length_format,
rx_msg[msg_length_start_byte_idx:msg_length_end_byte_idx])[0]
# Keep reading message in pieces until rx_msg is as long as expected_rx_msg_len.
while len(rx_msg) < (expected_rx_msg_len):
rx_msg += self.__sock.recv(self.__config.msg_buffer_size)
except socket.timeout:
logger.error(
"Timeout on receiving message from Arbin!", exc_info=True)
except socket.error:
logger.error(
"Error receiving message from Arbin!", exc_info=True)
else:
logger.error(
"Cannot send message! Socket does not exist!")
return rx_msg
def __create_connection(self, ip: str, port: int, timeout_s: float) -> bool:
"""
Creates a TCP/IP connection with Arbin server.
Parameters
----------
ip : str
The IP address of the Arbin cycler computer.
port : int
the port to connect to.
Returns
----------
success : bool
True/False based on whether or not the Arbin server connection was created.
"""
success = False
try:
self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.__sock.settimeout(timeout_s)
self.__sock.connect((ip, port))
logger.info("Connected to Arbin server!")
success = True
except:
logger.error(
"Failed to create TCP/IP connection with Arbin server!", exc_info=True)
return success
def __login(self, env_path: str) -> bool:
"""
Logs into the Arbin server with the username/password given in the env file defined in the path.
Must be done before issuing other commands.
Returns
-------
success : bool
True/False based on whether the login was successful
"""
success = False
logger.info(f'Loading environment variables from {env_path}')
dotenv.load_dotenv(env_path, override=True)
# Validate username and password are in the .env file.
if not os.getenv('ARBIN_CTI_USERNAME'):
raise ValueError('ARBIN_CTI_USERNAME not set in environment variables.')
if not os.getenv('ARBIN_CTI_PASSWORD'):
raise ValueError('ARBIN_CTI_PASSWORD not set in environment variables.')
login_msg_tx = Msg.Login.Client.pack(
msg_values={'username': os.getenv('ARBIN_CTI_USERNAME'), 'password': os.getenv('ARBIN_CTI_PASSWORD')})
response_msg_bin = self._send_receive_msg(login_msg_tx)
if response_msg_bin:
login_msg_rx_dict = Msg.Login.Server.unpack(response_msg_bin)
if login_msg_rx_dict['result'] == 'success':
success = True
logger.info(
"Successfully logged in to cycler " + str(login_msg_rx_dict['cycler_sn']))
logger.info(login_msg_rx_dict)
elif login_msg_rx_dict['result'] == "already logged in":
success = True
logger.warning(
"Already logged in to cycler " + str(login_msg_rx_dict['cycler_sn']))
elif login_msg_rx_dict['result'] == 'fail':
logger.error(
"Login failed with provided credentials!")
else:
logger.error(
f'Unknown login result {login_msg_rx_dict["result"]}')
self.__login_feedback = login_msg_rx_dict
return success
class CyclerInterfaceConfig(BaseModel):
'''
Holds channel config information for the CyclerInterface class.
Parameters
----------
ip_address : str
The IP address of the Arbin host computer.
port : int
The TCP port to communicate through.
timeout_s : float
How long to wait before timing out on TCP communication. Defaults to 3 seconds.
msg_buffer_size : int
How big of a message buffer to use for sending/receiving messages.
A minimum of 1024 bytes is recommended. Defaults to 4096 bytes.
'''
ip_address: str
port: int
timeout_s: float = 3.0
msg_buffer_size: int = 4096
Classes
class CyclerInterface (config: dict, env_path: str = '/Users/zander/Documents/Work/BattGenie/pycti/.env')
-
Class for interfacing with Arbin battery cycler at a cycler level.
Creates a class instance for interfacing with Arbin battery cycler at a cycler level.
Parameters
config
:dict
- A configuration dictionary. Must contain the following keys: ip_address : str The IP address of the Arbin host computer. port : int The TCP port to communicate through. timeout_s : optional : float How long to wait before timing out on TCP communication. Defaults to 3 seconds. msg_buffer_size : optional : int How big of a message buffer to use for sending/receiving messages. A minimum of 1024 bytes is recommended. Defaults to 4096 bytes.
env_path
:*optional* : str
- The path to the
.env
file containing the Arbin CTI username,ARBIN_CTI_USERNAME
, and password,ARBIN_CTI_PASSWORD
. Defaults to looking in the working directory.
Expand source code
class CyclerInterface: """ Class for interfacing with Arbin battery cycler at a cycler level. """ def __init__(self, config: dict, env_path: str = os.path.join(os.getcwd(), '.env')): """ Creates a class instance for interfacing with Arbin battery cycler at a cycler level. Parameters ---------- config : dict A configuration dictionary. Must contain the following keys: ip_address : str The IP address of the Arbin host computer. port : int The TCP port to communicate through. timeout_s : *optional* : float How long to wait before timing out on TCP communication. Defaults to 3 seconds. msg_buffer_size : *optional* : int How big of a message buffer to use for sending/receiving messages. A minimum of 1024 bytes is recommended. Defaults to 4096 bytes. env_path : *optional* : str The path to the `.env` file containing the Arbin CTI username,`ARBIN_CTI_USERNAME`, and password, `ARBIN_CTI_PASSWORD`. Defaults to looking in the working directory. """ self.__config = CyclerInterfaceConfig(**config) assert(self.__create_connection( ip=self.__config.ip_address, port=self.__config.port, timeout_s=self.__config.timeout_s)) assert(self.__login(env_path)) self.__num_channels = self.get_login_feedback()['num_channels'] def get_num_channels(self): ''' Returns the number of channels on the cycler ''' return self.__num_channels def get_login_feedback(self): """ Returns the login feedback message. """ return self.__login_feedback def read_channel_status(self, channel: int) -> dict: """ Reads the channel status for the passed channel. Parameters ---------- channel : int The channel to read the status for. Returns ------- status : dict A dictionary detailing the status of the channel. Returns None if there is an issue. """ channel_info_msg_rx_dict = {} if (channel > self.__num_channels) or (channel < 0): logger.error(f'Invalid channel value {channel}!') return channel_info_msg_rx_dict # Subtract one from the passed channel value to account for zero indexing channel_info_msg_tx = Msg.ChannelInfo.Client.pack( {'channel': (channel-1)}) response_msg_bin = self._send_receive_msg( channel_info_msg_tx) if response_msg_bin: channel_info_msg_rx_dict = Msg.ChannelInfo.Server.unpack( response_msg_bin) return channel_info_msg_rx_dict def _send_receive_msg(self, tx_msg): """ Sends the passed message and receives the response. Parameters ---------- tx_msg : bytearray Message to send. Returns ------- rx_msg : bytearray Response message.. """ rx_msg = b'' send_msg_success = False msg_length_format = MessageABC.base_template['msg_length']['format'] msg_length_start_byte_idx = MessageABC.base_template['msg_length']['start_byte'] msg_length_end_byte_idx = msg_length_start_byte_idx + \ struct.calcsize(msg_length_format) if self.__sock: try: self.__sock.sendall(tx_msg) send_msg_success = True except socket.timeout: logger.error( "Timeout on sending message from Arbin!", exc_info=True) except socket.error: logger.error( "Failed to send message to Arbin!", exc_info=True) if send_msg_success: try: # Receive first part of message and determine length of entire message. rx_msg += self.__sock.recv(self.__config.msg_buffer_size) expected_rx_msg_len = struct.unpack( msg_length_format, rx_msg[msg_length_start_byte_idx:msg_length_end_byte_idx])[0] # Keep reading message in pieces until rx_msg is as long as expected_rx_msg_len. while len(rx_msg) < (expected_rx_msg_len): rx_msg += self.__sock.recv(self.__config.msg_buffer_size) except socket.timeout: logger.error( "Timeout on receiving message from Arbin!", exc_info=True) except socket.error: logger.error( "Error receiving message from Arbin!", exc_info=True) else: logger.error( "Cannot send message! Socket does not exist!") return rx_msg def __create_connection(self, ip: str, port: int, timeout_s: float) -> bool: """ Creates a TCP/IP connection with Arbin server. Parameters ---------- ip : str The IP address of the Arbin cycler computer. port : int the port to connect to. Returns ---------- success : bool True/False based on whether or not the Arbin server connection was created. """ success = False try: self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.__sock.settimeout(timeout_s) self.__sock.connect((ip, port)) logger.info("Connected to Arbin server!") success = True except: logger.error( "Failed to create TCP/IP connection with Arbin server!", exc_info=True) return success def __login(self, env_path: str) -> bool: """ Logs into the Arbin server with the username/password given in the env file defined in the path. Must be done before issuing other commands. Returns ------- success : bool True/False based on whether the login was successful """ success = False logger.info(f'Loading environment variables from {env_path}') dotenv.load_dotenv(env_path, override=True) # Validate username and password are in the .env file. if not os.getenv('ARBIN_CTI_USERNAME'): raise ValueError('ARBIN_CTI_USERNAME not set in environment variables.') if not os.getenv('ARBIN_CTI_PASSWORD'): raise ValueError('ARBIN_CTI_PASSWORD not set in environment variables.') login_msg_tx = Msg.Login.Client.pack( msg_values={'username': os.getenv('ARBIN_CTI_USERNAME'), 'password': os.getenv('ARBIN_CTI_PASSWORD')}) response_msg_bin = self._send_receive_msg(login_msg_tx) if response_msg_bin: login_msg_rx_dict = Msg.Login.Server.unpack(response_msg_bin) if login_msg_rx_dict['result'] == 'success': success = True logger.info( "Successfully logged in to cycler " + str(login_msg_rx_dict['cycler_sn'])) logger.info(login_msg_rx_dict) elif login_msg_rx_dict['result'] == "already logged in": success = True logger.warning( "Already logged in to cycler " + str(login_msg_rx_dict['cycler_sn'])) elif login_msg_rx_dict['result'] == 'fail': logger.error( "Login failed with provided credentials!") else: logger.error( f'Unknown login result {login_msg_rx_dict["result"]}') self.__login_feedback = login_msg_rx_dict return success
Subclasses
Methods
def get_login_feedback(self)
-
Returns the login feedback message.
Expand source code
def get_login_feedback(self): """ Returns the login feedback message. """ return self.__login_feedback
def get_num_channels(self)
-
Returns the number of channels on the cycler
Expand source code
def get_num_channels(self): ''' Returns the number of channels on the cycler ''' return self.__num_channels
def read_channel_status(self, channel: int) ‑> dict
-
Reads the channel status for the passed channel.
Parameters
channel
:int
- The channel to read the status for.
Returns
status
:dict
- A dictionary detailing the status of the channel. Returns None if there is an issue.
Expand source code
def read_channel_status(self, channel: int) -> dict: """ Reads the channel status for the passed channel. Parameters ---------- channel : int The channel to read the status for. Returns ------- status : dict A dictionary detailing the status of the channel. Returns None if there is an issue. """ channel_info_msg_rx_dict = {} if (channel > self.__num_channels) or (channel < 0): logger.error(f'Invalid channel value {channel}!') return channel_info_msg_rx_dict # Subtract one from the passed channel value to account for zero indexing channel_info_msg_tx = Msg.ChannelInfo.Client.pack( {'channel': (channel-1)}) response_msg_bin = self._send_receive_msg( channel_info_msg_tx) if response_msg_bin: channel_info_msg_rx_dict = Msg.ChannelInfo.Server.unpack( response_msg_bin) return channel_info_msg_rx_dict
class CyclerInterfaceConfig (**data: Any)
-
Holds channel config information for the CyclerInterface class.
Parameters
ip_address : str The IP address of the Arbin host computer. port : int The TCP port to communicate through. timeout_s : float How long to wait before timing out on TCP communication. Defaults to 3 seconds. msg_buffer_size : int How big of a message buffer to use for sending/receiving messages. A minimum of 1024 bytes is recommended. Defaults to 4096 bytes.
Create a new model by parsing and validating input data from keyword arguments.
Raises ValidationError if the input data cannot be parsed to form a valid model.
Expand source code
class CyclerInterfaceConfig(BaseModel): ''' Holds channel config information for the CyclerInterface class. Parameters ---------- ip_address : str The IP address of the Arbin host computer. port : int The TCP port to communicate through. timeout_s : float How long to wait before timing out on TCP communication. Defaults to 3 seconds. msg_buffer_size : int How big of a message buffer to use for sending/receiving messages. A minimum of 1024 bytes is recommended. Defaults to 4096 bytes. ''' ip_address: str port: int timeout_s: float = 3.0 msg_buffer_size: int = 4096
Ancestors
- pydantic.main.BaseModel
- pydantic.utils.Representation
Class variables
var ip_address : str
var msg_buffer_size : int
var port : int
var timeout_s : float