Index: dialin/dg/calibration_record.py =================================================================== diff -u -ra27383c252f0f2c4468ce2cd6bf1aed38a809fff -r5fe81152c836440fe3c41308ebcc02cfbedce2cd --- dialin/dg/calibration_record.py (.../calibration_record.py) (revision a27383c252f0f2c4468ce2cd6bf1aed38a809fff) +++ dialin/dg/calibration_record.py (.../calibration_record.py) (revision 5fe81152c836440fe3c41308ebcc02cfbedce2cd) @@ -7,7 +7,7 @@ from ..common.msg_defs import MsgIds, MsgFieldPositions from ..protocols.CAN import (DenaliMessage, DenaliChannels) from ..utils.base import _AbstractSubSystem, _publish -from ..utils.utils import Utils +from ..utils.nv_ops_utils import NVOpsUtils from logging import Logger @@ -48,7 +48,7 @@ self.length = 0 self.cal_data = 0 self._raw_cal_record = [] - self._utilities = Utils() + self._utilities = NVOpsUtils() # DG calibration_record main record self.dg_calibration_record = OrderedDict() @@ -61,6 +61,13 @@ # Prepare the calibration_record record by putting sub-dictionaries together self._prepare_dg_calibration_record() + # Prepare the excel report and workspace + self._utilities.prepare_excel_report('DG', 'Calibration') + + def is_reading_record_done(self): + + return self._utilities.get_reading_status() + def get_dg_calibration_record(self): """ Handles getting DG calibration data from firmware. @@ -499,6 +506,8 @@ @return: none """ + self._utilities.write_excel_record_to_dialin_record(self.dg_calibration_record) + record_packets = self._utilities.prepare_record_to_send_to_fw(self.dg_calibration_record) self.logger.debug('Setting DG calibration started') @@ -522,3 +531,4 @@ if received_message is None: self.logger.debug("Timeout!!!!") return False + Index: dialin/dg/service_record.py =================================================================== diff -u -r97c98d84604e3700dd446235e91a5ebe2db08022 -r5fe81152c836440fe3c41308ebcc02cfbedce2cd --- dialin/dg/service_record.py (.../service_record.py) (revision 97c98d84604e3700dd446235e91a5ebe2db08022) +++ dialin/dg/service_record.py (.../service_record.py) (revision 5fe81152c836440fe3c41308ebcc02cfbedce2cd) @@ -5,7 +5,7 @@ from ..common.msg_defs import MsgIds, MsgFieldPositions from ..protocols.CAN import (DenaliMessage, DenaliChannels) from ..utils.base import _AbstractSubSystem, DialinEnum, _publish -from ..utils.utils import Utils +from ..utils.nv_ops_utils import NVOpsUtils from logging import Logger from enum import unique @@ -46,7 +46,7 @@ self.length = 0 self.service_data = 0 self.raw_service_record = [] - self.utilities = Utils() + self.utilities = NVOpsUtils() # Service main record self.dg_service_record = OrderedDict() Index: dialin/dg/system_record.py =================================================================== diff -u -r97c98d84604e3700dd446235e91a5ebe2db08022 -r5fe81152c836440fe3c41308ebcc02cfbedce2cd --- dialin/dg/system_record.py (.../system_record.py) (revision 97c98d84604e3700dd446235e91a5ebe2db08022) +++ dialin/dg/system_record.py (.../system_record.py) (revision 5fe81152c836440fe3c41308ebcc02cfbedce2cd) @@ -5,7 +5,7 @@ from ..common.msg_defs import MsgIds, MsgFieldPositions from ..protocols.CAN import (DenaliMessage, DenaliChannels) from ..utils.base import _AbstractSubSystem, DialinEnum, _publish -from ..utils.utils import Utils +from ..utils.nv_ops_utils import NVOpsUtils from logging import Logger from enum import unique @@ -48,7 +48,7 @@ self.length = 0 self.sys_data = 0 self.raw_system_record = [] - self.utilities = Utils() + self.utilities = NVOpsUtils() # System main record self.dg_system_record = OrderedDict() Index: dialin/hd/calibration_record.py =================================================================== diff -u -ra27383c252f0f2c4468ce2cd6bf1aed38a809fff -r5fe81152c836440fe3c41308ebcc02cfbedce2cd --- dialin/hd/calibration_record.py (.../calibration_record.py) (revision a27383c252f0f2c4468ce2cd6bf1aed38a809fff) +++ dialin/hd/calibration_record.py (.../calibration_record.py) (revision 5fe81152c836440fe3c41308ebcc02cfbedce2cd) @@ -6,7 +6,7 @@ from ..common.msg_defs import MsgIds, MsgFieldPositions from ..protocols.CAN import (DenaliMessage, DenaliChannels) from ..utils.base import _AbstractSubSystem, _publish -from ..utils.utils import Utils +from ..utils.nv_ops_utils import NVOpsUtils from logging import Logger @@ -45,7 +45,7 @@ self.length = 0 self.cal_data = 0 self.raw_cal_record = [] - self.utilities = Utils() + self.utilities = NVOpsUtils() # Calibration main record self.hd_calibration_record = OrderedDict() Index: dialin/hd/service_record.py =================================================================== diff -u -r97c98d84604e3700dd446235e91a5ebe2db08022 -r5fe81152c836440fe3c41308ebcc02cfbedce2cd --- dialin/hd/service_record.py (.../service_record.py) (revision 97c98d84604e3700dd446235e91a5ebe2db08022) +++ dialin/hd/service_record.py (.../service_record.py) (revision 5fe81152c836440fe3c41308ebcc02cfbedce2cd) @@ -5,7 +5,7 @@ from ..common.msg_defs import MsgIds, MsgFieldPositions from ..protocols.CAN import (DenaliMessage, DenaliChannels) from ..utils.base import _AbstractSubSystem, _publish, DialinEnum -from ..utils.utils import Utils +from ..utils.nv_ops_utils import NVOpsUtils from logging import Logger from enum import unique @@ -46,7 +46,7 @@ self.length = 0 self.service_data = 0 self.raw_service_record = [] - self.utilities = Utils() + self.utilities = NVOpsUtils() # Service main record self.hd_service_record = OrderedDict() Index: dialin/hd/system_record.py =================================================================== diff -u -r97c98d84604e3700dd446235e91a5ebe2db08022 -r5fe81152c836440fe3c41308ebcc02cfbedce2cd --- dialin/hd/system_record.py (.../system_record.py) (revision 97c98d84604e3700dd446235e91a5ebe2db08022) +++ dialin/hd/system_record.py (.../system_record.py) (revision 5fe81152c836440fe3c41308ebcc02cfbedce2cd) @@ -5,7 +5,7 @@ from ..common.msg_defs import MsgIds, MsgFieldPositions from ..protocols.CAN import (DenaliMessage, DenaliChannels) from ..utils.base import _AbstractSubSystem, _publish, DialinEnum -from ..utils.utils import Utils +from ..utils.nv_ops_utils import NVOpsUtils from logging import Logger from enum import unique @@ -49,7 +49,7 @@ self.length = 0 self.sys_data = 0 self.raw_system_record = [] - self.utilities = Utils() + self.utilities = NVOpsUtils() # System main record self.hd_system_record = OrderedDict() Index: dialin/ui/utils.py =================================================================== diff -u -r0392b232e0f257fb6946f6e8e2cdf4eadd05974d -r5fe81152c836440fe3c41308ebcc02cfbedce2cd --- dialin/ui/utils.py (.../utils.py) (revision 0392b232e0f257fb6946f6e8e2cdf4eadd05974d) +++ dialin/ui/utils.py (.../utils.py) (revision 5fe81152c836440fe3c41308ebcc02cfbedce2cd) @@ -5,7 +5,7 @@ # THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN # WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. # -# @file utils.py +# @file nv_ops_utils.py # # @author (last) Behrouz NematiPour # @date (last) 10-Nov-2020 Index: dialin/utils/excel_ops.py =================================================================== diff -u --- dialin/utils/excel_ops.py (revision 0) +++ dialin/utils/excel_ops.py (revision 5fe81152c836440fe3c41308ebcc02cfbedce2cd) @@ -0,0 +1,178 @@ + +import os +import math +import datetime +from openpyxl.styles import PatternFill, Font, Alignment +from openpyxl.utils import get_column_letter +from openpyxl import Workbook, load_workbook + + +class ExcelOps: + """ + Publicly accessible parent class for all observers. + + The class provides functions to operate an excel file + """ + COLUMN_WIDTH_TOLERANCE = 3.0 + COLUMN_MAX_WIDTH = 50.0 + ROW_DEFAULT_HEIGHT = 15.0 + + def __init__(self): + """ + Base class constructor + + """ + # Color definitions + self.green = PatternFill(start_color='00FF00', end_color='00FF00', fill_type='solid') + self.yellow = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid') + self.blue = PatternFill(start_color='00CCFF', end_color='00CCFF', fill_type='solid') + self.red = PatternFill(start_color='FF0000', end_color='FF0000', fill_type='solid') + + @staticmethod + def get_an_excel_workbook(): + + return Workbook() + + @staticmethod + def setup_excel_worksheet(workbook_obj, title, index=None): + """ + Creates the worksheets in the created excel file with the name of each software_project_name as the name of + the worksheet. + + @param workbook_obj: Excel workbook object + @param title: Title of the created worksheet + @param index: Index of the worksheet. If a sheet needs to be at a certain place (default none) + + @return none + """ + # Create a tab and name the tab with the name of the projects dictionary + workbook_obj.create_sheet(title=title, index=index) + + # If the first tab is Sheet or Sheet1, remove it + # The other tabs must be created first before removing the default tab + if workbook_obj.sheetnames[0] == 'Sheet' or workbook_obj.sheetnames[0] == 'Sheet1': + sheet = workbook_obj.get_sheet_by_name(workbook_obj.sheetnames[0]) + workbook_obj.remove_sheet(sheet) + + def write_to_excel(self, workbook_obj, project, row, column, data, name='Calibri', font=11, bold=False, color=None, + merge=None, horizontal='left', freeze=False, max_col_len=None): + """ + This function writes data at the specified row and column in an excel file (object). + + @param workbook_obj: Excel workbook object + @param project: Name of the sheet (i.e HD-DEN-4308) + @param row: Current row number + @param column: Current column number + @param data: Data to be written at the cell + @param name: Font type (default Calibri) + @param font: Font size (default 11) + @param bold: Bold or un-bold (default un-bold) + @param color: Color of the cell (default no color) + @param merge: Merge cells. Cells must be provided with A1:A4 format (default none) + @param horizontal: Horizontal alignment (default left) + @param freeze: Freeze top row (default false) + @param max_col_len: maximum length of a column (default none, means it is not restricted) + + @return: None + """ + row_height = 0 + # Get the number of an alphabetic column (i.e A -> 1) + column_letter = get_column_letter(column) + cell_name = column_letter + str(row) + + # Set the active worksheet to the provided worksheet + active_sheet = workbook_obj[project] + # Set the provided data into the specified row and column and set the bold, color and horizontal alignment + active_sheet.cell(row=row, column=column).value = data + active_sheet.cell(row=row, column=column).font = Font(size=font, bold=bold, name=name) + # Wrapping text is not needed unless the length of the data is more than maximum column length + active_sheet[cell_name].alignment = Alignment(vertical='center', horizontal=horizontal, wrap_text=False) + # Get the width of the current column + column_width = active_sheet.column_dimensions[column_letter].width + # When the column width is on the default, openpyxl reports None. If the width is reported as None, + # it will be set to 0 for math comparison + column_width = 0 if column_width is None else column_width + # If the length is not provided, use the default maximum length + max_len = self.COLUMN_MAX_WIDTH if max_col_len is None else max_col_len + # Remove all the end of the line artifacts + length_of_data = len(str(data).rstrip()) + + # If the length of data was greater than the maximum length, calculate the number of rows is needed with the + # default height + if length_of_data > max_len: + # Since the length is greater than maximum, enable wrap text + active_sheet[cell_name].alignment = Alignment(vertical='center', horizontal=horizontal, wrap_text=True) + # Calculate what the row height should be when the cell is extended + row_height = math.ceil(length_of_data / max_len) * self.ROW_DEFAULT_HEIGHT + active_sheet.column_dimensions[column_letter].width = max_len + # If the length of the data provided is already less than the length of cell, do nothing + elif column_width < length_of_data: + active_sheet.column_dimensions[column_letter].width = length_of_data + self.COLUMN_WIDTH_TOLERANCE + row_height = self.ROW_DEFAULT_HEIGHT + + # If the current row height is not defined or the row height is less than the calculated new height, set it + if active_sheet.row_dimensions[row].height is None or active_sheet.row_dimensions[row].height < row_height: + active_sheet.row_dimensions[row].height = row_height + + # If color has been defined, set the color of the cell + if color is not None: + active_sheet[cell_name].fill = color + + # If merge has been requested + if merge is not None: + # The format of merge for this function is A1:C1 + active_sheet.merge_cells(str(merge)) + + if freeze: + # To freeze row 1, make the cell is not row 1, that's why A2 was chosen + active_sheet.freeze_panes = 'A2' + + @staticmethod + def load_excel_report(path): + + return load_workbook(path) + + @staticmethod + def get_cell_value(workbook_obj, project, row, col): + + # Set the active worksheet to the provided worksheet + active_sheet = workbook_obj[project] + cell_obj = active_sheet.cell(row=row, column=col) + + return cell_obj.value + + + @staticmethod + def merge_cells(workbook_obj, project, start_row, start_col, end_row, end_col): + """ + This function merges the specified cells. + + @param workbook_obj: Excel workbook object + @param project: Name of the sheet (i.e HD-DEN-4308) + @param start_row: Row number at the beginning of merge + @param start_col: Column number at the beginning of merge + @param end_row: Row number at the end of merge + @param end_col: Column number at the end of merge + + @return: None + """ + # Go to the define sheet + active_sheet = workbook_obj[project] + # Convert the cell numbers to text and number (i.e row=1 and col=1 -> A1) for the start and end cells + merge_start_cell = get_column_letter(start_col) + str(start_row) + merge_end_cell = get_column_letter(end_col) + str(end_row) + # The format of merge for this function is A1:C1 + active_sheet.merge_cells(str(merge_start_cell) + ':' + str(merge_end_cell)) + + @staticmethod + def save_report(excel_workbook, save_dir, record_name): + """ + This function overrides the save function in the Base class. The function saves the excel file. + + @returns none + """ + # Get the current date + current_date = str(datetime.datetime.now().date()) + # Create the save path by using the path, date and current code review excel count out of total number of them + path = os.path.join(save_dir, current_date + '-' + str(record_name).upper() + '-Record.xlsx') + excel_workbook.save(filename=path) \ No newline at end of file Index: dialin/utils/nv_ops_utils.py =================================================================== diff -u --- dialin/utils/nv_ops_utils.py (revision 0) +++ dialin/utils/nv_ops_utils.py (revision 5fe81152c836440fe3c41308ebcc02cfbedce2cd) @@ -0,0 +1,586 @@ + +import struct +import datetime +import math +import os +import shutil +from ..ui.crc import crc_16 +from .excel_ops import ExcelOps + +class NVOpsUtils: + """ + + Dialysate Generator (DG) Dialin API sub-class for utilities commands. The commands are used to process + the calibration_record records, service records, system records, and the scheduled runs records. The records are + prepared to be sent to firmware to be received from firmware. + """ + _RECORD_START_INDEX = 6 + _RECORD_SPECS_BYTES = 12 + _RECORD_SPECS_BYTE_ARRAY = 3 + + _CURRENT_MESSAGE_NUM_INDEX = 0 + + _DATA_TYPE_INDEX = 0 + _DATA_VALUE_INDEX = 1 + _CHAR_LENGTH_INDEX = 2 + _DICT_VALUE_LIST_LEN = 2 + + _TARGET_BYTES_TO_SEND_TO_FW = 150 + _MIN_PAYLOAD_BYTES_SPACE = 4 + + _PAYLOAD_CURRENT_MSG_INDEX = 0 + _PAYLOAD_TOTAL_MSG_INDEX = 1 + _PAYLOAD_TOTAL_BYTES_INDEX = 2 + + def __init__(self): + + self._workspace_dir = '' + self._excel_workbook = '' + self._record_name = '' + self._firmware_stack = '' + self._is_read_done = False + + self.excel_ops = ExcelOps() + # This list contains different data packet lists + # i.e. [[message 1, payload], [message 2, payload], ...] + self._record_packets_to_send_to_fw = [] + + # Buffer that is used to keep the groups data except the crc to convert them to bytes and calculate the crc + # of the group. + self._temp_groups_data_to_calculate_crc = [] + + def prepare_excel_report(self, firmware_stack, record_name): + """ + Publicly accessible function to prepare the excel report + + @param firmware_stack: firmware stack name such as HD or DG + @param record_name: record type to check such as calibration, system, ... + + @return none + """ + # Create the workspace name + workspace_name = str(firmware_stack) + '_NV_Records' + + self.create_workspace(workspace_name) + + # Get an excel workbook object + self._excel_workbook = self.excel_ops.get_an_excel_workbook() + + self._record_name = record_name + self._firmware_stack = firmware_stack + # Setup worksheet and create the current tab + self.excel_ops.setup_excel_worksheet(self._excel_workbook, self._record_name) + + def write_fw_record_to_excel(self, dialin_record): + + row = 1 + + for group in dialin_record.keys(): + + start_row = row + start_col = 1 + col = 1 + if isinstance(dialin_record[group], dict): + + self.excel_ops.write_to_excel(self._excel_workbook, self._record_name, row, col, group, bold=True) + + for hardware in dialin_record[group].keys(): + list_of_keys = list(dialin_record[group].keys()) + col = 2 + self.excel_ops.write_to_excel(self._excel_workbook, self._record_name, row, col, hardware) + col += 1 + for spec in dialin_record[group][hardware]: + spec_value = dialin_record[group][hardware][spec][1] + self.excel_ops.write_to_excel(self._excel_workbook, self._record_name, row, col, spec) + col += 1 + self.excel_ops.write_to_excel(self._excel_workbook, self._record_name, row, col, spec_value) + col += 1 + + if list_of_keys.index(hardware) == len(list_of_keys) - 1: + self.excel_ops.merge_cells(self._excel_workbook, self._record_name, start_row, start_col, + row, start_col) + row += 1 + + row += 1 + + self.excel_ops.save_report(self._excel_workbook, self._workspace_dir, self._firmware_stack) + self._is_read_done = True + + def write_excel_record_to_dialin_record(self, dialin_record): + """ + Publicly accessible function to write excel record to Dialin record + + @param dialin_record: Dialin record dictionary(i.e calibration record) + + @return True if reading to firmware records to excel is done otherwise False + """ + + path = '' + row = 1 + # List all the files in the workspace directory + for file in os.listdir(self._workspace_dir): + # If the file has an extension of .xlsx + if file.endswith('.xlsx'): + # Check if the firmware stack (i.e. DG) is in the file and name of the file + # does not have lock in it. When the file is open, there is a hidden lock file + # in there and it is ignored + if self._firmware_stack in str(file) and 'lock' not in str(file): + # Create the file path and exit the loop + path = os.path.join(self._workspace_dir, file) + break + + # Load the excel workbook + self._excel_workbook = self.excel_ops.load_excel_report(path) + + for group in dialin_record.keys(): + + col = 1 + cell_value = self.excel_ops.get_cell_value(self._excel_workbook, self._record_name, row, col) + + if cell_value == group: + print(cell_value) + col = 4 + if isinstance(dialin_record[group], dict): + for hardware in dialin_record[group].keys(): + for spec in dialin_record[group][hardware]: + cell_value = self.excel_ops.get_cell_value(self._excel_workbook, self._record_name, row, + col) + print(spec, cell_value) + dialin_record[group][hardware][spec][1] = cell_value + col += 2 + + row += 1 + col = 4 + + row += 1 + #print(self.excel_ops.get_cell_value(self._excel_workbook, self._record_name, 1, 1)) + + def get_reading_status(self): + """ + Publicly accessible function to get the reading status + + @return True if reading to firmware records to excel is done otherwise False + """ + + return self._is_read_done + + def create_workspace(self, dir_name): + """ + Publicly accessible function to get create a workspace for the script that is running. + + @param dir_name: Name of the workspace directory + + @return none + """ + # Get the root directory of the current script + scripts_root_dir = os.path.dirname(os.path.dirname(__file__)) + # Get the root directory of the entire scripts folder. The workspace that holds the + # code review reports and clones other scripts and repositories must be outside of the scripts + root_dir = os.path.dirname(os.path.dirname(scripts_root_dir)) + # Create the address of the workspace + self._workspace_dir = os.path.join(root_dir, dir_name) + # If the path does not exist, make it, otherwise, change to that directory + if not os.path.isdir(self._workspace_dir): + # Create the directory and go to it + os.mkdir(self._workspace_dir) + os.chdir(self._workspace_dir) + + @staticmethod + def get_current_time_in_epoch(): + """ + Returns the current date and time in epoch in integer format. This is a static method. + + @return: data and time in epoch in integer format + """ + return int(datetime.datetime.now().timestamp()) + + def process_received_record_from_fw(self, dialin_record, fw_raw_records_bytes): + """ + Handles processing the received record from firmware + + @param dialin_record: dictionary that is being updated from firmware + @param fw_raw_records_bytes: list of the firmware record from firmware in bytes + + @return: none + """ + raw_payload_temp_start_index = 0 + # Convert the concatenated raw data into a byte array since the struct library requires byte arrays. + fw_raw_records_bytes = bytearray(fw_raw_records_bytes) + + # Loop through the keys for the main calibration_record dictionary + # DG_Calibration : {pressure_sensors : { ppi : { gain: [' self._MIN_PAYLOAD_BYTES_SPACE: + current_payload_length += data_type_bytes + temp_buffer[self._PAYLOAD_TOTAL_MSG_INDEX] = struct.pack(' self._MIN_PAYLOAD_BYTES_SPACE: + current_payload_length += data_type_bytes + # Insert a 4-byte 0 to the index of the total messages. This is a place holder and it will + # be updated with the right value later. + temp_buffer[self._PAYLOAD_TOTAL_MSG_INDEX] = struct.pack(' self._DICT_VALUE_LIST_LEN: + byte_size += current_byte_size * group[key][self._CHAR_LENGTH_INDEX] + else: + byte_size += current_byte_size + + return byte_size + + @staticmethod + def get_data_type_bytes(data): + """ + Handles converting the string representation of the bytes of the data types in a struct to numbers. + This is a static method. + + @param data: the data to be converted to bytes in number + @return: calculated byte size + """ + number_of_bytes = 0 + # Unsigned integer = i + # Signed integer = I + # Float = f. All 4 bytes. + if data == '