def test_can_item(): # make a valid frame cf = CanFrame.from_raw_canbadger_bytes( b'\x16\xaa\xaa\xaa\xaa\x00\x00\x02\x35\x40\x42\x0f\x00\x02\xcc\xdd' ) #make a valid frame # create can item and parent parent = CanItem() item = CanItem(parent=parent, frame=cf) parent.append_child(item) assert item.data(2) == str(b'\xcc\xdd') assert item.column_count() == 4 assert parent.child_count() == 1 assert parent.child(0) is item
class CanLoggerItemModel(QAbstractItemModel): resetRow = Signal(int) def __init__(self, parent=None, *args): super().__init__(parent, *args) self.root = CanItem(model=self) self.header_labels = ['#', 'ID', 'Payload', 'Interface', 'Occurrence'] # used to keep count of frames, could use root children but we might nest more in the future! self.frame_count = 0 # occurrence_dict to keep track of multiples of frames (aka count frames with same content) # sender_dict to track last message from each sender id # highlight_dict to store highlight information self.occurrence_dict = dict() self.sender_dict = dict() self.highlight_dict = dict() # return an index for the given row/column pair and the parent element # this index is then used by views to access data def index(self, row: int, column: int, parent: QModelIndex = None) -> QModelIndex: # get parent CanItem if not parent or not parent.isValid(): parent_item = self.root else: parent_item = parent.internalPointer() # check if row is valid if row >= parent_item.child_count(): return QModelIndex() child_item = parent_item.child(row) # check if column is valid for child if column >= child_item.column_count(): return QModelIndex() # return the valid index for the childs column return self.createIndex(row, column, child_item) def parent(self, index: QModelIndex = None) -> QModelIndex: if not index.isValid(): return QModelIndex() child_item = index.internalPointer() parent_item = child_item.parent() # return invalid index if we hit root if parent_item == self.root: return QModelIndex() return self.createIndex(parent_item.row(), 0, parent_item) # returns the amount of nested rows aka children for the parent item the index identifies def rowCount(self, parent: QModelIndex = None) -> int: # if the column count is nonzero we return # indices that identify a node/row and not a value/field have column=0 by convention if parent is not None and parent.column() > 0: return 0 if not parent or not parent.isValid(): parent_item = self.root else: parent_item = parent.internalPointer() return parent_item.child_count() # returns the number of columns available for an index def columnCount(self, index: QModelIndex = None) -> int: # for now, we accept indices with column>0, as there is no drawback if index is not None and index.isValid(): return index.internalPointer().column_count() return self.root.column_count() # returns the value of the data attribute mapped to the column # column mapping is done in the respective class the CanItem holds # via the column_representation classmethod def data(self, index: QModelIndex, role: int = None): # we return None for root data or roles we dont serve if not index.isValid(): return None elif role != Qt.DisplayRole: return None # we get the desired data by calling the CanItems data() function # that then calls the held objects get_data function return index.internalPointer().data(index.column()) def flags(self, index: QModelIndex) -> Qt.ItemFlags: if not index.isValid(): return Qt.NoItemFlags if index.column() != 2: return Qt.ItemIsEnabled | Qt.ItemIsSelectable else: return Qt.ItemIsEnabled # returns header labeling, atm just copied from the table model def headerData(self, section, orientation, role: Qt.DisplayRole = None): if role == Qt.DisplayRole and orientation == Qt.Orientation.Horizontal: return self.header_labels[section] return QAbstractTableModel.headerData(self, section, orientation, role) # we wrap the frame in an item and attack it as a child to the index def add_frame(self, index: QModelIndex, frame: CanFrame): # find the parent if not index.isValid(): parent_item = self.root else: parent_item = index.internalPointer() # save models frame counter in frame self.frame_count += 1 frame.set_counter(self.frame_count) # create new item and add as child item = CanItem(model=self, parent=parent_item, frame=frame) # send insert signals and append child self.beginInsertRows(index, parent_item.child_count(), parent_item.child_count()) parent_item.append_child(item) # add to hashmap self.hash_increase(item) self.endInsertRows() # retrieve the object stored in the items data at index ## TODO replace return with Union[CanFrame, CanMessage] def get_can_object(self, index: QModelIndex) -> CanFrame: if not index.isValid(): return None return index.internalPointer().get_data() # get the type of object that is stored in the CanItem def get_can_object_type(self, index: QModelIndex) -> ItemType: if not index.isValid(): return None return index.internalPointer().get_type() # add multiple frames to model def add_frames(self, index: QModelIndex, frames: [CanFrame]): for frame in frames: self.add_frame(index, frame) # get list representation of all frames def get_frame_list(self): frames = [] for item in self.root.children: frames.append(item.list_representation()) return frames # removes all items except for the root # can be used together with add_frames() to achieve the table models set_frames() functionality # TODO affect hashmap correctly def clear(self, index: QModelIndex): if not index.isValid(): parent_item = self.root else: parent_item = index.internalPointer() parent_item.remove_children() self.layoutChanged.emit() def remove_item(self, index: QModelIndex): # cant remove root if not index.isValid(): return # retrieve parameters fo signal call parent_item = index.internalPointer().parent() parent_index = self.parent(index) row = index.row() # decrease hashmap entry self.hash_decrease(index.internalPointer().get_compare_hash()) # remove child from parent self.beginRemoveRows(parent_index, row, row) parent_item.remove_child(index.row()) self.endRemoveRows() self.layoutChanged.emit() # functions that increase or decrease counts in the hashmap we use to track multiples of frames (frame occurrence) # and keep track of the last message from each id + use this info to get the highlighting done def hash_increase(self, item: CanItem): # handle occurrence item_hash = item.get_compare_hash() if item_hash in self.occurrence_dict: self.occurrence_dict[item_hash] += 1 else: self.occurrence_dict[item_hash] = 1 if item.get_type() == ItemType.CanFrame: # check for highlighting f_id = item.get_data().frame_id if f_id in self.sender_dict: last_sent_row = self.sender_dict[f_id] # compare payloads to decide which bytes need highlighting last_sent_payload = self.root.child( last_sent_row).get_data().frame_payload frame = item.get_data() now_sent_payload = frame.frame_payload self.highlight_dict[item.row()] = highlighting_compare( last_sent_payload, now_sent_payload) # set this item as the last message from the sender last_row = None if f_id not in self.sender_dict else self.sender_dict[ f_id] self.sender_dict[f_id] = item.row() if last_row is not None: self.resetRow.emit(last_row) def hash_decrease(self, item_hash: int): # handle occurrence if item_hash in self.occurrence_dict: self.occurrence_dict[item_hash] -= 1 if self.occurrence_dict[item_hash] == 0: del self.occurrence_dict[item_hash] # TODO sender handling # this could be nasty because we have to search for the previously last sent message # which could add significant overhead for big frame counts # currently not implemented as we remove frames, like, never def hash_get(self, item_hash: int) -> int: return self.occurrence_dict[item_hash] # checks in the sender dict if a given row was the last frame for a given sender id def is_last_from_sender(self, f_id: int, row: int) -> bool: if self.sender_dict[f_id] == row: return True return False # used to transform indices with no valid internal pointer def translate_index(self, invalid_index: QModelIndex) -> QModelIndex: if invalid_index.parent().isValid(): return self.index(invalid_index.row(), invalid_index.column(), self.translate_index(invalid_index.parent())) else: return self.index(invalid_index.row(), invalid_index.column(), QModelIndex())