def CAN_Read(self) -> int:
  # Mirrors VB: loops reading until "queue empty" bit (0x20) is present.
  str2 = ""  # kept for parity (debug-ish accumulation), not otherwise used
  flag1 = False
  numArray = [0] * 22

  EventData = self.clsCANMotor.sEventData()

  while True:
    MessageBuffer = TPCANMsg()
    TimestampBuffer = TPCANTimestamp()

    ErrCode = int(PCANBasic.Read(int(self.CAN_Device), MessageBuffer, TimestampBuffer))

    # VB: if ErrCode != 0 and ErrCode != 32 (0x20) => error
    if ErrCode not in (0, 0x20):
      self.ErrorCode[5] = self.CAN_GetError(ErrCode)
      return 5

    if ErrCode == 0:
      cob_id = int(MessageBuffer.ID)
      num1 = cob_id // 0x80
      index1 = cob_id - (num1 * 0x80)  # node id (0..127)

      data = MessageBuffer.DATA
      data_len = int(getattr(MessageBuffer, "LEN", len(data)))

      if num1 == 0:
        pass

      elif num1 == 1:
        # EMCY message
        # CombineBytes(DATA,1,0) => little-endian uint16 from bytes [0],[1]
        err_code = int(data[0]) | (int(data[1]) << 8)
        err_reg = int(data[2])
        elmo_err = int(data[3])
        err_data1 = int(data[4]) | (int(data[5]) << 8)
        err_data2 = int(data[6]) | (int(data[7]) << 8)

        sEmcy = self.clsCANMotor.sEmcy()
        sEmcy.ErrCode = err_code
        sEmcy.ErrReg = err_reg
        sEmcy.ElmoErrCode = elmo_err
        sEmcy.ErrCodeData1 = err_data1
        sEmcy.ErrCodeData2 = err_data2
        self.LastEmcy = sEmcy

        flag2 = False  # VB uses to suppress event on some IP buffer underflow case
        desc = ""

        # IMPORTANT: compare INTS, not hex strings
        if err_code == 0xFF00:
          if elmo_err == 0x56:
            self.PVT_EMCY[index1].QueueLow.State = True
            self.PVT_EMCY[index1].QueueLow.WritePointer = err_data1
            self.PVT_EMCY[index1].QueueLow.ReadPointer = err_data2
            desc = "Queue Low"
          elif elmo_err == 0x5B:
            self.PVT_EMCY[index1].BadHeadPointer = True
            desc = "Bad Head Pointer"
          elif elmo_err == 0x34:
            self.PVT_EMCY[index1].QueueFull.State = True
            self.PVT_EMCY[index1].QueueFull.FailedWritePointer = err_data1
            desc = "Queue Full"
          elif elmo_err == 0x07:
            self.PVT_EMCY[index1].BadModeInitData = True
            desc = "Bad Mode Init Data"
          elif elmo_err == 0x08:
            self.PVT_EMCY[index1].MotionTerminated = True
            desc = "Motion Terminated"
          elif elmo_err == 0xA6:
            self.PVT_EMCY[index1].OutOfModulo = True
            desc = "Out Of Modulo"

        elif err_code == 0x8110:
          desc = "CAN message lost (corrupted or overrun)"
        elif err_code == 0x8200:
          desc = "Protocol error (unrecognized NMT request)"
        elif err_code == 0x8210:
          desc = "Attempt to access an unconfigured RPDO"
        elif err_code == 0x8130:
          desc = "Heartbeat event"
        elif err_code == 0x6180:
          desc = "Fatal CPU error: stack overflow"
        elif err_code == 0x6200:
          desc = "User program aborted by an error"
        elif err_code == 0xFF01:
          desc = "Request by user program 'emit' function"
        elif err_code == 0x6300:
          desc = (
            "Object mapped to an RPDO returned an error during interpretation or a referenced motion "
            "failed to be performed"
          )
        elif err_code == 0xFF02:
          if elmo_err == 0x07:
            self.PVT_EMCY[index1].BadModeInitData = True
            desc = "Bad Mode Init Data"
          elif elmo_err == 0x08:
            self.PVT_EMCY[index1].MotionTerminated = True
            desc = "Motion Terminated"
          elif elmo_err == 0x34:
            self.PVT_EMCY[index1].QueueFull.State = True
            self.PVT_EMCY[index1].QueueFull.FailedWritePointer = err_data1
            desc = "Queue Full"
          elif elmo_err == 0x56:
            self.PVT_EMCY[index1].QueueLow.State = True
            self.PVT_EMCY[index1].QueueLow.WritePointer = err_data1
            self.PVT_EMCY[index1].QueueLow.ReadPointer = err_data2
            desc = "Queue Low"
          elif elmo_err == 0x5B:
            self.PVT_EMCY[index1].BadHeadPointer = True
            desc = "Bad Head Pointer"
          elif elmo_err == 0x8A:
            self.PVT_EMCY[index1].QueueLow.State = True
            desc = "Position Interpolation buffer underflow"
            flag2 = True
          elif elmo_err == 0xA6:
            self.PVT_EMCY[index1].OutOfModulo = True
            desc = "Out Of Modulo"
          elif elmo_err == 0xBA:
            self.PVT_EMCY[index1].QueueFull.State = True
            desc = "Interpolation queue is full"
            flag2 = False
          elif elmo_err == 0xBB:
            desc = "Incorrect interpolation sub-mode"
          else:
            desc = f"DS402 IP Error 0x{elmo_err:02X}"

        elif err_code == 0x7300:
          desc = "Resolver or Analog Encoder feedback failed"
          flag1 = True
        elif err_code == 0x7380:
          desc = "Feedback loss: no match between encoder and Hall locations."
          flag1 = True
        elif err_code == 0x8311:
          desc = "Peak current has been exceeded due to drive malfunction or badly-tuned current controller"
          flag1 = True
        elif err_code == 0x5441:
          desc = "E-stop button was pressed"
          flag1 = True
        elif err_code == 0x5280:
          desc = "ECAM table problem"
        elif err_code == 0x7381:
          desc = (
            "Two digital Hall sensors changed at once; only one sensor can be changed at a time"
          )
          flag1 = True
        elif err_code == 0x8480:
          desc = "Speed tracking error"
          flag1 = True
        elif err_code == 0x8611:
          desc = "Position tracking error"
          flag1 = True
        elif err_code == 0x6320:
          desc = "Cannot start due to inconsistent database"
        elif err_code == 0x8380:
          desc = "Cannot find electrical zero of motor when attempting to start motor"
        elif err_code == 0x8481:
          desc = "Speed limit exceeded"
          flag1 = True
        elif err_code == 0x6181:
          desc = "CPU exception: fatal exception"
        elif err_code == 0x5281:
          desc = "Timing error"
        elif err_code == 0x7121:
          desc = "Motor stuck: motor powered but not moving"
          flag1 = True
        elif err_code == 0x8680:
          desc = "Position limit exceeded"
          flag1 = True
        elif err_code == 0x8381:
          desc = "Cannot tune current offsets"
        elif err_code == 0xFF10:
          desc = "Cannot start motor"
        elif err_code == 0x5400:
          desc = "Cannot start motor"
        elif err_code == 0x3120:
          desc = "Under-voltage: power supply is shut down or it has too high an output impedance"
          flag1 = True
        elif err_code == 0x3310:
          desc = (
            "Over-voltage: power supply voltage is too high or servo driver could not absorb kinetic energy "
            "while braking a load"
          )
          flag1 = True
        elif err_code == 0x2340:
          desc = "Short circuit: motor or its wiring may be defective, or drive is faulty"
          flag1 = True
        elif err_code == 0x4310:
          desc = "Temperature: drive overheating"
          flag1 = True
        elif err_code == 0xFF20:
          desc = "Safety switch is sensed - Drive in safety state"
          flag1 = True

        if flag1:
          self.EmcyMoveErrorReceived = True
          self.EmcyMoveError = desc
          self.EmcyMoveErrorNodeID = index1

        if not flag2:
          EventData.EventType = self.clsCANMotor.eEventType.CANEmergencyMessageReceived
          EventData.NodeID = index1
          EventData.CEMR_Description = desc
          EventData.CEMR_EmcyMsg = sEmcy
          EventData.CEMR_DisableMotors = bool(flag1)
          self.RaiseAnEvent(EventData)

      elif num1 in (3, 7, 9):
        # TPDO1/3/4
        if num1 == 3:
          tpdo = self.clsCANMotor.eTPDO.TPDO1
        elif num1 == 7:
          tpdo = self.clsCANMotor.eTPDO.TPDO3
        else:
          tpdo = self.clsCANMotor.eTPDO.TPDO4

        if self.NodeIDList is not None and self.GetNodeIDIndex(index1) > -1:
          node_idx = self.GetNodeIDIndex(index1)

          if self.TPDOMappedObject is not None:
            num2 = 0
            # UBound(dim=3) => iterate index3 over third dimension length.
            # We infer from storage: TPDOMappedObject[node, tpdo, i]
            # Determine max i by probing sequentially until IndexError.
            index3 = 0
            while True:
              try:
                mapped_obj = _get_item_3d(self.TPDOMappedObject, index1, int(tpdo), index3)
              except Exception:
                break

              mapped_int = int(mapped_obj)

              # read value based on width flags (0x10 => 16-bit, 0x20 => 32-bit)
              num4: int = 0
              if (mapped_int & 0x10) == 0x10:
                lo = int(data[0 + num2])
                hi = int(data[1 + num2])
                num4 = (hi << 8) | lo
                num2 += 2

              if (mapped_int & 0x20) == 0x20:
                b0 = int(data[0 + num2])
                b1 = int(data[1 + num2])
                b2 = int(data[2 + num2])
                b3 = int(data[3 + num2])
                num4 = (b3 << 24) | (b2 << 16) | (b1 << 8) | b0
                num2 += 4

              # Switch on mapped object identity
              if mapped_obj == self.clsCANMotor.eTPDOMappedObject.Timestamp:
                self.LastTimeStamp = num4

              elif mapped_obj == self.clsCANMotor.eTPDOMappedObject.StatusWord:
                self.MoveDoneResponse[node_idx] = True
                EventData.EventType = self.clsCANMotor.eEventType.MotionStatusReceived
                EventData.NodeID = index1
                EventData.MSR_Status = True
                EventData.MSR_StatusWord = (
                  int(num4 & 0xFFFF) if num4 <= 0x7FFF else int((num4 & 0xFFFF) - 0x10000)
                )
                self.RaiseAnEvent(EventData)

              elif mapped_obj == self.clsCANMotor.eTPDOMappedObject.PositionActualValue:
                # uint32 -> int32
                if num4 <= 0x7FFFFFFF:
                  pos = int(num4)
                else:
                  pos = int(num4 - 0x100000000)

                if self.EncoderPosition[node_idx] != pos:
                  self.EncoderPosition[node_idx] = pos
                  EventData.EventType = self.clsCANMotor.eEventType.MotorPositionChanged
                  EventData.NodeID = index1
                  # VB builds array sized by NodeIDList; then assigns EncoderPosition.
                  # Keep parity: provide the current array.
                  EventData.MPC_Position = self.EncoderPosition
                  EventData.MPC_Position[node_idx] = pos
                  self.RaiseAnEvent(EventData)

              elif mapped_obj == self.clsCANMotor.eTPDOMappedObject.DigitalInputs:
                # Bits 16..21 -> six inputs mapped into a 6-bit state [1..6]
                numArray[16] = 1 if (num4 & 0x00010000) else 0
                numArray[17] = 1 if (num4 & 0x00020000) else 0
                numArray[18] = 1 if (num4 & 0x00040000) else 0
                numArray[19] = 1 if (num4 & 0x00080000) else 0
                numArray[20] = 1 if (num4 & 0x00100000) else 0
                numArray[21] = 1 if (num4 & 0x00200000) else 0

                num6 = (
                  numArray[16]
                  + (numArray[17] << 1)
                  + (numArray[18] << 2)
                  + (numArray[19] << 3)
                  + (numArray[20] << 4)
                  + (numArray[21] << 5)
                )

                if int(self.InputState[node_idx]) != int(num6):
                  prev_state = int(self.InputState[node_idx])

                  for i in range(1, 7):
                    prev_bit = 1 if (prev_state & (1 << (i - 1))) else 0
                    new_bit = int(numArray[15 + i])
                    logic = self.NodeInputConfig[node_idx][i].Logic
                    if (
                      prev_bit != new_bit
                      and (
                        logic == self.eInputLogic.EnableForwardOnly
                        or logic == self.eInputLogic.EnableReverseOnly
                      )
                      and new_bit == 1
                    ):
                      self.NextEncoderPosition[index1] = -2_000_000_000
                      self.MoveDoneResponse[node_idx] = True

                  self.InputState[node_idx] = num6
                  EventData.EventType = self.clsCANMotor.eEventType.DigitalInputChangedState
                  EventData.NodeID = index1
                  EventData.DICS_State = [False] * 7
                  EventData.DICS_State[1] = numArray[16] > 0
                  EventData.DICS_State[2] = numArray[17] > 0
                  EventData.DICS_State[3] = numArray[18] > 0
                  EventData.DICS_State[4] = numArray[19] > 0
                  EventData.DICS_State[5] = numArray[20] > 0
                  EventData.DICS_State[6] = numArray[21] > 0
                  self.RaiseAnEvent(EventData)

              index3 += 1

      elif num1 == 5:
        # Query response: 2-char type + 14-bit index + payload (int32 or float32)
        msg_type = chr(int(data[0])) + chr(int(data[1]))

        msg_index = ((int(data[3]) & 0x3F) << 8) | int(
          data[2]
        )  # last 6 bits of data[3], then data[2]
        is_float = (int(data[3]) & 0x80) != 0  # MSB

        if not is_float:
          raw_u32 = int.from_bytes(bytes(data[4:8]), byteorder="little", signed=False)
          # match VB behavior: if fits in signed int32, return that; else return raw unsigned
          value = int(raw_u32) if raw_u32 <= 0x7FFFFFFF else int(raw_u32)
        else:
          value = struct.unpack_from("<f", bytes(data), 4)[0]

        node_idx = self.GetNodeIDIndex(index1)
        for slot in range(0, 10):
          entry = _get_item_2d(self.QueryWaitBuffer, node_idx, slot)
          if (
            int(entry.NodeID) == int(index1)
            and int(entry.MsgIndex) == int(msg_index)
            and str(entry.MsgType) == msg_type
            and _future_is_unset(entry)
          ):
            _future_set_result(entry, value)

      elif num1 == 11:
        node_idx = self.GetNodeIDIndex(index1)

        # SDO abort
        if int(data[0]) == 0x80:
          abort_code = (
            int(data[4]) | (int(data[5]) << 8) | (int(data[6]) << 16) | (int(data[7]) << 24)
          )
          sdo_abort_message = self.GetSDOAbortMessage(int(abort_code))

          for slot in range(0, 10):
            entry = _get_item_2d(self.OSQueryWaitBuffer, node_idx, slot)
            if (
              int(entry.NodeID) == int(index1)
              and int(entry.ObjectByte0) == int(data[2])
              and int(entry.ObjectByte1) == int(data[1])
              and int(entry.SubIndex) == int(data[3])
              and _future_is_unset(entry)
            ):
              _future_set_exception(entry, CanError(sdo_abort_message))

        # SDO download initiate response (0x60)
        if int(data[0]) == 0x60:
          for slot in range(0, 10):
            entry = _get_item_2d(self.OSQueryWaitBuffer, node_idx, slot)
            if (
              int(entry.NodeID) == int(index1)
              and _future_is_unset(entry)
              and int(entry.ObjectByte0) == int(data[2])
              and int(entry.ObjectByte1) == int(data[1])
              and int(entry.SubIndex) == int(data[3])
              and str(entry.MsgType) == "SDODI"
            ):
              _future_set_result(entry, True)

        # "CHR" ack: (0x20 or 0x30) and bytes 1..3 are 0
        if (
          int(data[0]) in (0x20, 0x30)
          and int(data[1]) == 0
          and int(data[2]) == 0
          and int(data[3]) == 0
        ):
          for slot in range(0, 10):
            entry = _get_item_2d(self.OSQueryWaitBuffer, node_idx, slot)
            if (
              int(entry.NodeID) == int(index1)
              and str(entry.MsgType) == "CHR"
              and _future_is_unset(entry)
            ):
              _future_set_result(entry, True)

        # "STAT" response
        if int(data[0]) == 79 and int(data[1]) == 35 and int(data[2]) == 0x10 and int(data[3]) == 2:
          for slot in range(0, 10):
            entry = _get_item_2d(self.OSQueryWaitBuffer, node_idx, slot)
            if (
              int(entry.NodeID) == int(index1)
              and str(entry.MsgType) == "STAT"
              and _future_is_unset(entry)
            ):
              _future_set_result(entry, int(data[4]))

        # "VAL" response: bytes 4..LEN-1
        if int(data[0]) == 67 and int(data[1]) == 35 and int(data[2]) == 0x10 and int(data[3]) == 3:
          payload = [int(data[i]) for i in range(4, max(4, data_len))]
          for slot in range(0, 10):
            entry = _get_item_2d(self.OSQueryWaitBuffer, node_idx, slot)
            if (
              int(entry.NodeID) == int(index1)
              and str(entry.MsgType) == "VAL"
              and _future_is_unset(entry)
            ):
              _future_set_result(entry, payload)

        # SDO upload response (expedited-ish): bit 0x40 set
        if (int(data[0]) & 0x40) == 0x40:
          cmd = int(data[0])
          num9 = (cmd >> 2) & 0x03  # bits 3..2
          num10 = (cmd >> 1) & 0x01  # bit 1
          # num11 = cmd & 0x01        # bit 0 (unused here, but exists in VB)

          for slot in range(0, 10):
            entry = _get_item_2d(self.OSQueryWaitBuffer, node_idx, slot)
            if (
              int(entry.NodeID) == int(index1)
              and str(entry.MsgType) == "SDOUA"
              and int(entry.ObjectByte0) == int(data[2])
              and int(entry.ObjectByte1) == int(data[1])
              and int(entry.SubIndex) == int(data[3])
              and _future_is_unset(entry)
            ):
              nbytes = 4 - int(num9)

              if int(num10) == 0:
                # numeric (little-endian)
                val = 0
                for i in range(nbytes):
                  val |= int(data[4 + i]) << (8 * i)
                _future_set_result(entry, int(val))
              else:
                # text
                s = "".join(chr(int(data[4 + i])) for i in range(nbytes))
                _future_set_result(entry, s)

        # SDO segmented-ish response: none of bits 0x20,0x40,0x80 set
        if (int(data[0]) & 0x20) == 0 and (int(data[0]) & 0x40) == 0 and (int(data[0]) & 0x80) == 0:
          cmd = int(data[0])
          num17 = (cmd >> 4) & 0x01  # bit 4
          num18 = (cmd >> 1) & 0x07  # bits 3..1
          num19 = cmd & 0x01  # bit 0

          for slot in range(0, 10):
            entry = _get_item_2d(self.OSQueryWaitBuffer, node_idx, slot)
            if (
              int(entry.NodeID) == int(index1)
              and str(entry.MsgType) == "SDOSU"
              and _future_is_unset(entry)
            ):
              n = 7 - int(num18)
              s = "".join(chr(int(data[i])) for i in range(1, 1 + max(0, n)))
              # Return structured info instead of concatenated strings
              _future_set_result(entry, (int(num17), s, bool(num19)))

      elif num1 == 14:
        # Node guarding error
        # Preserve str2 accumulation behavior (binary-ish display); not used elsewhere.
        bits = [format(int(b), "b") for b in data]
        str2 = " " + " ".join(bits)

        if hasattr(self, "ErrCtrl") and self.ErrCtrl is not None:
          try:
            if len(self.ErrCtrl) > index1:
              self.ErrCtrl[index1].DataByte = data
          except Exception:
            pass

        if not self.Connecting:
          EventData.EventType = self.clsCANMotor.eEventType.CANEmergencyMessageReceived
          EventData.NodeID = index1
          EventData.CEMR_Description = "Node Guarding error. Motor drive restarted spontaneously."
          EventData.CEMR_DisableMotors = True
          self.RaiseAnEvent(EventData)

          _ = self.CAN_Write(self.clsCANMotor.eCOBType.NMT, 0, 1, index1)
          self.CAN_TPDO_Unmap(self.clsCANMotor.eTPDO.TPDO1, index1)
          _ = self.CAN_TPDO3_Map(index1)
          _ = self.CAN_TPDO4_Map(index1)

      else:
        # Default: accumulate debug bits (as in VB)
        bits = [format(int(b), "b") for b in data]
        str2 = " " + " ".join(bits)

    # VB loop condition: until (ErrCode & 0x20) == 0x20
    if (ErrCode & 0x20) == 0x20:
      break

  return 0
