BYTE_ORDER_INTEL = 0
BYTE_ORDER_MOTOROLA = 1
DATA_TYPE_UNSIGNED = 0
DATA_TYPE_SIGNED = 1
DATA_TYPE_FLOAT = 2
DATA_TYPE_DOUBLE = 3
DATA_TYPE_STRING = 7
DATA_TYPE_BYTEARRAY = 8
DATA_TYPE_UNSIGNED_INTEL = 13
DATA_TYPE_UNSIGNED_MOTOROLA = 9
DATA_TYPE_SIGNED_INTEL = 14
DATA_TYPE_SIGNED_MOTOROLA = 10
DATA_TYPE_FLOAT_INTEL = 15
DATA_TYPE_FLOAT_MOTOROLA = 11
DATA_TYPE_DOUBLE_INTEL = 16
DATA_TYPE_DOUBLE_MOTOROLA = 12
DATA_TYPE_TO_STRING = {
    DATA_TYPE_UNSIGNED: "UNSIGNED",
    DATA_TYPE_SIGNED: "SIGNED",
    DATA_TYPE_FLOAT: "FLOAT",
    DATA_TYPE_DOUBLE: "DOUBLE",
    DATA_TYPE_STRING: "STRING",
    DATA_TYPE_BYTEARRAY: "BYTEARRAY",
    DATA_TYPE_UNSIGNED_INTEL: "UNSIGNED_INTEL",
    DATA_TYPE_UNSIGNED_MOTOROLA: "UNSIGNED_MOTOROLA",
    DATA_TYPE_SIGNED_INTEL: "SIGNED_INTEL",
    DATA_TYPE_SIGNED_MOTOROLA: "SIGNED_MOTOROLA",
    DATA_TYPE_FLOAT_INTEL: "FLOAT_INTEL",
    DATA_TYPE_FLOAT_MOTOROLA: "FLOAT_MOTOROLA",
    DATA_TYPE_DOUBLE_INTEL: "DOUBLE_INTEL",
    DATA_TYPE_DOUBLE_MOTOROLA: "DOUBLE_MOTOROLA",
}
SIGNED_INT = {DATA_TYPE_SIGNED, DATA_TYPE_SIGNED_INTEL, DATA_TYPE_SIGNED_MOTOROLA}
FLOATS = {
    DATA_TYPE_FLOAT,
    DATA_TYPE_DOUBLE,
    DATA_TYPE_FLOAT_INTEL,
    DATA_TYPE_FLOAT_MOTOROLA,
    DATA_TYPE_DOUBLE_INTEL,
    DATA_TYPE_DOUBLE_MOTOROLA,
}
STANDARD_INT_SIZES = {8, 16, 32, 64}
SIGNAL_TYPE_SCALAR = 0
SIGNAL_TYPE_STRING = 1
SIGNAL_TYPE_CANOPEN = 2
SIGNAL_TYPE_STRUCTURE_COMPOSITION = 3
SIGNAL_TYPE_ARRAY = 4
INT_TYPES = {
    DATA_TYPE_UNSIGNED,
    DATA_TYPE_SIGNED,
    DATA_TYPE_UNSIGNED_INTEL,
    DATA_TYPE_UNSIGNED_MOTOROLA,
    DATA_TYPE_SIGNED_INTEL,
    DATA_TYPE_SIGNED_MOTOROLA,
}
CHANNEL_TYPE_VALUE = 0
CHANNEL_TYPE_MASTER = 1
CHANNEL_TYPE_TO_STRING = {CHANNEL_TYPE_VALUE: "VALUE", CHANNEL_TYPE_MASTER: "MASTER"}
CONVERSION_TYPE_NONE = 65535
CONVERSION_TYPE_LINEAR = 0
CONVERSION_TYPE_TABI = 1
CONVERSION_TYPE_TAB = 2
CONVERSION_TYPE_POLY = 6
CONVERSION_TYPE_EXPO = 7
CONVERSION_TYPE_LOGH = 8
CONVERSION_TYPE_RAT = 9
CONVERSION_TYPE_FORMULA = 10
CONVERSION_TYPE_TABX = 11
CONVERSION_TYPE_RTABX = 12
CONVERSION_TYPE_TO_STRING = {
    CONVERSION_TYPE_NONE: "NONE",
    CONVERSION_TYPE_LINEAR: "LINEAR",
    CONVERSION_TYPE_TABI: "TABI",
    CONVERSION_TYPE_TAB: "TAB",
    CONVERSION_TYPE_POLY: "POLY",
    CONVERSION_TYPE_EXPO: "EXPO",
    CONVERSION_TYPE_LOGH: "LOGH",
    CONVERSION_TYPE_RAT: "RAT",
    CONVERSION_TYPE_FORMULA: "FORMULA",
    CONVERSION_TYPE_TABX: "TABX",
    CONVERSION_TYPE_RTABX: "RTABX",
}
RAT_CONV_TEXT = "(P1 * X**2 + P2 * X + P3) / (P4 * X**2 + P5 * X + P6)"
POLY_CONV_SHORT_TEXT = "P4 * X / P1"
POLY_CONV_LONG_TEXT = "(P2 - (P4 * (X - P5 -P6))) / (P3* (X - P5 - P6) - P1)"
DEPENDENCY_TYPE_NONE = 0
DEPENDENCY_TYPE_VECTOR = 1
DEPENDENCY_TYPE_NDIM = 256
DEPENDENCY_TYPE_TO_STRING = {
    DEPENDENCY_TYPE_NONE: "NONE",
    DEPENDENCY_TYPE_VECTOR: "VECTOR",
    DEPENDENCY_TYPE_NDIM: "NDIM",
}
FLAG_PRECISION = 1
FLAG_PHY_RANGE_OK = 2
FLAG_VAL_RANGE_OK = 8
SOURCE_ECU = 2
SOURCE_VECTOR = 19
SOURCE_TYPE_TO_STRING = {SOURCE_ECU: "ECU", SOURCE_VECTOR: "VECTOR"}
BUS_TYPE_NONE = 0
BUS_TYPE_CAN = 2
BUS_TYPE_FLEXRAY = 5
BUS_TYPE_TO_STRING = {
    BUS_TYPE_NONE: "NONE",
    BUS_TYPE_CAN: "CAN",
    BUS_TYPE_FLEXRAY: "FLEXRAY",
}
SEEK_START = 0
SEEK_REL = 1
SEEK_END = 2
ID_BLOCK_SIZE = 64
HEADER_COMMON_SIZE = 164
HEADER_POST_320_EXTRA_SIZE = 44
CE_BLOCK_SIZE = 128
FH_BLOCK_SIZE = 56
DG_PRE_320_BLOCK_SIZE = 24
DG_POST_320_BLOCK_SIZE = 28
HD_BLOCK_SIZE = 104
CN_DISPLAYNAME_BLOCK_SIZE = 228
CN_SHORT_BLOCK_SIZE = 218
CN_LONGNAME_BLOCK_SIZE = 222
CG_PRE_330_BLOCK_SIZE = 26
CG_POST_330_BLOCK_SIZE = 30
DT_BLOCK_SIZE = 24
CC_COMMON_BLOCK_SIZE = 46
CC_COMMON_SHORT_SIZE = 42
CC_ALG_BLOCK_SIZE = 88
CC_LIN_BLOCK_SIZE = 62
CC_POLY_BLOCK_SIZE = 94
CC_EXPO_BLOCK_SIZE = 102
CC_FORMULA_BLOCK_SIZE = 304
SR_BLOCK_SIZE = 156
MAX_UINT8 = (1 << 8) - 1
MAX_UINT16 = (1 << 16) - 1
MAX_UINT32 = (1 << 32) - 1
MAX_UINT64 = (1 << 64) - 1
LOCATION_ORIGINAL_FILE = 0
LOCATION_TEMPORARY_FILE = 1
LOCATION_MEMORY = 2
ID_FMT = "<8s8s8s4H2s26s2H"
ID_KEYS = (
    "file_identification",
    "version_str",
    "program_identification",
    "byte_order",
    "float_format",
    "mdf_version",
    "code_page",
    "reserved0",
    "reserved1",
    "unfinalized_standard_flags",
    "unfinalized_custom_flags",
)
HEADER_COMMON_FMT = "<2sH3IH10s8s32s32s32s32s"
HEADER_COMMON_KEYS = (
    "id",
    "block_len",
    "first_dg_addr",
    "comment_addr",
    "program_addr",
    "dg_nr",
    "date",
    "time",
    "author_field",
    "department_field",
    "project_field",
    "subject_field",
)
FMT_COMMON = "<2sH"
COMMON_SIZE = 4
HEADER_POST_320_EXTRA_FMT = "Q2H32s"
HEADER_POST_320_EXTRA_KEYS = (
    "abs_time",
    "tz_offset",
    "time_quality",
    "timer_identification",
)
FMT_CHANNEL_FILTER = "<2sHI16xH32s"
CHANNEL_FILTER_SIZE = 58
FMT_CHANNEL_DISPLAYNAME = "<2sH5IH32s128s4H3d2IH"
KEYS_CHANNEL_DISPLAYNAME = (
    "id",
    "block_len",
    "next_ch_addr",
    "conversion_addr",
    "source_addr",
    "ch_depend_addr",
    "comment_addr",
    "channel_type",
    "short_name",
    "description",
    "start_offset",
    "bit_count",
    "data_type",
    "range_flag",
    "min_raw_value",
    "max_raw_value",
    "sampling_rate",
    "long_name_addr",
    "display_name_addr",
    "additional_byte_offset",
)
FMT_CHANNEL_SHORT = "<2sH5IH32s128s4H3d"
FMT_CHANNEL_LONGNAME = FMT_CHANNEL_SHORT + "I"
KEYS_CHANNEL_SHORT = (
    "id",
    "block_len",
    "next_ch_addr",
    "conversion_addr",
    "source_addr",
    "ch_depend_addr",
    "comment_addr",
    "channel_type",
    "short_name",
    "description",
    "start_offset",
    "bit_count",
    "data_type",
    "range_flag",
    "min_raw_value",
    "max_raw_value",
    "sampling_rate",
)
KEYS_CHANNEL_LONGNAME = KEYS_CHANNEL_SHORT + ("long_name_addr",)
FMT_CHANNEL_GROUP = "<2sH3I3HI"
KEYS_CHANNEL_GROUP = (
    "id",
    "block_len",
    "next_cg_addr",
    "first_ch_addr",
    "comment_addr",
    "record_id",
    "ch_nr",
    "samples_byte_nr",
    "cycles_nr",
)
FMT_DATA_GROUP_POST_320 = "<2sH4I2H4s"
KEYS_DATA_GROUP_POST_320 = (
    "id",
    "block_len",
    "next_dg_addr",
    "first_cg_addr",
    "trigger_addr",
    "data_block_addr",
    "cg_nr",
    "record_id_len",
    "reserved0",
)
FMT_DATA_GROUP_PRE_320 = "<2sH4I2H"
KEYS_DATA_GROUP_PRE_320 = (
    "id",
    "block_len",
    "next_dg_addr",
    "first_cg_addr",
    "trigger_addr",
    "data_block_addr",
    "cg_nr",
    "record_id_len",
)
FMT_SOURCE_COMMON = "<2s2H"
FMT_SOURCE_ECU = "<2s3HI80s32s4s"
FMT_SOURCE_EXTRA_ECU = "<HI80s32s4s"
KEYS_SOURCE_ECU = (
    "id",
    "block_len",
    "type",
    "module_nr",
    "module_address",
    "description",
    "ECU_identification",
    "reserved0",
)
FMT_SOURCE_VECTOR = "<2s2H2I36s36s42s"
FMT_SOURCE_EXTRA_VECTOR = "<2I36s36s42s"
KEYS_SOURCE_VECTOR = (
    "id",
    "block_len",
    "type",
    "CAN_id",
    "CAN_ch_index",
    "message_name",
    "sender_name",
    "reserved0",
)
KEYS_TEXT_BLOCK = ("id", "block_len", "text")
FMT_CONVERSION_COMMON = FMT_CONVERSION_NONE = "<2s2H2d20s2H"
FMT_CONVERSION_COMMON_SHORT = "<H2d20s2H"
KEYS_CONVERSION_NONE = (
    "id",
    "block_len",
    "range_flag",
    "min_phy_value",
    "max_phy_value",
    "unit_field",
    "conversion_type",
    "ref_param_nr",
)
FMT_CONVERSION_FORMULA = "<2s2H2d20s2H{}s"
KEYS_CONVERSION_FORMULA = (
    "id",
    "block_len",
    "range_flag",
    "min_phy_value",
    "max_phy_value",
    "unit_field",
    "conversion_type",
    "ref_param_nr",
    "formula_field",
)
FMT_CONVERSION_LINEAR = "<2s2H2d20s2H2d"
KEYS_CONVERSION_LINEAR = (
    "id",
    "block_len",
    "range_flag",
    "min_phy_value",
    "max_phy_value",
    "unit_field",
    "conversion_type",
    "ref_param_nr",
    "b",
    "a",
)
FMT_CONVERSION_POLY_RAT = "<2s2H2d20s2H6d"
KEYS_CONVERSION_POLY_RAT = (
    "id",
    "block_len",
    "range_flag",
    "min_phy_value",
    "max_phy_value",
    "unit_field",
    "conversion_type",
    "ref_param_nr",
    "P1",
    "P2",
    "P3",
    "P4",
    "P5",
    "P6",
)
FMT_CONVERSION_EXPO_LOGH = "<2s2H2d20s2H7d"
KEYS_CONVERSION_EXPO_LOGH = (
    "id",
    "block_len",
    "range_flag",
    "min_phy_value",
    "max_phy_value",
    "unit_field",
    "conversion_type",
    "ref_param_nr",
    "P1",
    "P2",
    "P3",
    "P4",
    "P5",
    "P6",
    "P7",
)
FMT_PROGRAM_BLOCK = "<2sH{}s"
KEYS_PROGRAM_BLOCK = ("id", "block_len", "data")
FMT_SAMPLE_REDUCTION_BLOCK = "<2sH3Id"
KEYS_SAMPLE_REDUCTION_BLOCK = (
    "id",
    "block_len",
    "next_sr_addr",
    "data_block_addr",
    "cycles_nr",
    "time_interval",
) MAX_UINT64 = (1 << 64) - 1
DATA_TYPE_UNSIGNED_INTEL = 0
DATA_TYPE_UNSIGNED_MOTOROLA = 1
DATA_TYPE_SIGNED_INTEL = 2
DATA_TYPE_SIGNED_MOTOROLA = 3
DATA_TYPE_REAL_INTEL = 4
DATA_TYPE_REAL_MOTOROLA = 5
DATA_TYPE_STRING_LATIN_1 = 6
DATA_TYPE_STRING_UTF_8 = 7
DATA_TYPE_STRING_UTF_16_LE = 8
DATA_TYPE_STRING_UTF_16_BE = 9
DATA_TYPE_BYTEARRAY = 10
DATA_TYPE_MIME_SAMPLE = 11
DATA_TYPE_MIME_STREAM = 12
DATA_TYPE_CANOPEN_DATE = 13
DATA_TYPE_CANOPEN_TIME = 14
DATA_TYPE_COMPLEX_INTEL = 15
DATA_TYPE_COMPLEX_MOTOROLA = 16
DATA_TYPE_TO_STRING = dict(
    enumerate(
        [
            "UNSIGNED_INTEL",
            "UNSIGNED_MOTOROLA",
            "SIGNED_INTEL",
            "SIGNED_MOTOROLA",
            "REAL_INTEL",
            "REAL_MOTOROLA",
            "STRING_LATIN_1",
            "STRING_UTF_8",
            "STRING_UTF_16_LE",
            "STRING_UTF_16_BE",
            "BYTEARRAY",
            "MIME_SAMPLE",
            "MIME_STREAM",
            "CANOPEN_DATE",
            "CANOPEN_TIME",
            "COMPLEX_INTEL",
            "COMPLEX_MOTOROLA",
        ]
    )
)
NON_SCALAR_TYPES = {
    DATA_TYPE_BYTEARRAY,
    DATA_TYPE_MIME_SAMPLE,
    DATA_TYPE_MIME_STREAM,
    DATA_TYPE_STRING_UTF_8,
    DATA_TYPE_STRING_LATIN_1,
    DATA_TYPE_STRING_UTF_16_BE,
    DATA_TYPE_STRING_UTF_16_LE,
    DATA_TYPE_CANOPEN_DATE,
    DATA_TYPE_CANOPEN_TIME,
}
STRING_TYPES = {
    DATA_TYPE_STRING_UTF_8,
    DATA_TYPE_STRING_LATIN_1,
    DATA_TYPE_STRING_UTF_16_BE,
    DATA_TYPE_STRING_UTF_16_LE,
}
SIGNAL_TYPE_SCALAR = 0
SIGNAL_TYPE_STRING = 1
SIGNAL_TYPE_CANOPEN = 2
SIGNAL_TYPE_STRUCTURE_COMPOSITION = 3
SIGNAL_TYPE_ARRAY = 4
SIGNAL_TYPE_BYTEARRAY = 5
SIGNED_INT = {DATA_TYPE_SIGNED_INTEL, DATA_TYPE_SIGNED_MOTOROLA}
FLOATS = {DATA_TYPE_REAL_INTEL, DATA_TYPE_REAL_MOTOROLA}
STANDARD_INT_SIZES = {8, 16, 32, 64}
INT_TYPES = {
    DATA_TYPE_UNSIGNED_INTEL,
    DATA_TYPE_UNSIGNED_MOTOROLA,
    DATA_TYPE_SIGNED_INTEL,
    DATA_TYPE_SIGNED_MOTOROLA,
}
CHANNEL_TYPE_VALUE = 0
CHANNEL_TYPE_VLSD = 1
CHANNEL_TYPE_MASTER = 2
CHANNEL_TYPE_VIRTUAL_MASTER = 3
CHANNEL_TYPE_SYNC = 4
CHANNEL_TYPE_MLSD = 5
CHANNEL_TYPE_VIRTUAL = 6
SYNC_TYPE_NONE = 0
SYNC_TYPE_TIME = 1
SYNC_TYPE_ANGLE = 2
SYNC_TYPE_DISTANCE = 3
SYNC_TYPE_INDEX = 4
SYNC_TYPE_TO_STRING = dict(enumerate(["NONE", "TIME", "ANGLE", "DISTANCE", "INDEX"]))
CHANNEL_TYPE_TO_STRING = CHANNEL_TYPE_TO_DESCRIPTION = {
    CHANNEL_TYPE_VALUE: "VALUE",
    CHANNEL_TYPE_VLSD: "VLSD",
    CHANNEL_TYPE_MASTER: "MASTER",
    CHANNEL_TYPE_VIRTUAL_MASTER: "VIRTUAL MASTER",
    CHANNEL_TYPE_SYNC: "SYNC",
    CHANNEL_TYPE_MLSD: "MLSD",
    CHANNEL_TYPE_VIRTUAL: "VIRTUAL",
}
MASTER_TYPES = {CHANNEL_TYPE_MASTER, CHANNEL_TYPE_VIRTUAL_MASTER}
VIRTUAL_TYPES = {CHANNEL_TYPE_VIRTUAL, CHANNEL_TYPE_VIRTUAL_MASTER}
CONVERSION_TYPE_NON = 0
CONVERSION_TYPE_LIN = 1
CONVERSION_TYPE_RAT = 2
CONVERSION_TYPE_ALG = 3
CONVERSION_TYPE_TABI = 4
CONVERSION_TYPE_TAB = 5
CONVERSION_TYPE_RTAB = 6
CONVERSION_TYPE_TABX = 7
CONVERSION_TYPE_RTABX = 8
CONVERSION_TYPE_TTAB = 9
CONVERSION_TYPE_TRANS = 10
CONVERSION_TYPE_BITFIELD = 11
CONVERSION_TYPE_TO_STRING = dict(
    enumerate(
        [
            "NON",
            "LIN",
            "RAT",
            "ALG",
            "TABI",
            "TAB",
            "RTAB",
            "TABX",
            "RTABX",
            "TTAB",
            "TRANS",
            "BITFIELD",
        ]
    )
)
CONV_RAT_TEXT = "(P1 * X**2 + P2 * X + P3) / (P4 * X**2 + P5 * X + P6)"
TABULAR_CONVERSIONS = {
    CONVERSION_TYPE_TABX,
    CONVERSION_TYPE_RTABX,
    CONVERSION_TYPE_TTAB,
    CONVERSION_TYPE_BITFIELD,
}
CONVERSIONS_WITH_TEXTS = {
    CONVERSION_TYPE_ALG,
    CONVERSION_TYPE_RTABX,
    CONVERSION_TYPE_TABX,
    CONVERSION_TYPE_TRANS,
    CONVERSION_TYPE_TTAB,
    CONVERSION_TYPE_BITFIELD,
}
CONVERSION_GROUP_1 = {CONVERSION_TYPE_NON, CONVERSION_TYPE_TRANS, CONVERSION_TYPE_TTAB}
CONVERSION_GROUP_2 = {
    CONVERSION_TYPE_LIN,
    CONVERSION_TYPE_RAT,
    CONVERSION_TYPE_ALG,
    CONVERSION_TYPE_TABI,
    CONVERSION_TYPE_TAB,
    CONVERSION_TYPE_RTAB,
}
CA_TYPE_ARRAY = 0
CA_TYPE_SCALE_AXIS = 1
CA_TYPE_LOOKUP = 2
CA_STORAGE_TYPE_CN_TEMPLATE = 0
CA_STORAGE_TYPE_CG_TEMPLATE = 1
CA_STORAGE_TYPE_DG_TEMPLATE = 2
SOURCE_OTHER = 0
SOURCE_ECU = 1
SOURCE_BUS = 2
SOURCE_IO = 3
SOURCE_TOOL = 4
SOURCE_USER = 5
SOURCE_TYPE_TO_STRING = {
    SOURCE_OTHER: "OTHER",
    SOURCE_ECU: "ECU",
    SOURCE_BUS: "BUS",
    SOURCE_IO: "IO",
    SOURCE_TOOL: "TOOL",
    SOURCE_USER: "USER",
}
BUS_TYPE_NONE = 0
BUS_TYPE_OTHER = 1
BUS_TYPE_CAN = 2
BUS_TYPE_LIN = 3
BUS_TYPE_MOST = 4
BUS_TYPE_FLEXRAY = 5
BUS_TYPE_K_LINE = 6
BUS_TYPE_ETHERNET = 7
BUS_TYPE_USB = 8
BUS_TYPE_TO_STRING = {
    BUS_TYPE_NONE: "NONE",
    BUS_TYPE_OTHER: "OTHER",
    BUS_TYPE_CAN: "CAN",
    BUS_TYPE_LIN: "LIN",
    BUS_TYPE_MOST: "MOST",
    BUS_TYPE_FLEXRAY: "FLEXRAY",
    BUS_TYPE_K_LINE: "K_LINE",
    BUS_TYPE_ETHERNET: "ETHERNET",
    BUS_TYPE_USB: "USB",
}
EVENT_TYPE_RECORDING = 0
EVENT_TYPE_RECORDING_INTERRUPT = 1
EVENT_TYPE_ACQUISITION_INTERRUPT = 2
EVENT_TYPE_START_RECORDING_TRIGGER = 3
EVENT_TYPE_STOP_RECORDING_TRIGGER = 4
EVENT_TYPE_TRIGGER = 5
EVENT_TYPE_MARKER = 6
EVENT_TYPE_TO_STRING = {
    EVENT_TYPE_RECORDING: "Recording",
    EVENT_TYPE_RECORDING_INTERRUPT: "REcording interrupt",
    EVENT_TYPE_ACQUISITION_INTERRUPT: "Acquisition interrupt",
    EVENT_TYPE_START_RECORDING_TRIGGER: "Start recording trigger",
    EVENT_TYPE_STOP_RECORDING_TRIGGER: "Stop recording trigger",
    EVENT_TYPE_TRIGGER: "Trigger",
    EVENT_TYPE_MARKER: "Marker",
}
EVENT_SYNC_TYPE_S = 1
EVENT_SYNC_TYPE_RAD = 2
EVENT_SYNC_TYPE_M = 3
EVENT_SYNC_TYPE_INDEX = 4
EVENT_RANGE_TYPE_POINT = 0
EVENT_RANGE_TYPE_BEGINNING = 1
EVENT_RANGE_TYPE_END = 2
EVENT_CAUSE_OTHER = 0
EVENT_CAUSE_ERROR = 1
EVENT_CAUSE_TOOL = 2
EVENT_CAUSE_SCRIPT = 3
EVENT_CAUSE_USER = 4
SEEK_START = 0
SEEK_REL = 1
SEEK_END = 2
TIME_CH_SIZE = 8
SI_BLOCK_SIZE = 56
FH_BLOCK_SIZE = 56
DG_BLOCK_SIZE = 64
HD_BLOCK_SIZE = 104
CN_BLOCK_SIZE = 160
CN_SINGLE_ATTACHMENT_BLOCK_SIZE = 168
CG_BLOCK_SIZE = 104
CG_RM_BLOCK_SIZE = 112
COMMON_SIZE = 24
COMMON_SHORT_SIZE = 16
CC_NONE_BLOCK_SIZE = 80
CC_ALG_BLOCK_SIZE = 88
CC_LIN_BLOCK_SIZE = 96
AT_COMMON_SIZE = 96
DZ_COMMON_SIZE = 48
DZ_COMMON_INFO_SIZE = 22
CC_COMMON_BLOCK_SIZE = 80
HL_BLOCK_SIZE = 40
IDENTIFICATION_BLOCK_SIZE = 64
HEADER_BLOCK_SIZE = 104
SR_BLOCK_SIZE = 64
FLAG_ALL_SAMPLES_VALID = 1
FLAG_INVALIDATION_BIT_VALID = 2
FLAG_PRECISION = 1
FLAG_PHY_RANGE_OK = 1 << 4
FLAG_VAL_RANGE_OK = 1 << 3
FLAG_AT_EMBEDDED = 1
FLAG_AT_COMPRESSED_EMBEDDED = 2
FLAG_AT_MD5_VALID = 4
FLAG_DZ_DEFLATE = 0
FLAG_DZ_TRANPOSED_DEFLATE = 1
FLAG_CA_DYNAMIC_AXIS = 1
FLAG_CA_INPUT_QUANTITY = 1 << 1
FLAG_CA_OUTPUT_QUANTITY = 1 << 2
FLAG_CA_COMPARISON_QUANTITY = 1 << 3
FLAG_CA_AXIS = 1 << 4
FLAG_CA_FIXED_AXIS = 1 << 5
FLAG_CA_INVERSE_LAYOUT = 1 << 6
FLAG_CA_LEFT_OPENED_INTERVAL = 1 << 7
FLAG_CA_STANDARD_AXIS = 1 << 8
FLAG_DL_EQUAL_LENGHT = 1
FLAG_DL_TIME_VALUES = 1 << 1
FLAG_DL_ANGLE_VALUES = 1 << 2
FLAG_DL_DISTANCE_VALUES = 1 << 3
FLAG_LD_EQUAL_LENGHT = 1
FLAG_LD_TIME_VALUES = 1 << 1
FLAG_LD_ANGLE_VALUES = 1 << 2
FLAG_LD_DISTANCE_VALUES = 1 << 3
FLAG_LD_INVALIDATION_PRESENT = 1 << 31
FLAG_EV_POST_PROCESSING = 1
FLAG_EV_GROUP_NAME = 1 << 1
FLAG_CG_VLSD = 1
FLAG_CG_BUS_EVENT = 1 << 1
FLAG_CG_PLAIN_BUS_EVENT = 1 << 2
FLAG_CG_REMOTE_MASTER = 1 << 3
FLAG_CG_EVENT_GROUP = 1 << 4
FLAG_UNFIN_UPDATE_CG_COUNTER = 0x1
FLAG_UNFIN_UPDATE_SR_COUNTER = 0x2
FLAG_UNFIN_UPDATE_LAST_DT_LENGTH = 0x4
FLAG_UNFIN_UPDATE_LAST_RD_LENGTH = 0x8
FLAG_UNFIN_UPDATE_LAST_DL = 0x10
FLAG_UNFIN_UPDATE_VLSD_BYTES = 0x20
FLAG_UNFIN_UPDATE_VLSD_OFFSETS = 0x40
FLAG_CG_TO_STRING = {
    FLAG_CG_VLSD: "VLSD",
    FLAG_CG_BUS_EVENT: "BUS_EVENT",
    FLAG_CG_PLAIN_BUS_EVENT: "PLAIN_BUS_EVENT",
    FLAG_CG_REMOTE_MASTER: "REMOTE_MASTER",
    FLAG_CG_EVENT_GROUP: "EVENT_GROUP",
}
FLAG_AT_TO_STRING = {
    FLAG_AT_EMBEDDED: "Embedded",
    FLAG_AT_COMPRESSED_EMBEDDED: "Compressed",
    FLAG_AT_MD5_VALID: "MD5 sum valid",
}
FLAG_CN_ALL_INVALID = 1
FLAG_CN_INVALIDATION_PRESENT = 1 << 1
FLAG_CN_PRECISION = 1 << 2
FLAG_CN_VALUE_RANGE = 1 << 3
FLAG_CN_LIMIT_RANGE = 1 << 4
FLAG_CN_EXTENDED_LIMIT_RANGE = 1 << 5
FLAG_CN_DISCRETE = 1 << 6
FLAG_CN_CALIBRATION = 1 << 7
FLAG_CN_CALCULATED = 1 << 8
FLAG_CN_VIRTUAL = 1 << 9
FLAG_CN_BUS_EVENT = 1 << 10
FLAG_CN_MONOTONOUS = 1 << 11
FLAG_CN_DEFAULT_X = 1 << 12
FLAG_CN_EVENT_SIGNAL = 1 << 13
FLAG_CN_VLSD = 1 << 14
FLAG_CN_TO_STRING = {
    FLAG_CN_ALL_INVALID: "ALL_INVALID",
    FLAG_CN_INVALIDATION_PRESENT: "INVALIDATION_PRESENT",
    FLAG_CN_PRECISION: "PRECISION",
    FLAG_CN_VALUE_RANGE: "VALUE_RANGE",
    FLAG_CN_LIMIT_RANGE: "LIMIT_RANGE",
    FLAG_CN_EXTENDED_LIMIT_RANGE: "EXTENDED_LIMIT_RANGE",
    FLAG_CN_DISCRETE: "DISCRETE",
    FLAG_CN_CALIBRATION: "CALIBRATION",
    FLAG_CN_CALCULATED: "CALCULATED",
    FLAG_CN_VIRTUAL: "VIRTUAL",
    FLAG_CN_BUS_EVENT: "BUS_EVENT",
    FLAG_CN_MONOTONOUS: "MONOTONOUS",
    FLAG_CN_DEFAULT_X: "DEFAULT_X",
    FLAG_CN_EVENT_SIGNAL: "EVENT_SIGNAL",
    FLAG_CN_VLSD: "VLSD",
}
FLAG_CC_PRECISION = 1
FLAG_CC_RANGE = 1 << 1
FLAG_HD_LOCAL_TIME = 1
FLAG_HD_TIME_OFFSET_VALID = 2
LOCATION_ORIGINAL_FILE = 0
LOCATION_TEMPORARY_FILE = 1
LOCATION_MEMORY = 2
DT_BLOCK = 0
DZ_BLOCK_DEFLATE = 1
DZ_BLOCK_TRANSPOSED = 2
DZ_BLOCK_LZ = 3
FMT_CHANNEL = "<4sI2Q{}Q4B4I2BH6d"
FMT_CHANNEL_PARAMS = "<4B4I2BH6d"
FMT_CHANNEL_FILTER = "<4s12x4Q32xQ"
CHANNEL_FILTER_SIZE = 88
FMT_SIMPLE_CHANNEL = "<4sI10Q4B4I2BH6d"
KEYS_SIMPLE_CHANNEL = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "next_ch_addr",
    "component_addr",
    "name_addr",
    "source_addr",
    "conversion_addr",
    "data_block_addr",
    "unit_addr",
    "comment_addr",
    "channel_type",
    "sync_type",
    "data_type",
    "bit_offset",
    "byte_offset",
    "bit_count",
    "flags",
    "pos_invalidation_bit",
    "precision",
    "reserved1",
    "attachment_nr",
    "min_raw_value",
    "max_raw_value",
    "lower_limit",
    "upper_limit",
    "lower_ext_limit",
    "upper_ext_limit",
)
FMT_SIMPLE_CHANNEL_PARAMS = "<8Q4B4I2BH6d"
SIMPLE_CHANNEL_PACK = struct.Struct(FMT_SIMPLE_CHANNEL).pack
FMT_SINGLE_ATTACHMENT_CHANNEL = "<4sI11Q4B4I2BH6d"
SINGLE_ATTACHMENT_CHANNEL_PACK = struct.Struct(FMT_SINGLE_ATTACHMENT_CHANNEL).pack
FMT_SINGLE_ATTACHMENT_CHANNEL_PARAMS = "<9Q4B4I2BH6d"
FMT_TEXT_BLOCK = "<4sIQQ{}s"
KEYS_TEXT_BLOCK = ("id", "reserved0", "block_len", "links_nr", "text")
FMT_SOURCE_INFORMATION = "<4sI5Q3B5s"
KEYS_SOURCE_INFORMATION = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "name_addr",
    "path_addr",
    "comment_addr",
    "source_type",
    "bus_type",
    "flags",
    "reserved1",
)
SOURCE_INFORMATION_PACK = struct.Struct(FMT_SOURCE_INFORMATION).pack
FMT_CHANNEL_GROUP = "<4sI10Q2H3I"
KEYS_CHANNEL_GROUP = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "next_cg_addr",
    "first_ch_addr",
    "acq_name_addr",
    "acq_source_addr",
    "first_sample_reduction_addr",
    "comment_addr",
    "record_id",
    "cycles_nr",
    "flags",
    "path_separator",
    "reserved1",
    "samples_byte_nr",
    "invalidation_bytes_nr",
)
FMT_CHANNEL_GROUP_SHORT = "<8Q2H3I"
KEYS_CHANNEL_GROUP_SHORT = (
    "next_cg_addr",
    "first_ch_addr",
    "acq_name_addr",
    "acq_source_addr",
    "first_sample_reduction_addr",
    "comment_addr",
    "record_id",
    "cycles_nr",
    "flags",
    "path_separator",
    "reserved1",
    "samples_byte_nr",
    "invalidation_bytes_nr",
)
FMT_CHANNEL_GROUP_RM = "<4sI11Q2H3I"
KEYS_CHANNEL_GROUP_RM = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "next_cg_addr",
    "first_ch_addr",
    "acq_name_addr",
    "acq_source_addr",
    "first_sample_reduction_addr",
    "comment_addr",
    "cg_master_addr",
    "record_id",
    "cycles_nr",
    "flags",
    "path_separator",
    "reserved1",
    "samples_byte_nr",
    "invalidation_bytes_nr",
)
FMT_CHANNEL_GROUP_RM_SHORT = "<9Q2H3I"
KEYS_CHANNEL_GROUP_RM_SHORT = (
    "next_cg_addr",
    "first_ch_addr",
    "acq_name_addr",
    "acq_source_addr",
    "first_sample_reduction_addr",
    "comment_addr",
    "cg_master_addr",
    "record_id",
    "cycles_nr",
    "flags",
    "path_separator",
    "reserved1",
    "samples_byte_nr",
    "invalidation_bytes_nr",
)
FMT_DATA_BLOCK = "<4sI2Q{}s"
KEYS_DATA_BLOCK = ("id", "reserved0", "block_len", "links_nr", "data")
FMT_COMMON = "<4sI2Q"
FMT_COMMON_SHORT = "<4s4xQ"
FMT_FILE_HISTORY = "<4sI5Q2hB3s"
KEYS_FILE_HISTORY = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "next_fh_addr",
    "comment_addr",
    "abs_time",
    "tz_offset",
    "daylight_save_time",
    "time_flags",
    "reserved1",
)
FMT_DATA_GROUP = "<4sI6QB7s"
KEYS_DATA_GROUP = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "next_dg_addr",
    "first_cg_addr",
    "data_block_addr",
    "comment_addr",
    "record_id_len",
    "reserved1",
)
FMT_DATA_LIST = "<4sI2Q{}QB3sIQ"
FMT_CONVERSION_NONE = "<4sI6Q2B3H2d"
KEYS_CONVERSION_NONE = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "name_addr",
    "unit_addr",
    "comment_addr",
    "inv_conv_addr",
    "conversion_type",
    "precision",
    "flags",
    "ref_param_nr",
    "val_param_nr",
    "min_phy_value",
    "max_phy_value",
)
CONVERSION_NONE_PACK = struct.Struct(FMT_CONVERSION_NONE).pack
FMT_CONVERSION_NONE_INIT = "<4Q2B3H2d"
FMT_CONVERSION_LINEAR = FMT_CONVERSION_NONE + "2d"
CONVERSION_LINEAR_PACK = struct.Struct(FMT_CONVERSION_LINEAR).pack
KEYS_CONVERSION_LINEAR = KEYS_CONVERSION_NONE + ("b", "a")
FMT_CONVERSION_LINEAR_INIT = "<4Q2B3H4d"
FMT_CONVERSION_ALGEBRAIC = "<4sI7Q2B3H2d"
CONVERSION_ALGEBRAIC_PACK = struct.Struct(FMT_CONVERSION_ALGEBRAIC).pack
KEYS_CONVERSION_ALGEBRAIC = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "name_addr",
    "unit_addr",
    "comment_addr",
    "inv_conv_addr",
    "formula_addr",
    "conversion_type",
    "precision",
    "flags",
    "ref_param_nr",
    "val_param_nr",
    "min_phy_value",
    "max_phy_value",
)
FMT_CONVERSION_ALGEBRAIC_INIT = "<5Q2B3H2d"
FMT_CONVERSION_RAT = FMT_CONVERSION_NONE + "6d"
CONVERSION_RAT_PACK = struct.Struct(FMT_CONVERSION_RAT).pack
KEYS_CONVERSION_RAT = KEYS_CONVERSION_NONE + ("P1", "P2", "P3", "P4", "P5", "P6")
FMT_CONVERSION_RAT_INIT = "<4Q2B3H8d"
FMT_CONVERSION_RAT_INIT = "<4Q2B3H8d"
FMT_HEADER_BLOCK = "<4sI9Q2h4B2Q"
FMT_IDENTIFICATION_BLOCK = "<8s8s8s4sH30s2H"
KEYS_HEADER_BLOCK = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "first_dg_addr",
    "file_history_addr",
    "channel_tree_addr",
    "first_attachment_addr",
    "first_event_addr",
    "comment_addr",
    "abs_time",
    "tz_offset",
    "daylight_save_time",
    "time_flags",
    "time_quality",
    "flags",
    "reserved1",
    "start_angle",
    "start_distance",
)
KEYS_IDENTIFICATION_BLOCK = (
    "file_identification",
    "version_str",
    "program_identification",
    "reserved0",
    "mdf_version",
    "reserved1",
    "unfinalized_standard_flags",
    "unfinalized_custom_flags",
)
FMT_AT_COMMON = "<4sI6Q2HI16s2Q"
KEYS_AT_BLOCK = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "next_at_addr",
    "file_name_addr",
    "mime_addr",
    "comment_addr",
    "flags",
    "creator_index",
    "reserved1",
    "md5_sum",
    "original_size",
    "embedded_size",
    "embedded_data",
)
FMT_DZ_COMMON = "<4sI2Q2s2BI2Q"
KEYS_DZ_BLOCK = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "original_type",
    "zip_type",
    "reserved1",
    "param",
    "original_size",
    "zip_size",
    "data",
)
FMT_DZ_INFO_COMMON = "<B1xI2Q"
DZ_INFO_COMMON_OFFSET = 26
FMT_HL_BLOCK = "<4sI3QHB5s"
KEYS_HL_BLOCK = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "first_dl_addr",
    "flags",
    "zip_type",
    "reserved1",
)
FMT_EVENT_PARAMS = "<5B3sI2Hqd"
FMT_EVENT = "<4sI2Q{}Q5B3sI2Hqd"
FMT_SR_BLOCK = "<4sI5Qd2B6s"
KEYS_SR_BLOCK = (
    "id",
    "reserved0",
    "block_len",
    "links_nr",
    "next_sr_addr",
    "data_block_addr",
    "cycles_nr",
    "interval",
    "sync_type",
    "flags",
    "reserved1",
)
ASAM_XML_NAMESPACE = "{http://www.asam.net/mdf/v4}"
CAN_ID_PATTERN = re.compile(r"(?i:can)(?P<id>\d+)")
CAN_DATA_FRAME_PATTERN = re.compile(r"(?i:CAN_DataFrame)_(?P<id>\d+)")
LIN_DATA_FRAME_PATTERN = re.compile(r"(?i:LIN_Frame)_(?P<id>\d+)")
CN_COMMENT_TEMPLATE = """<CNcomment>
<TX>{}</TX>
<names>
{}
</names>
</CNcomment>"""
HD_COMMENT_TEMPLATE = """<HDcomment>
<TX>{}</TX>
<common_properties>
    <e name="author">{}</e>
    <e name="department">{}</e>
    <e name="project">{}</e>
    <e name="subject">{}</e>
</common_properties>
</HDcomment>"""
CANOPEN_TIME_FIELDS = ("ms", "days")
CANOPEN_DATE_FIELDS = (
    "ms",
    "min",
    "hour",
    "day",
    "month",
    "year",
    "summer_time",
    "day_of_week",
)
CAN_ERROR_TYPES = {
    0: "Other/unknown error ",
    1: "Bit Error",
    2: "Form Error",
    3: "Bit Stuffing Error",
    4: "CRC Error",
    5: "Acknowledgment Error",
}
 def extract_mux(
    payload: NDArray[Any],
    message: Frame,
    message_id: int,
    bus: int,
    t: NDArray[Any],
    muxer: str | None = None,
    muxer_values: NDArray[Any] | None = None,
    original_message_id: int | None = None,
    raw: bool = False,
    include_message_name: bool = False,
    ignore_value2text_conversion: bool = True,
    is_j1939: bool = False,
) -> dict[tuple[Any, ...], dict[str, ExtractedSignal]]:
    """extract multiplexed CAN signals from the raw payload

    Parameters
    ----------
    payload : np.ndarray
        raw CAN payload as numpy array
    message : canmatrix.Frame
        CAN message description parsed by canmatrix
    message_id : int
        message id
    original_message_id : int
        original message id
    bus : int
        bus channel number
    t : np.ndarray
        timestamps for the raw payload
    muxer (None): str
        name of the parent multiplexor signal
    muxer_values (None): np.ndarray
        multiplexor signal values
    ignore_value2text_conversion (True): bool
        ignore value to text conversions

        .. versionadded:: 5.23.0


    Returns
    -------
    extracted_signal : dict
        each value in the dict is a list of signals that share the same
        multiplexors

    """

    if muxer is None:
        if message.is_multiplexed:
            for sig in message:
                if sig.multiplex == "Multiplexor" and sig.muxer_for_signal is None:
                    multiplexor_name = sig.name
                    break
            for sig in message:
                if (
                    sig.multiplex not in (None, "Multiplexor")
                    and sig.muxer_for_signal is None
                ):
                    sig.muxer_for_signal = multiplexor_name
                    sig.mux_val_min = sig.mux_val_max = int(sig.multiplex)
                    sig.mux_val_grp.insert(0, (int(sig.multiplex), int(sig.multiplex)))

    extracted_signals = {}

    if message.size > payload.shape[1] or message.size == 0:
        return extracted_signals

    pairs = {}
    for signal in message:
        if signal.muxer_for_signal == muxer:
            try:
                entry = signal.mux_val_min, signal.mux_val_max
            except:
                entry = tuple(signal.mux_val_grp[0]) if signal.mux_val_grp else (0, 0)
            pair_signals = pairs.setdefault(entry, [])
            pair_signals.append(signal)

    for pair, pair_signals in pairs.items():
        entry = bus, message_id, original_message_id, muxer, *pair

        extracted_signals[entry] = signals = {}

        if muxer_values is not None:
            min_, max_ = pair
            idx = np.argwhere((min_ <= muxer_values) & (muxer_values <= max_)).ravel()
            payload_ = payload[idx]
            t_ = t[idx]
        else:
            t_ = t
            payload_ = payload

        for sig in pair_signals:
            samples = extract_signal(
                sig,
                payload_,
                ignore_value2text_conversion=ignore_value2text_conversion,
                raw=True,
            )
            if len(samples) == 0 and len(t_):
                continue

            if include_message_name:
                sig_name = f"{message.name}.{sig.name}"
            else:
                sig_name = sig.name

            try:
                signals[sig_name] = {
                    "name": sig_name,
                    "comment": sig.comment or "",
                    "unit": sig.unit or "",
                    "samples": samples
                    if raw
                    else apply_conversion(samples, sig, ignore_value2text_conversion),
                    "t": t_,
                    "invalidation_bits": None,
                }

                if is_j1939:
                    signals[sig_name]["invalidation_bits"] = (
                        samples > MAX_VALID_J1939[defined_j1939_bit_count(sig)]
                    )

            except:
                print(format_exc())
                print(message, sig)
                print(samples, set(samples), samples.dtype, samples.shape)
                raise

            if sig.multiplex == "Multiplexor":
                extracted_signals.update(
                    extract_mux(
                        payload_,
                        message,
                        message_id,
                        bus,
                        t_,
                        muxer=sig.name,
                        muxer_values=samples,
                        original_message_id=original_message_id,
                        ignore_value2text_conversion=ignore_value2text_conversion,
                        raw=raw,
                        is_j1939=is_j1939,
                    )
                )

    return extracted_signals def from_dict(conversion: dict[str, Any]) -> v4b.ChannelConversion:
    if not conversion:
        conversion = None

    elif "a" in conversion:
        conversion["conversion_type"] = v4c.CONVERSION_TYPE_LIN
        conversion = v4b.ChannelConversion(**conversion)

    elif "formula" in conversion:
        conversion["formula"] = conversion["formula"]
        conversion["conversion_type"] = v4c.CONVERSION_TYPE_ALG
        conversion = v4b.ChannelConversion(**conversion)

    elif all(key in conversion for key in [f"P{i}" for i in range(1, 7)]):
        conversion["conversion_type"] = v4c.CONVERSION_TYPE_RAT
        conversion = v4b.ChannelConversion(**conversion)

    elif "raw_0" in conversion and "phys_0" in conversion:
        conversion["conversion_type"] = v4c.CONVERSION_TYPE_TAB
        nr = 0
        while f"phys_{nr}" in conversion:
            nr += 1
        conversion["val_param_nr"] = nr * 2
        if "interpolation" in conversion:
            conversion["conversion_type"] = v4c.CONVERSION_TYPE_TABI
        else:
            conversion["conversion_type"] = v4c.CONVERSION_TYPE_TAB
        conversion = v4b.ChannelConversion(**conversion)

    elif "mask_0" in conversion and "text_0" in conversion:
        conversion["conversion_type"] = v4c.CONVERSION_TYPE_BITFIELD
        nr = 0
        while f"text_{nr}" in conversion:
            val = conversion[f"text_{nr}"]
            if isinstance(val, (bytes, str)):
                partial_conversion = {
                    "conversion_type": v4c.CONVERSION_TYPE_RTABX,
                    "upper_0": conversion[f"upper_{nr}"],
                    "lower_0": conversion[f"lower_{nr}"],
                    "text_0": conversion[f"text_{nr}"]
                    if isinstance(conversion[f"text_{nr}"], bytes)
                    else conversion[f"text_{nr}"].encode("utf-8"),
                    "default": b"",
                }
                conversion[f"text_{nr}"] = from_dict(partial_conversion)

            nr += 1

        conversion["ref_param_nr"] = nr
        conversion["val_param_nr"] = nr
        conversion = v4b.ChannelConversion(**conversion)

    elif "upper_0" in conversion and "phys_0" in conversion:
        conversion["conversion_type"] = v4c.CONVERSION_TYPE_RTAB
        nr = 0
        while f"phys_{nr}" in conversion:
            nr += 1
        conversion["val_param_nr"] = nr * 3 + 1
        conversion = v4b.ChannelConversion(**conversion)

    elif "val_0" in conversion and "text_0" in conversion:
        conversion["conversion_type"] = v4c.CONVERSION_TYPE_TABX
        nr = 0
        while f"text_{nr}" in conversion:
            val = conversion[f"text_{nr}"]
            if isinstance(val, str):
                conversion[f"text_{nr}"] = val.encode("utf-8")
            elif isinstance(val, dict):
                conversion[f"text_{nr}"] = from_dict(val)
            nr += 1

        val = conversion.get("default_addr", b"")
        if isinstance(val, str):
            conversion[f"default_addr"] = val.encode("utf-8")
        elif isinstance(val, dict):
            conversion[f"default_addr"] = from_dict(val)

        conversion["ref_param_nr"] = nr + 1
        conversion = v4b.ChannelConversion(**conversion)

    elif "upper_0" in conversion and "text_0" in conversion:
        conversion["conversion_type"] = v4c.CONVERSION_TYPE_RTABX
        nr = 0
        while f"text_{nr}" in conversion:
            val = conversion[f"text_{nr}"]
            if isinstance(val, str):
                conversion[f"text_{nr}"] = val.encode("utf-8")
            elif isinstance(val, dict):
                conversion[f"text_{nr}"] = from_dict(val)
            nr += 1

        conversion["ref_param_nr"] = nr + 1

        val = conversion.get("default_addr", b"")
        if isinstance(val, str):
            conversion[f"default_addr"] = val.encode("utf-8")
        elif isinstance(val, dict):
            conversion[f"default_addr"] = from_dict(val)
        conversion = v4b.ChannelConversion(**conversion)

    else:
        conversion = v4b.ChannelConversion(conversion_type=v4c.CONVERSION_TYPE_NON)

    return conversion class MDF2(MDF3):

    _terminate = False

    """ shared implementation for mdf version 2 and 3 """

    def __init__(
        self,
        name: BufferedReader | BytesIO | StrPathType | None = None,
        version: str = "2.14",
        **kwargs,
    ) -> None:
        version = validate_version_argument(version, hint=2)

        super().__init__(name, version, **kwargs) class MDF3(MDF_Common):
    """The *header* attibute is a *HeaderBlock*.

    The *groups* attribute is a list of dicts, each one with the following keys

    * ``data_group`` - DataGroup object
    * ``channel_group`` - ChannelGroup object
    * ``channels`` - list of Channel objects with the same order as found in
      the mdf file
    * ``channel_dependencies`` - list of *ChannelArrayBlock* in case of channel
      arrays; list of Channel objects in case of structure channel
      composition
    * ``data_block`` - address of
      data block
    * ``data_location``- integer code for data location (original file,
      temporary file or memory)
    * ``data_block_addr`` - list of raw samples starting addresses
    * ``data_block_type`` - list of codes for data block type
    * ``data_block_size`` - list of raw samples block size
    * ``sorted`` - sorted indicator flag
    * ``record_size`` - dict that maps record ID's to record sizes in bytes
    * ``size`` - total size of data block for the current group
    * ``trigger`` - *Trigger* object for current group

    Parameters
    ----------
    name : string | pathlib.Path
        mdf file name (if provided it must be a real file name) or
        file-like object

    version : string
        mdf file version ('2.00', '2.10', '2.14', '3.00', '3.10', '3.20' or
        '3.30'); default '3.30'
    callback : function
        keyword only argument: function to call to update the progress; the
        function must accept two arguments (the current progress and maximum
        progress value)


    Attributes
    ----------
    attachments : list
        list of file attachments
    channels_db : dict
        used for fast channel access by name; for each name key the value is a
        list of (group index, channel index) tuples
    groups : list
        list of data group dicts
    header : HeaderBlock
        mdf file header
    identification : FileIdentificationBlock
        mdf file start block
    last_call_info : dict | None
        a dict to hold information about the last called method.

        .. versionadded:: 5.12.0

    masters_db : dict
        used for fast master channel access; for each group index key the value
         is the master channel index
    memory : str
        memory optimization option
    name : string
        mdf file name
    version : str
        mdf version

    """

    _terminate = False

    def __init__(
        self,
        name: BufferedReader | BytesIO | StrPathType | None = None,
        version: str = "3.30",
        channels: list[str] | None = None,
        **kwargs,
    ) -> None:
        self._kwargs = kwargs
        self.original_name = kwargs["original_name"]
        if channels is None:
            self.load_filter = set()
            self.use_load_filter = False
        else:
            self.load_filter = set(channels)
            self.use_load_filter = True

        self.temporary_folder = kwargs.get(
            "temporary_folder", get_global_option("temporary_folder")
        )

        self.groups = []
        self.header = None
        self.identification = None
        self.channels_db = ChannelsDB()
        self.masters_db = {}
        self.version = version

        self._master_channel_metadata = {}

        self._tempfile = TemporaryFile(dir=self.temporary_folder)
        self._tempfile.write(b"\0")
        self._file = None

        self._remove_source_from_channel_names = kwargs.get(
            "remove_source_from_channel_names", False
        )

        self._read_fragment_size = get_global_option("read_fragment_size")
        self._write_fragment_size = get_global_option("write_fragment_size")
        self._single_bit_uint_as_bool = get_global_option("single_bit_uint_as_bool")
        self._integer_interpolation = get_global_option("integer_interpolation")
        self._float_interpolation = get_global_option("float_interpolation")
        self._raise_on_multiple_occurrences = kwargs.get(
            "raise_on_multiple_occurrences",
            get_global_option("raise_on_multiple_occurrences"),
        )
        self._use_display_names = kwargs.get(
            "use_display_names", get_global_option("use_display_names")
        )
        self.copy_on_get = False

        self._si_map = {}
        self._cc_map = {}

        self._callback = kwargs.get("callback", None)

        self.last_call_info = None
        self._master = None

        self.virtual_groups_map = {}
        self.virtual_groups = {}

        if name:
            if is_file_like(name):
                self._file = name
                self.name = self.original_name = Path("From_FileLike.mdf")
                self._from_filelike = True
                self._read(mapped=False)
            else:
                if sys.maxsize < 2**32:
                    self.name = Path(name)
                    self._file = open(self.name, "rb")
                    self._from_filelike = False
                    self._read(mapped=False)
                else:
                    self.name = Path(name)
                    x = open(self.name, "rb")
                    self._file = mmap.mmap(x.fileno(), 0, access=mmap.ACCESS_READ)
                    self._from_filelike = False
                    self._read(mapped=True)

                    self._file.close()
                    x.close()

                    self._file = open(self.name, "rb")
        else:
            self._from_filelike = False
            version = validate_version_argument(version, hint=3)
            self.identification = FileIdentificationBlock(version=version)
            self.version = version
            self.header = HeaderBlock(version=self.version)
            self.name = Path("__new__.mdf")

        self._sort()

        for index, grp in enumerate(self.groups):

            self.virtual_groups_map[index] = index
            if index not in self.virtual_groups:
                self.virtual_groups[index] = VirtualChannelGroup()

            virtual_channel_group = self.virtual_groups[index]
            virtual_channel_group.groups.append(index)
            virtual_channel_group.record_size = grp.channel_group.samples_byte_nr
            virtual_channel_group.cycles_nr = grp.channel_group.cycles_nr

        self._parent = None

    def __del__(self) -> None:
        self.close()

    def _load_data(
        self,
        group: Group,
        record_offset: int = 0,
        record_count: int | None = None,
        optimize_read: bool = True,
    ) -> Iterator[tuple[bytes, int, int | None]]:
        """get group's data block bytes"""
        has_yielded = False
        offset = 0
        _count = record_count
        channel_group = group.channel_group

        if group.data_location == v23c.LOCATION_ORIGINAL_FILE:
            # go to the first data block of the current data group
            stream = self._file
        else:
            stream = self._tempfile

        samples_size = channel_group.samples_byte_nr

        record_offset *= samples_size
        if record_count is not None:
            record_count *= samples_size

        finished = False

        # go to the first data block of the current data group
        if group.sorted:
            if not samples_size:
                yield b"", 0, _count
                has_yielded = True
            else:
                if self._read_fragment_size:
                    split_size = self._read_fragment_size // samples_size
                    split_size *= samples_size
                else:
                    channels_nr = len(group.channels)

                    y_axis = CONVERT

                    idx = searchsorted(CHANNEL_COUNT, channels_nr, side="right") - 1
                    if idx < 0:
                        idx = 0
                    split_size = y_axis[idx]

                    split_size = split_size // samples_size
                    split_size *= samples_size

                if split_size == 0:
                    split_size = samples_size

                split_size = int(split_size)

                blocks = iter(group.data_blocks)

                cur_size = 0
                data = []

                while True:
                    try:
                        info = next(blocks)
                        address, size = info.address, info.original_size
                        current_address = address
                    except StopIteration:
                        break

                    if offset + size < record_offset + 1:
                        offset += size
                        continue

                    stream.seek(address)

                    if offset < record_offset:
                        delta = record_offset - offset
                        stream.seek(delta, 1)
                        current_address += delta
                        size -= delta
                        offset = record_offset

                    if record_count:
                        while size >= split_size - cur_size:
                            stream.seek(current_address)
                            if data:
                                data.append(
                                    stream.read(
                                        min(record_count, split_size - cur_size)
                                    )
                                )

                                bts = b"".join(data)[:record_count]
                                record_count -= len(bts)
                                __count = len(bts) // samples_size
                                yield bts, offset // samples_size, __count
                                has_yielded = True
                                current_address += split_size - cur_size

                                if record_count <= 0:
                                    finished = True
                                    break
                            else:

                                bts = stream.read(min(split_size, record_count))[
                                    :record_count
                                ]
                                record_count -= len(bts)
                                __count = len(bts) // samples_size
                                yield bts, offset // samples_size, __count
                                has_yielded = True
                                current_address += split_size - cur_size

                                if record_count <= 0:
                                    finished = True
                                    break

                            offset += split_size

                            size -= split_size - cur_size
                            data = []
                            cur_size = 0
                    else:

                        while size >= split_size - cur_size:
                            stream.seek(current_address)
                            if data:
                                data.append(stream.read(split_size - cur_size))

                                yield b"".join(data), offset, _count
                                has_yielded = True
                                current_address += split_size - cur_size
                            else:
                                yield stream.read(split_size), offset, _count
                                has_yielded = True
                                current_address += split_size

                            offset += split_size

                            size -= split_size - cur_size
                            data = []
                            cur_size = 0

                    if finished:
                        data = []
                        offset = -1
                        break

                    if size:
                        stream.seek(current_address)
                        if record_count:
                            data.append(stream.read(min(record_count, size)))
                        else:
                            data.append(stream.read(size))
                        cur_size += size
                        offset += size

                if data:
                    data = b"".join(data)
                    if record_count is not None:
                        data = data[:record_count]
                        yield data, offset, len(data) // samples_size
                        has_yielded = True
                    else:
                        yield data, offset, _count
                        has_yielded = True

                elif not offset:
                    yield b"", 0, _count
                    has_yielded = True
                if not has_yielded:
                    yield b"", 0, _count

        else:
            record_id = group.channel_group.record_id
            cg_size = group.record_size
            if group.data_group.record_id_len <= 2:
                record_id_nr = group.data_group.record_id_len
            else:
                record_id_nr = 0
            cg_data = []

            blocks = group.data_blocks

            for info in blocks:
                address, size = info.address, info.original_size
                stream.seek(address)
                data = stream.read(size)

                i = 0
                while i < size:
                    rec_id = data[i]
                    # skip record id
                    i += 1
                    rec_size = cg_size[rec_id]
                    if rec_id == record_id:
                        rec_data = data[i : i + rec_size]
                        cg_data.append(rec_data)
                    # consider the second record ID if it exists
                    if record_id_nr == 2:
                        i += rec_size + 1
                    else:
                        i += rec_size
                cg_data = b"".join(cg_data)
                size = len(cg_data)

                if size:
                    if offset + size < record_offset + 1:
                        offset += size
                        continue

                    if offset < record_offset:
                        delta = record_offset - offset
                        size -= delta
                        offset = record_offset

                    yield cg_data, offset, _count
                    has_yielded = True
                    offset += size
        if not has_yielded:
            yield b"", 0, _count

    def _prepare_record(
        self, group: Group
    ) -> tuple[dict[int, tuple[str, int]], dtype[Any]]:
        """compute record list

        Parameters
        ----------
        group : dict
            MDF group dict

        Returns
        -------
        record : list
            mapping of channels to records fields, records fields dtype

        """

        record = group.record

        if record is None:
            byte_order = self.identification.byte_order
            channels = group.channels

            record = []

            for idx, new_ch in enumerate(channels):
                start_offset = new_ch.start_offset
                try:
                    additional_byte_offset = new_ch.additional_byte_offset
                    start_offset += 8 * additional_byte_offset
                except AttributeError:
                    pass

                byte_offset, bit_offset = divmod(start_offset, 8)
                data_type = new_ch.data_type
                bit_count = new_ch.bit_count

                if not new_ch.component_addr:

                    if not new_ch.dtype_fmt:
                        new_ch.dtype_fmt = dtype(
                            get_fmt_v3(data_type, bit_count, byte_order)
                        )

                    # adjust size to 1, 2, 4 or 8 bytes
                    size = bit_offset + bit_count

                    byte_size, rem = size // 8, size % 8
                    if rem:
                        byte_size += 1
                    bit_size = byte_size * 8

                    if data_type in (
                        v23c.DATA_TYPE_SIGNED_MOTOROLA,
                        v23c.DATA_TYPE_UNSIGNED_MOTOROLA,
                    ):
                        if size > 32:
                            bit_offset += 64 - bit_size
                        elif size > 16:
                            bit_offset += 32 - bit_size
                        elif size > 8:
                            bit_offset += 16 - bit_size

                    record.append(
                        (
                            new_ch.dtype_fmt,
                            new_ch.dtype_fmt.itemsize,
                            byte_offset,
                            bit_offset,
                        )
                    )
                else:
                    record.append(None)

            group.record = record

        return record

    def _get_not_byte_aligned_data(
        self, data: bytes, group: Group, ch_nr: int
    ) -> NDArray[Any]:
        big_endian_types = (
            v23c.DATA_TYPE_UNSIGNED_MOTOROLA,
            v23c.DATA_TYPE_FLOAT_MOTOROLA,
            v23c.DATA_TYPE_DOUBLE_MOTOROLA,
            v23c.DATA_TYPE_SIGNED_MOTOROLA,
        )

        record_size = group.channel_group.samples_byte_nr

        channel = group.channels[ch_nr]

        byte_offset, bit_offset = divmod(channel.start_offset, 8)
        bit_count = channel.bit_count

        byte_size = bit_offset + bit_count
        if byte_size % 8:
            byte_size = (byte_size // 8) + 1
        else:
            byte_size //= 8

        types = [
            ("", f"a{byte_offset}"),
            ("vals", f"({byte_size},)u1"),
            ("", f"a{record_size - byte_size - byte_offset}"),
        ]

        vals = fromstring(data, dtype=dtype(types))

        vals = vals["vals"]

        if byte_size in {1, 2, 4, 8}:
            extra_bytes = 0
        else:
            extra_bytes = 4 - (byte_size % 4)

        std_size = byte_size + extra_bytes

        big_endian = channel.data_type in big_endian_types

        # prepend or append extra bytes columns
        # to get a standard size number of bytes

        if extra_bytes:
            if big_endian:

                vals = column_stack(
                    [vals, zeros(len(vals), dtype=f"<({extra_bytes},)u1")]
                )
                try:
                    vals = vals.view(f">u{std_size}").ravel()
                except:
                    vals = frombuffer(vals.tobytes(), dtype=f">u{std_size}")

                vals = vals >> (extra_bytes * 8 + bit_offset)
                vals &= (1 << bit_count) - 1

            else:
                vals = column_stack(
                    [vals, zeros(len(vals), dtype=f"<({extra_bytes},)u1")]
                )
                try:
                    vals = vals.view(f"<u{std_size}").ravel()
                except:
                    vals = frombuffer(vals.tobytes(), dtype=f"<u{std_size}")

                vals = vals >> bit_offset
                vals &= (1 << bit_count) - 1

        else:
            if big_endian:
                try:
                    vals = vals.view(f">u{std_size}").ravel()
                except:
                    vals = frombuffer(vals.tobytes(), dtype=f">u{std_size}")

                vals = vals >> bit_offset
                vals &= (1 << bit_count) - 1

            else:
                try:
                    vals = vals.view(f"<u{std_size}").ravel()
                except:
                    vals = frombuffer(vals.tobytes(), dtype=f"<u{std_size}")

                vals = vals >> bit_offset
                vals &= (1 << bit_count) - 1

        data_type = channel.data_type

        if data_type in v23c.SIGNED_INT:
            return as_non_byte_sized_signed_int(vals, bit_count)
        elif data_type in v23c.FLOATS:
            return vals.view(
                get_fmt_v3(data_type, bit_count, self.identification.byte_order)
            )
        else:
            return vals

    def _read(self, mapped: bool = False) -> None:
        stream = self._file
        filter_channels = self.use_load_filter

        cg_count, _ = count_channel_groups(stream)
        if self._callback:
            self._callback(0, cg_count)
        current_cg_index = 0

        stream.seek(0, 2)
        self.file_limit = stream.tell()
        stream.seek(0)

        dg_cntr = 0

        self.identification = FileIdentificationBlock(stream=stream)
        self.header = HeaderBlock(stream=stream)

        self.version = self.identification.version_str.decode("latin-1").strip(
            " \n\t\0"
        )

        # this will hold mapping from channel address to Channel object
        # needed for linking dependency blocks to referenced channels after
        # the file is loaded
        ch_map = {}

        # go to first data group
        dg_addr = self.header.first_dg_addr
        # read each data group sequentially
        while dg_addr:
            if dg_addr > self.file_limit:
                logger.warning(
                    f"Data group address {dg_addr:X} is outside the file size {self.file_limit}"
                )
                break
            data_group = DataGroup(address=dg_addr, stream=stream, mapped=mapped)
            record_id_nr = data_group.record_id_len
            cg_nr = data_group.cg_nr
            cg_addr = data_group.first_cg_addr
            data_addr = data_group.data_block_addr

            # read trigger information if available
            trigger_addr = data_group.trigger_addr
            if trigger_addr:
                if trigger_addr > self.file_limit:
                    logger.warning(
                        f"Trigger address {trigger_addr:X} is outside the file size {self.file_limit}"
                    )
                    trigger = None
                else:
                    trigger = TriggerBlock(address=trigger_addr, stream=stream)
            else:
                trigger = None

            new_groups = []
            for i in range(cg_nr):

                new_groups.append(Group(None))
                grp = new_groups[-1]
                grp.channels = []
                grp.trigger = trigger
                grp.channel_dependencies = []

                if record_id_nr:
                    grp.sorted = False
                else:
                    grp.sorted = True

                kargs = {"first_cg_addr": cg_addr, "data_block_addr": data_addr}
                if self.version >= "3.20":
                    kargs["block_len"] = v23c.DG_POST_320_BLOCK_SIZE
                else:
                    kargs["block_len"] = v23c.DG_PRE_320_BLOCK_SIZE
                kargs["record_id_len"] = record_id_nr
                kargs["address"] = data_group.address

                grp.data_group = DataGroup(**kargs)

                # read each channel group sequentially
                if cg_addr > self.file_limit:
                    logger.warning(
                        f"Channel group address {cg_addr:X} is outside the file size {self.file_limit}"
                    )
                    break
                grp.channel_group = ChannelGroup(address=cg_addr, stream=stream)

                # go to first channel of the current channel group
                ch_addr = grp.channel_group.first_ch_addr
                ch_cntr = 0
                grp_chs = grp.channels

                while ch_addr:
                    if ch_addr > self.file_limit:
                        logger.warning(
                            f"Channel address {ch_addr:X} is outside the file size {self.file_limit}"
                        )
                        break

                    if filter_channels:
                        display_names = {}
                        if mapped:
                            (
                                id_,
                                block_len,
                                next_ch_addr,
                                channel_type,
                                name,
                            ) = v23c.CHANNEL_FILTER_uf(stream, ch_addr)
                            name = name.decode("latin-1").strip(" \t\n\r\0")
                            if block_len >= v23c.CN_LONGNAME_BLOCK_SIZE:
                                tx_address = v23c.UINT32_uf(
                                    stream, ch_addr + v23c.CN_SHORT_BLOCK_SIZE
                                )[0]
                                if tx_address:
                                    name = get_text_v3(
                                        tx_address, stream, mapped=mapped
                                    )
                                if block_len == v23c.CN_DISPLAYNAME_BLOCK_SIZE:
                                    tx_address = v23c.UINT32_uf(
                                        stream, ch_addr + v23c.CN_LONGNAME_BLOCK_SIZE
                                    )[0]
                                    if tx_address:
                                        display_names = {
                                            get_text_v3(
                                                tx_address, stream, mapped=mapped
                                            ): "display_name",
                                        }

                        else:
                            stream.seek(ch_addr)
                            (
                                id_,
                                block_len,
                                next_ch_addr,
                                channel_type,
                                name,
                            ) = v23c.CHANNEL_FILTER_u(
                                stream.read(v23c.CHANNEL_FILTER_SIZE)
                            )
                            name = name.decode("latin-1").strip(" \t\n\r\0")

                            if block_len >= v23c.CN_LONGNAME_BLOCK_SIZE:
                                stream.seek(ch_addr + v23c.CN_SHORT_BLOCK_SIZE)
                                tx_address = v23c.UINT32_u(stream.read(4))[0]
                                if tx_address:
                                    name = get_text_v3(
                                        tx_address, stream, mapped=mapped
                                    )
                                if block_len == v23c.CN_DISPLAYNAME_BLOCK_SIZE:
                                    stream.seek(ch_addr + v23c.CN_LONGNAME_BLOCK_SIZE)
                                    tx_address = v23c.UINT32_u(stream.read(4))[0]
                                    if tx_address:
                                        display_names = {
                                            get_text_v3(
                                                tx_address, stream, mapped=mapped
                                            ): "display_name",
                                        }

                        if id_ != b"CN":
                            message = (
                                f'Expected "CN" block @{hex(ch_addr)} but found "{id_}"'
                            )
                            raise MdfException(message)

                        if (
                            channel_type == v23c.CHANNEL_TYPE_MASTER
                            or name in self.load_filter
                            or (
                                any(
                                    _name in self.load_filter for _name in display_names
                                )
                            )
                        ):
                            new_ch = Channel(
                                address=ch_addr,
                                stream=stream,
                                mapped=mapped,
                                si_map=self._si_map,
                                cc_map=self._cc_map,
                                parsed_strings=(name, display_names),
                            )
                        else:
                            ch_addr = next_ch_addr
                            continue
                    else:
                        # read channel block and create channel object
                        new_ch = Channel(
                            address=ch_addr,
                            stream=stream,
                            mapped=mapped,
                            si_map=self._si_map,
                            cc_map=self._cc_map,
                            parsed_strings=None,
                        )

                    if self._remove_source_from_channel_names:
                        new_ch.name = new_ch.name.split("\\", 1)[0]
                        new_ch.display_names = {
                            _name.split("\\", 1)[0]: val
                            for _name, val in new_ch.display_names.items()
                        }

                    # check if it has channel dependencies
                    if new_ch.component_addr:
                        dep = ChannelDependency(
                            address=new_ch.component_addr, stream=stream
                        )
                        grp.channel_dependencies.append(dep)
                    else:
                        grp.channel_dependencies.append(None)

                    # update channel map
                    entry = dg_cntr, ch_cntr
                    ch_map[ch_addr] = entry

                    for name in (new_ch.name, *tuple(new_ch.display_names)):
                        if name:
                            self.channels_db.add(name, entry)

                    if new_ch.channel_type == v23c.CHANNEL_TYPE_MASTER:
                        self.masters_db[dg_cntr] = ch_cntr
                    # go to next channel of the current channel group

                    ch_cntr += 1
                    grp_chs.append(new_ch)
                    ch_addr = new_ch.next_ch_addr

                cg_addr = grp.channel_group.next_cg_addr
                dg_cntr += 1

                current_cg_index += 1
                if self._callback:
                    self._callback(current_cg_index, cg_count)

                if self._terminate:
                    self.close()
                    return

            # store channel groups record sizes dict and data block size in
            # each new group data belong to the initial unsorted group, and
            # add the key 'sorted' with the value False to use a flag;
            # this is used later if memory=False

            cg_size = {}
            total_size = 0

            for grp in new_groups:
                record_id = grp.channel_group.record_id
                cycles_nr = grp.channel_group.cycles_nr
                record_size = grp.channel_group.samples_byte_nr
                self._prepare_record(grp)

                cg_size[record_id] = record_size

                record_size += record_id_nr
                total_size += record_size * cycles_nr

                grp.record_size = cg_size

            for grp in new_groups:
                grp.data_location = v23c.LOCATION_ORIGINAL_FILE
                if total_size:
                    grp.data_blocks.append(
                        DataBlockInfo(
                            address=data_group.data_block_addr,
                            block_type=0,
                            original_size=total_size,
                            compressed_size=total_size,
                            param=0,
                        )
                    )

            self.groups.extend(new_groups)

            # go to next data group
            dg_addr = data_group.next_dg_addr

        # finally update the channel dependency references
        for grp in self.groups:
            for dep in grp.channel_dependencies:
                if dep:
                    for i in range(dep.sd_nr):
                        ref_channel_addr = dep[f"ch_{i}"]
                        channel = ch_map[ref_channel_addr]
                        dep.referenced_channels.append(channel)

    def _filter_occurrences(
        self,
        occurrences: Sequence[tuple[int, int]],
        source_name: str | None = None,
        source_path: str | None = None,
        acq_name: str | None = None,
    ) -> Iterator[tuple[int, int]]:
        if source_name is not None:
            occurrences = (
                (gp_idx, cn_idx)
                for gp_idx, cn_idx in occurrences
                if (
                    self.groups[gp_idx].channels[cn_idx].source is not None
                    and self.groups[gp_idx].channels[cn_idx].source.name == source_name
                )
            )

        if source_path is not None:
            occurrences = (
                (gp_idx, cn_idx)
                for gp_idx, cn_idx in occurrences
                if (
                    self.groups[gp_idx].channels[cn_idx].source is not None
                    and self.groups[gp_idx].channels[cn_idx].source.path == source_path
                )
            )

        return occurrences

    def add_trigger(
        self,
        group: int,
        timestamp: float,
        pre_time: float = 0,
        post_time: float = 0,
        comment: str = "",
    ) -> None:
        """add trigger to data group

        Parameters
        ----------
        group : int
            group index
        timestamp : float
            trigger time
        pre_time : float
            trigger pre time; default 0
        post_time : float
            trigger post time; default 0
        comment : str
            trigger comment

        """
        comment_template = """<EVcomment>
    <TX>{}</TX>
</EVcomment>"""
        try:
            group = self.groups[group]
        except IndexError:
            return

        trigger = group.trigger

        if comment:
            try:
                comment = ET.fromstring(comment)
                if comment.find(".//TX"):
                    comment = comment.find(".//TX").text
                else:
                    comment = ""
            except ET.ParseError:
                pass

        if trigger:
            count = trigger["trigger_events_nr"]
            trigger["trigger_events_nr"] += 1
            trigger.block_len += 24
            trigger[f"trigger_{count}_time"] = timestamp
            trigger[f"trigger_{count}_pretime"] = pre_time
            trigger[f"trigger_{count}_posttime"] = post_time
            if comment:
                if trigger.comment is None:
                    comment = f"{count + 1}. {comment}"
                    comment = comment_template.format(comment)
                    trigger.comment = comment
                else:
                    current_comment = trigger.comment
                    try:
                        current_comment = ET.fromstring(current_comment)
                        if current_comment.find(".//TX"):
                            current_comment = current_comment.find(".//TX").text
                        else:
                            current_comment = ""
                    except ET.ParseError:
                        pass

                    comment = f"{current_comment}\n{count + 1}. {comment}"
                    comment = comment_template.format(comment)
                    trigger.comment = comment
        else:
            trigger = TriggerBlock(
                trigger_event_nr=1,
                trigger_0_time=timestamp,
                trigger_0_pretime=pre_time,
                trigger_0_posttime=post_time,
            )
            if comment:
                comment = f"1. {comment}"
                comment = comment_template.format(comment)
                trigger.comment = comment

            group.trigger = trigger

    def append(
        self,
        signals: list[Signal] | Signal | DataFrame,
        acq_name: str | None = None,
        acq_source: Source | None = None,
        comment: str = "Python",
        common_timebase: bool = False,
        units: dict[str, str | bytes] | None = None,
    ) -> int | None:
        """Appends a new data group.

        For channel dependencies type Signals, the *samples* attribute must be
        a numpy.recarray

        Parameters
        ----------
        signals : list | Signal | pandas.DataFrame
            list of *Signal* objects, or a single *Signal* object, or a pandas
            *DataFrame* object. All bytes columns in the pandas *DataFrame*
            must be *latin-1* encoded
        acq_name : str
            channel group acquisition name
        acq_source : Source
            channel group acquisition source
        comment : str
            channel group comment; default 'Python'
        common_timebase : bool
            flag to hint that the signals have the same timebase. Only set this
            if you know for sure that all appended channels share the same
            time base
        units : dict
            will contain the signal units mapped to the signal names when
            appending a pandas DataFrame


        Examples
        --------
        >>> # case 1 conversion type None
        >>> s1 = np.array([1, 2, 3, 4, 5])
        >>> s2 = np.array([-1, -2, -3, -4, -5])
        >>> s3 = np.array([0.1, 0.04, 0.09, 0.16, 0.25])
        >>> t = np.array([0.001, 0.002, 0.003, 0.004, 0.005])
        >>> names = ['Positive', 'Negative', 'Float']
        >>> units = ['+', '-', '.f']
        >>> info = {}
        >>> s1 = Signal(samples=s1, timestamps=t, unit='+', name='Positive')
        >>> s2 = Signal(samples=s2, timestamps=t, unit='-', name='Negative')
        >>> s3 = Signal(samples=s3, timestamps=t, unit='flts', name='Floats')
        >>> mdf = MDF3('new.mdf')
        >>> mdf.append([s1, s2, s3], comment='created by asammdf v1.1.0')
        >>> # case 2: VTAB conversions from channels inside another file
        >>> mdf1 = MDF3('in.mdf')
        >>> ch1 = mdf1.get("Channel1_VTAB")
        >>> ch2 = mdf1.get("Channel2_VTABR")
        >>> sigs = [ch1, ch2]
        >>> mdf2 = MDF3('out.mdf')
        >>> mdf2.append(sigs, comment='created by asammdf v1.1.0')
        >>> df = pd.DataFrame.from_dict({'s1': np.array([1, 2, 3, 4, 5]), 's2': np.array([-1, -2, -3, -4, -5])})
        >>> units = {'s1': 'V', 's2': 'A'}
        >>> mdf2.append(df, units=units)

        """
        if isinstance(signals, Signal):
            signals = [signals]
        elif isinstance(signals, DataFrame):
            self._append_dataframe(signals, comment=comment, units=units)
            return

        version = self.version
        integer_interp_mode = self._integer_interpolation
        float_interp_mode = self._float_interpolation

        # check if the signals have a common timebase
        # if not interpolate the signals using the union of all timebases
        if signals:
            timestamps = signals[0].timestamps
            if not common_timebase:
                for signal in signals[1:]:
                    if not array_equal(signal.timestamps, timestamps):
                        different = True
                        break
                else:
                    different = False

                if different:
                    times = [s.timestamps for s in signals]
                    timestamps = unique(concatenate(times)).astype(float64)
                    signals = [
                        s.interp(
                            timestamps,
                            integer_interpolation_mode=integer_interp_mode,
                            float_interpolation_mode=float_interp_mode,
                        )
                        for s in signals
                    ]
                    times = None
        else:
            timestamps = array([])

        if self.version >= "3.00":
            channel_size = v23c.CN_DISPLAYNAME_BLOCK_SIZE
        elif self.version >= "2.10":
            channel_size = v23c.CN_LONGNAME_BLOCK_SIZE
        else:
            channel_size = v23c.CN_SHORT_BLOCK_SIZE

        file = self._tempfile
        tell = file.tell

        kargs = {
            "module_nr": 0,
            "module_address": 0,
            "type": v23c.SOURCE_ECU,
            "description": b"Channel inserted by Python Script",
        }
        ce_block = ChannelExtension(**kargs)

        canopen_time_fields = ("ms", "days")
        canopen_date_fields = (
            "ms",
            "min",
            "hour",
            "day",
            "month",
            "year",
            "summer_time",
            "day_of_week",
        )

        dg_cntr = len(self.groups)

        gp = Group(None)
        gp.channels = gp_channels = []
        gp.channel_dependencies = gp_dep = []
        gp.signal_types = gp_sig_types = []
        gp.string_dtypes = []
        gp.record = record = []

        self.groups.append(gp)

        cycles_nr = len(timestamps)
        fields = []
        types = []
        ch_cntr = 0
        offset = 0
        field_names = UniqueDB()

        if signals:
            master_metadata = signals[0].master_metadata
        else:
            master_metadata = None
        if master_metadata:
            time_name = master_metadata[0]
        else:
            time_name = "time"

        if signals:
            # conversion for time channel
            kargs = {
                "conversion_type": v23c.CONVERSION_TYPE_NONE,
                "unit": b"s",
                "min_phy_value": timestamps[0] if cycles_nr else 0,
                "max_phy_value": timestamps[-1] if cycles_nr else 0,
            }
            conversion = ChannelConversion(**kargs)
            conversion.unit = "s"
            source = ce_block

            # time channel
            t_type, t_size = fmt_to_datatype_v3(timestamps.dtype, timestamps.shape)
            kargs = {
                "short_name": time_name.encode("latin-1"),
                "channel_type": v23c.CHANNEL_TYPE_MASTER,
                "data_type": t_type,
                "start_offset": 0,
                "min_raw_value": timestamps[0] if cycles_nr else 0,
                "max_raw_value": timestamps[-1] if cycles_nr else 0,
                "bit_count": t_size,
                "block_len": channel_size,
                "version": version,
            }
            channel = Channel(**kargs)
            channel.name = name = time_name
            channel.conversion = conversion
            channel.source = source

            gp_channels.append(channel)

            self.channels_db.add(name, (dg_cntr, ch_cntr))
            self.masters_db[dg_cntr] = 0

            # time channel doesn't have channel dependencies
            gp_dep.append(None)

            fields.append(timestamps)
            types.append((name, timestamps.dtype))

            offset += t_size
            ch_cntr += 1

            gp_sig_types.append(0)
            record.append(
                (
                    timestamps.dtype,
                    timestamps.dtype.itemsize,
                    0,
                    0,
                )
            )

        for signal in signals:
            sig = signal
            names = sig.samples.dtype.names
            name = signal.name
            if names is None:
                sig_type = v23c.SIGNAL_TYPE_SCALAR
            else:
                if names in (canopen_time_fields, canopen_date_fields):
                    sig_type = v23c.SIGNAL_TYPE_CANOPEN
                elif names[0] != sig.name:
                    sig_type = v23c.SIGNAL_TYPE_STRUCTURE_COMPOSITION
                else:
                    sig_type = v23c.SIGNAL_TYPE_ARRAY

            gp_sig_types.append(sig_type)

            # conversions for channel

            conversion = conversion_transfer(signal.conversion)
            conversion.unit = unit = signal.unit

            israw = signal.raw

            if not israw and not unit:
                conversion = None

            if sig_type == v23c.SIGNAL_TYPE_SCALAR:

                # source for channel
                if signal.source:
                    source = signal.source
                    if source.source_type != 2:
                        kargs = {
                            "type": v23c.SOURCE_ECU,
                            "description": source.name.encode("latin-1"),
                            "ECU_identification": source.path.encode("latin-1"),
                        }
                    else:
                        kargs = {
                            "type": v23c.SOURCE_VECTOR,
                            "message_name": source.name.encode("latin-1"),
                            "sender_name": source.path.encode("latin-1"),
                        }

                    new_source = ChannelExtension(**kargs)

                else:
                    new_source = ce_block

                # compute additional byte offset for large records size
                if offset > v23c.MAX_UINT16:
                    additional_byte_offset = ceil((offset - v23c.MAX_UINT16) / 8)
                    start_bit_offset = offset - additional_byte_offset * 8

                else:
                    start_bit_offset = offset
                    additional_byte_offset = 0

                s_type, s_size = fmt_to_datatype_v3(
                    signal.samples.dtype, signal.samples.shape
                )

                name = signal.name
                display_names = signal.display_names

                if signal.samples.dtype.kind == "u" and signal.bit_count <= 4:
                    s_size_ = signal.bit_count
                else:
                    s_size_ = s_size

                kargs = {
                    "channel_type": v23c.CHANNEL_TYPE_VALUE,
                    "data_type": s_type,
                    "start_offset": start_bit_offset,
                    "bit_count": s_size_,
                    "additional_byte_offset": additional_byte_offset,
                    "block_len": channel_size,
                    "version": version,
                }

                if s_size < 8:
                    s_size = 8

                channel = Channel(**kargs)
                channel.name = signal.name
                channel.comment = signal.comment
                channel.source = new_source
                channel.conversion = conversion
                channel.display_names = display_names
                gp_channels.append(channel)

                if len(signal.samples.shape) > 1:
                    channel.dtype_fmt = dtype(
                        (signal.samples.dtype, signal.samples.shape[1:])
                    )
                else:
                    channel.dtype_fmt = signal.samples.dtype

                record.append(
                    (
                        channel.dtype_fmt,
                        channel.dtype_fmt.itemsize,
                        offset // 8,
                        0,
                    )
                )

                offset += s_size

                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in display_names:
                    self.channels_db.add(_name, entry)

                field_name = field_names.get_unique_name(name)

                if signal.samples.dtype.kind == "S":
                    gp.string_dtypes.append(signal.samples.dtype)

                fields.append(signal.samples)
                if s_type != v23c.DATA_TYPE_BYTEARRAY:
                    types.append((field_name, signal.samples.dtype))
                else:
                    types.append(
                        (field_name, signal.samples.dtype, signal.samples.shape[1:])
                    )

                ch_cntr += 1

                # simple channels don't have channel dependencies
                gp_dep.append(None)

            # second, add the composed signals
            elif sig_type in (
                v23c.SIGNAL_TYPE_CANOPEN,
                v23c.SIGNAL_TYPE_STRUCTURE_COMPOSITION,
            ):
                new_dg_cntr = len(self.groups)
                new_gp = Group(None)
                new_gp.channels = new_gp_channels = []
                new_gp.channel_dependencies = new_gp_dep = []
                new_gp.signal_types = new_gp_sig_types = []
                new_gp.record = new_record = []
                self.groups.append(new_gp)

                new_fields = []
                new_types = []
                new_ch_cntr = 0
                new_offset = 0
                new_field_names = UniqueDB()

                # conversion for time channel
                kargs = {
                    "conversion_type": v23c.CONVERSION_TYPE_NONE,
                    "unit": b"s",
                    "min_phy_value": timestamps[0] if cycles_nr else 0,
                    "max_phy_value": timestamps[-1] if cycles_nr else 0,
                }
                conversion = ChannelConversion(**kargs)
                conversion.unit = "s"

                source = ce_block

                # time channel
                t_type, t_size = fmt_to_datatype_v3(timestamps.dtype, timestamps.shape)
                kargs = {
                    "short_name": time_name.encode("latin-1"),
                    "channel_type": v23c.CHANNEL_TYPE_MASTER,
                    "data_type": t_type,
                    "start_offset": 0,
                    "min_raw_value": timestamps[0] if cycles_nr else 0,
                    "max_raw_value": timestamps[-1] if cycles_nr else 0,
                    "bit_count": t_size,
                    "block_len": channel_size,
                    "version": version,
                }
                channel = Channel(**kargs)
                channel.name = name = time_name
                channel.source = source
                channel.conversion = conversion
                new_gp_channels.append(channel)

                new_record.append(
                    (
                        timestamps.dtype,
                        timestamps.dtype.itemsize,
                        0,
                        0,
                    )
                )

                self.channels_db.add(name, (new_dg_cntr, new_ch_cntr))

                self.masters_db[new_dg_cntr] = 0

                # time channel doesn't have channel dependencies
                new_gp_dep.append(None)

                new_fields.append(timestamps)
                new_types.append((name, timestamps.dtype))
                new_field_names.get_unique_name(name)
                new_gp_sig_types.append(0)

                new_offset += t_size
                new_ch_cntr += 1

                names = signal.samples.dtype.names
                if names == ("ms", "days"):
                    channel_group_comment = "From mdf v4 CANopen Time channel"
                elif names == (
                    "ms",
                    "min",
                    "hour",
                    "day",
                    "month",
                    "year",
                    "summer_time",
                    "day_of_week",
                ):
                    channel_group_comment = "From mdf v4 CANopen Date channel"
                else:
                    channel_group_comment = "From mdf v4 structure channel composition"

                for name in names:

                    samples = signal.samples[name]

                    new_record.append(
                        (
                            samples.dtype,
                            samples.dtype.itemsize,
                            new_offset // 8,
                            0,
                        )
                    )

                    # conversions for channel

                    kargs = {
                        "conversion_type": v23c.CONVERSION_TYPE_NONE,
                        "unit": signal.unit.encode("latin-1"),
                        "min_phy_value": 0,
                        "max_phy_value": 0,
                    }
                    conversion = ChannelConversion(**kargs)

                    # source for channel
                    if signal.source:
                        source = signal.source
                        if source.source_type != 2:
                            kargs = {
                                "type": v23c.SOURCE_ECU,
                                "description": source.name.encode("latin-1"),
                                "ECU_identification": source.path.encode("latin-1"),
                            }
                        else:
                            kargs = {
                                "type": v23c.SOURCE_VECTOR,
                                "message_name": source.name.encode("latin-1"),
                                "sender_name": source.path.encode("latin-1"),
                            }

                        source = ChannelExtension(**kargs)

                    else:
                        source = ce_block

                    # compute additional byte offset for large records size
                    if new_offset > v23c.MAX_UINT16:
                        additional_byte_offset = ceil(
                            (new_offset - v23c.MAX_UINT16) / 8
                        )
                        start_bit_offset = new_offset - additional_byte_offset * 8
                    else:
                        start_bit_offset = new_offset
                        additional_byte_offset = 0
                    s_type, s_size = fmt_to_datatype_v3(samples.dtype, samples.shape)

                    kargs = {
                        "channel_type": v23c.CHANNEL_TYPE_VALUE,
                        "data_type": s_type,
                        "start_offset": start_bit_offset,
                        "bit_count": s_size,
                        "additional_byte_offset": additional_byte_offset,
                        "block_len": channel_size,
                        "version": version,
                    }

                    if s_size < 8:
                        s_size = 8

                    channel = Channel(**kargs)
                    channel.name = name
                    channel.source = source
                    channel.conversion = conversion

                    new_gp_channels.append(channel)
                    new_offset += s_size

                    self.channels_db.add(name, (new_dg_cntr, new_ch_cntr))

                    field_name = new_field_names.get_unique_name(name)

                    new_fields.append(samples)
                    new_types.append((field_name, samples.dtype))

                    new_ch_cntr += 1

                    # simple channels don't have channel dependencies
                    new_gp_dep.append(None)

                # channel group
                kargs = {
                    "cycles_nr": cycles_nr,
                    "samples_byte_nr": new_offset // 8,
                    "ch_nr": new_ch_cntr,
                }
                new_gp.channel_group = ChannelGroup(**kargs)
                new_gp.channel_group.comment = channel_group_comment

                # data group
                if self.version >= "3.20":
                    block_len = v23c.DG_POST_320_BLOCK_SIZE
                else:
                    block_len = v23c.DG_PRE_320_BLOCK_SIZE
                new_gp.data_group = DataGroup(block_len=block_len)

                # data block
                new_types = dtype(new_types)

                new_gp.sorted = True

                samples = fromarrays(new_fields, dtype=new_types)

                block = samples.tobytes()

                new_gp.data_location = v23c.LOCATION_TEMPORARY_FILE
                if cycles_nr:
                    data_address = tell()
                    new_gp.data_group.data_block_addr = data_address
                    self._tempfile.write(block)
                    size = len(block)
                    new_gp.data_blocks.append(
                        DataBlockInfo(
                            address=data_address,
                            original_size=size,
                            compressed_size=size,
                            block_type=0,
                            param=0,
                        )
                    )
                else:
                    new_gp.data_group.data_block_addr = 0

                # data group trigger
                new_gp.trigger = None

            else:
                names = signal.samples.dtype.names
                name = signal.name

                component_names = []
                component_samples = []
                if names:
                    samples = signal.samples[names[0]]
                else:
                    samples = signal.samples

                shape = samples.shape[1:]
                dims = [list(range(size)) for size in shape]

                for indexes in product(*dims):
                    subarray = samples
                    for idx in indexes:
                        subarray = subarray[:, idx]
                    component_samples.append(subarray)

                    indexes = "".join(f"[{idx}]" for idx in indexes)
                    component_name = f"{name}{indexes}"
                    component_names.append(component_name)

                # add channel dependency block for composed parent channel
                sd_nr = len(component_samples)
                kargs = {"sd_nr": sd_nr}
                for i, dim in enumerate(shape[::-1]):
                    kargs[f"dim_{i}"] = dim
                parent_dep = ChannelDependency(**kargs)
                gp_dep.append(parent_dep)

                # source for channel
                if signal.source:
                    source = signal.source
                    if source.source_type != 2:
                        kargs = {
                            "type": v23c.SOURCE_ECU,
                            "description": source.name.encode("latin-1"),
                            "ECU_identification": source.path.encode("latin-1"),
                        }
                    else:
                        kargs = {
                            "type": v23c.SOURCE_VECTOR,
                            "message_name": source.name.encode("latin-1"),
                            "sender_name": source.path.encode("latin-1"),
                        }

                    source = ChannelExtension(**kargs)

                else:
                    source = ce_block

                s_type, s_size = fmt_to_datatype_v3(samples.dtype, (), True)
                # compute additional byte offset for large records size
                if offset > v23c.MAX_UINT16:
                    additional_byte_offset = ceil((offset - v23c.MAX_UINT16) / 8)
                    start_bit_offset = offset - additional_byte_offset * 8
                else:
                    start_bit_offset = offset
                    additional_byte_offset = 0

                kargs = {
                    "channel_type": v23c.CHANNEL_TYPE_VALUE,
                    "data_type": s_type,
                    "start_offset": start_bit_offset,
                    "bit_count": s_size,
                    "additional_byte_offset": additional_byte_offset,
                    "block_len": channel_size,
                    "version": version,
                }

                if s_size < 8:
                    s_size = 8

                new_record.append(None)

                channel = Channel(**kargs)
                channel.comment = signal.comment
                channel.display_names = signal.display_names

                gp_channels.append(channel)

                self.channels_db.add(name, (dg_cntr, ch_cntr))

                ch_cntr += 1

                for i, (name, samples) in enumerate(
                    zip(component_names, component_samples)
                ):

                    if i < sd_nr:
                        dep_pair = dg_cntr, ch_cntr
                        parent_dep.referenced_channels.append(dep_pair)
                        description = b"\0"
                    else:
                        description = f"{signal.name} - axis {name}"
                        description = description.encode("latin-1")

                    s_type, s_size = fmt_to_datatype_v3(samples.dtype, ())
                    shape = samples.shape[1:]

                    # source for channel
                    if signal.source:
                        source = signal.source
                        if source.source_type != 2:
                            kargs = {
                                "type": v23c.SOURCE_ECU,
                                "description": source.name.encode("latin-1"),
                                "ECU_identification": source.path.encode("latin-1"),
                            }
                        else:
                            kargs = {
                                "type": v23c.SOURCE_VECTOR,
                                "message_name": source.name.encode("latin-1"),
                                "sender_name": source.path.encode("latin-1"),
                            }

                        source = ChannelExtension(**kargs)
                    else:
                        source = ce_block

                    # compute additional byte offset for large records size
                    if offset > v23c.MAX_UINT16:
                        additional_byte_offset = ceil((offset - v23c.MAX_UINT16) / 8)
                        start_bit_offset = offset - additional_byte_offset * 8
                    else:
                        start_bit_offset = offset
                        additional_byte_offset = 0

                    new_record.append(
                        (
                            samples.dtype,
                            samples.dtype.itemsize,
                            offset // 8,
                            0,
                        )
                    )

                    kargs = {
                        "channel_type": v23c.CHANNEL_TYPE_VALUE,
                        "data_type": s_type,
                        "start_offset": start_bit_offset,
                        "bit_count": s_size,
                        "additional_byte_offset": additional_byte_offset,
                        "block_len": channel_size,
                        "description": description,
                        "version": version,
                    }

                    if s_size < 8:
                        s_size = 8

                    channel = Channel(**kargs)
                    channel.name = name
                    channel.source = source
                    gp_channels.append(channel)

                    size = s_size
                    for dim in shape:
                        size *= dim
                    offset += size

                    self.channels_db.add(name, (dg_cntr, ch_cntr))

                    field_name = field_names.get_unique_name(name)

                    fields.append(samples)
                    types.append((field_name, samples.dtype, shape))

                    gp_dep.append(None)

                    ch_cntr += 1

                for name in names[1:]:
                    samples = signal.samples[name]

                    component_names = []
                    component_samples = []

                    shape = samples.shape[1:]
                    dims = [list(range(size)) for size in shape]

                    for indexes in product(*dims):
                        subarray = samples
                        for idx in indexes:
                            subarray = subarray[:, idx]
                        component_samples.append(subarray)

                        indexes = "".join(f"[{idx}]" for idx in indexes)
                        component_name = f"{name}{indexes}"
                        component_names.append(component_name)

                    # add channel dependency block for composed parent channel
                    sd_nr = len(component_samples)
                    kargs = {"sd_nr": sd_nr}
                    for i, dim in enumerate(shape[::-1]):
                        kargs["dim_{}".format(i)] = dim
                    parent_dep = ChannelDependency(**kargs)
                    gp_dep.append(parent_dep)

                    # source for channel
                    if signal.source:
                        source = signal.source
                        if source.source_type != 2:
                            kargs = {
                                "type": v23c.SOURCE_ECU,
                                "description": source.name.encode("latin-1"),
                                "ECU_identification": source.path.encode("latin-1"),
                            }
                        else:
                            kargs = {
                                "type": v23c.SOURCE_VECTOR,
                                "message_name": source.name.encode("latin-1"),
                                "sender_name": source.path.encode("latin-1"),
                            }

                        source = ChannelExtension(**kargs)

                    else:
                        source = ce_block

                    s_type, s_size = fmt_to_datatype_v3(samples.dtype, ())
                    # compute additional byte offset for large records size
                    if offset > v23c.MAX_UINT16:
                        additional_byte_offset = ceil((offset - v23c.MAX_UINT16) / 8)
                        start_bit_offset = offset - additional_byte_offset * 8
                    else:
                        start_bit_offset = offset
                        additional_byte_offset = 0

                    kargs = {
                        "channel_type": v23c.CHANNEL_TYPE_VALUE,
                        "data_type": s_type,
                        "start_offset": start_bit_offset,
                        "bit_count": s_size,
                        "additional_byte_offset": additional_byte_offset,
                        "block_len": channel_size,
                        "version": version,
                    }

                    if s_size < 8:
                        s_size = 8

                    new_record.append(None)

                    channel = Channel(**kargs)
                    channel.name = name
                    channel.comment = signal.comment
                    channel.source = source
                    gp_channels.append(channel)

                    self.channels_db.add(name, (dg_cntr, ch_cntr))

                    ch_cntr += 1

                    for i, (name, samples) in enumerate(
                        zip(component_names, component_samples)
                    ):

                        if i < sd_nr:
                            dep_pair = dg_cntr, ch_cntr
                            parent_dep.referenced_channels.append(dep_pair)
                            description = b"\0"
                        else:
                            description = f"{signal.name} - axis {name}"
                            description = description.encode("latin-1")

                        s_type, s_size = fmt_to_datatype_v3(samples.dtype, ())
                        shape = samples.shape[1:]

                        # source for channel
                        if signal.source:
                            source = signal.source
                            if source.source_type != 2:
                                kargs = {
                                    "type": v23c.SOURCE_ECU,
                                    "description": source.name.encode("latin-1"),
                                    "ECU_identification": source.path.encode("latin-1"),
                                }
                            else:
                                kargs = {
                                    "type": v23c.SOURCE_VECTOR,
                                    "message_name": source.name.encode("latin-1"),
                                    "sender_name": source.path.encode("latin-1"),
                                }

                            source = ChannelExtension(**kargs)

                        else:
                            source = ce_block

                        # compute additional byte offset for large records size
                        if offset > v23c.MAX_UINT16:
                            additional_byte_offset = ceil(
                                (offset - v23c.MAX_UINT16) / 8
                            )
                            start_bit_offset = offset - additional_byte_offset * 8
                        else:
                            start_bit_offset = offset
                            additional_byte_offset = 0

                        kargs = {
                            "channel_type": v23c.CHANNEL_TYPE_VALUE,
                            "data_type": s_type,
                            "start_offset": start_bit_offset,
                            "bit_count": s_size,
                            "additional_byte_offset": additional_byte_offset,
                            "block_len": channel_size,
                            "description": description,
                            "version": version,
                        }

                        if s_size < 8:
                            s_size = 8

                        new_record.append(
                            (
                                samples.dtype,
                                samples.dtype.itemsize,
                                offset // 8,
                                0,
                            )
                        )

                        channel = Channel(**kargs)
                        channel.name = name
                        channel.source = source
                        gp_channels.append(channel)

                        size = s_size
                        for dim in shape:
                            size *= dim
                        offset += size

                        self.channels_db.add(name, (dg_cntr, ch_cntr))

                        field_name = field_names.get_unique_name(name)

                        fields.append(samples)
                        types.append((field_name, samples.dtype, shape))

                        gp_dep.append(None)

                        ch_cntr += 1

        # channel group
        kargs = {
            "cycles_nr": cycles_nr,
            "samples_byte_nr": offset // 8,
            "ch_nr": ch_cntr,
        }
        if self.version >= "3.30":
            kargs["block_len"] = v23c.CG_POST_330_BLOCK_SIZE
        else:
            kargs["block_len"] = v23c.CG_PRE_330_BLOCK_SIZE
        gp.channel_group = ChannelGroup(**kargs)
        gp.channel_group.comment = comment

        # data group
        if self.version >= "3.20":
            block_len = v23c.DG_POST_320_BLOCK_SIZE
        else:
            block_len = v23c.DG_PRE_320_BLOCK_SIZE
        gp.data_group = DataGroup(block_len=block_len)

        # data block
        types = dtype(types)

        gp.sorted = True

        if signals:
            samples = fromarrays(fields, dtype=types)
        else:
            samples = array([])

        block = samples.tobytes()

        gp.data_location = v23c.LOCATION_TEMPORARY_FILE
        if cycles_nr:
            data_address = tell()
            gp.data_group.data_block_addr = data_address
            size = len(block)
            self._tempfile.write(block)

            gp.data_blocks.append(
                DataBlockInfo(
                    address=data_address,
                    block_type=0,
                    original_size=size,
                    compressed_size=size,
                    param=0,
                )
            )

        else:
            gp.data_location = v23c.LOCATION_TEMPORARY_FILE

        self.virtual_groups_map[dg_cntr] = dg_cntr
        if dg_cntr not in self.virtual_groups:
            self.virtual_groups[dg_cntr] = VirtualChannelGroup()

        virtual_channel_group = self.virtual_groups[dg_cntr]
        virtual_channel_group.groups.append(dg_cntr)
        virtual_channel_group.record_size = gp.channel_group.samples_byte_nr
        virtual_channel_group.cycles_nr = gp.channel_group.cycles_nr

        # data group trigger
        gp.trigger = None

        return dg_cntr

    def _append_dataframe(
        self,
        df: DataFrame,
        comment: str = "",
        units: dict[str, str | bytes] | None = None,
    ) -> None:
        """
        Appends a new data group from a Pandas data frame.
        """
        units = units or {}

        t = df.index
        index_name = df.index.name
        time_name = index_name or "time"

        version = self.version

        timestamps = t

        if self.version >= "3.00":
            channel_size = v23c.CN_DISPLAYNAME_BLOCK_SIZE
        elif self.version >= "2.10":
            channel_size = v23c.CN_LONGNAME_BLOCK_SIZE
        else:
            channel_size = v23c.CN_SHORT_BLOCK_SIZE

        file = self._tempfile
        tell = file.tell

        kargs = {
            "module_nr": 0,
            "module_address": 0,
            "type": v23c.SOURCE_ECU,
            "description": b"Channel inserted by Python Script",
        }
        ce_block = ChannelExtension(**kargs)

        dg_cntr = len(self.groups)

        gp = {}
        gp.channels = gp_channels = []
        gp.channel_dependencies = gp_dep = []
        gp.signal_types = gp_sig_types = []
        gp.string_dtypes = []
        gp.record = record = []

        self.groups.append(gp)

        cycles_nr = len(timestamps)
        fields = []
        types = []
        ch_cntr = 0
        offset = 0
        field_names = UniqueDB()

        if df.shape[0]:
            # conversion for time channel
            kargs = {
                "conversion_type": v23c.CONVERSION_TYPE_NONE,
                "unit": b"s",
                "min_phy_value": timestamps[0] if cycles_nr else 0,
                "max_phy_value": timestamps[-1] if cycles_nr else 0,
            }
            conversion = ChannelConversion(**kargs)
            conversion.unit = "s"
            source = ce_block

            # time channel
            t_type, t_size = fmt_to_datatype_v3(timestamps.dtype, timestamps.shape)
            kargs = {
                "short_name": time_name.encode("latin-1"),
                "channel_type": v23c.CHANNEL_TYPE_MASTER,
                "data_type": t_type,
                "start_offset": 0,
                "min_raw_value": timestamps[0] if cycles_nr else 0,
                "max_raw_value": timestamps[-1] if cycles_nr else 0,
                "bit_count": t_size,
                "block_len": channel_size,
                "version": version,
            }
            channel = Channel(**kargs)
            channel.name = name = time_name
            channel.conversion = conversion
            channel.source = source

            gp_channels.append(channel)

            self.channels_db.add(name, (dg_cntr, ch_cntr))
            self.masters_db[dg_cntr] = 0

            record.append(
                (
                    timestamps.dtype,
                    timestamps.dtype.itemsize,
                    0,
                    0,
                )
            )

            # time channel doesn't have channel dependencies
            gp_dep.append(None)

            fields.append(timestamps)
            types.append((name, timestamps.dtype))
            field_names.add(name)

            offset += t_size
            ch_cntr += 1

            gp_sig_types.append(0)

        for signal in df:

            sig = df[signal]
            name = signal

            sig_type = v23c.SIGNAL_TYPE_SCALAR

            gp_sig_types.append(sig_type)

            new_source = ce_block

            # compute additional byte offset for large records size
            if offset > v23c.MAX_UINT16:
                additional_byte_offset = ceil((offset - v23c.MAX_UINT16) / 8)
                start_bit_offset = offset - additional_byte_offset * 8
            else:
                start_bit_offset = offset
                additional_byte_offset = 0

            s_type, s_size = fmt_to_datatype_v3(sig.dtype, sig.shape)

            kargs = {
                "channel_type": v23c.CHANNEL_TYPE_VALUE,
                "data_type": s_type,
                "min_raw_value": 0,
                "max_raw_value": 0,
                "start_offset": start_bit_offset,
                "bit_count": s_size,
                "additional_byte_offset": additional_byte_offset,
                "block_len": channel_size,
                "version": version,
            }

            if s_size < 8:
                s_size = 8

            channel = Channel(**kargs)
            channel.name = name
            channel.source = new_source
            channel.dtype_fmt = dtype((sig.dtype, sig.shape[1:]))

            record.append(
                (
                    channel.dtype_fmt,
                    channel.dtype_fmt.itemsize,
                    offset // 8,
                    0,
                )
            )

            unit = units.get(name, b"")
            if unit:
                if hasattr(unit, "encode"):
                    unit = unit.encode("latin-1")
                # conversion for time channel
                kargs = {
                    "conversion_type": v23c.CONVERSION_TYPE_NONE,
                    "unit": unit,
                    "min_phy_value": 0,
                    "max_phy_value": 0,
                }
                conversion = ChannelConversion(**kargs)
                conversion.unit = unit

            gp_channels.append(channel)

            offset += s_size

            self.channels_db.add(name, (dg_cntr, ch_cntr))

            field_name = field_names.get_unique_name(name)

            if sig.dtype.kind == "S":
                gp.string_dtypes.append(sig.dtype)

            fields.append(sig)
            types.append((field_name, sig.dtype))

            ch_cntr += 1

            # simple channels don't have channel dependencies
            gp_dep.append(None)

        # channel group
        kargs = {
            "cycles_nr": cycles_nr,
            "samples_byte_nr": offset // 8,
            "ch_nr": ch_cntr,
        }
        if self.version >= "3.30":
            kargs["block_len"] = v23c.CG_POST_330_BLOCK_SIZE
        else:
            kargs["block_len"] = v23c.CG_PRE_330_BLOCK_SIZE
        gp.channel_group = ChannelGroup(**kargs)
        gp.channel_group.comment = comment

        # data group
        if self.version >= "3.20":
            block_len = v23c.DG_POST_320_BLOCK_SIZE
        else:
            block_len = v23c.DG_PRE_320_BLOCK_SIZE
        gp.data_group = DataGroup(block_len=block_len)

        # data block
        types = dtype(types)

        gp.sorted = True

        if df.shape[0]:
            samples = fromarrays(fields, dtype=types)
        else:
            samples = array([])

        block = samples.tobytes()

        gp.data_location = v23c.LOCATION_TEMPORARY_FILE
        if cycles_nr:
            data_address = tell()
            gp.data_group.data_block_addr = data_address
            size = len(block)
            self._tempfile.write(block)

            gp.data_blocks.append(
                DataBlockInfo(
                    address=data_address,
                    block_type=0,
                    original_size=size,
                    compressed_size=size,
                    param=0,
                )
            )
        else:
            gp.data_location = v23c.LOCATION_TEMPORARY_FILE

        self.virtual_groups_map[dg_cntr] = dg_cntr
        if dg_cntr not in self.virtual_groups:
            self.virtual_groups[dg_cntr] = VirtualChannelGroup()

        virtual_channel_group = self.virtual_groups[dg_cntr]
        virtual_channel_group.groups.append(dg_cntr)
        virtual_channel_group.record_size = gp.channel_group.samples_byte_nr
        virtual_channel_group.cycles_nr = gp.channel_group.cycles_nr

        # data group trigger
        gp.trigger = None

    def close(self) -> None:
        """if the MDF was created with memory='minimum' and new
        channels have been appended, then this must be called just before the
        object is not used anymore to clean-up the temporary file

        """

        self._parent = None
        if self._tempfile is not None:
            self._tempfile.close()
        if self._file is not None and not self._from_filelike:
            self._file.close()

        if self.original_name is not None:
            if Path(self.original_name).suffix.lower() in (
                ".bz2",
                ".gzip",
                ".mf4z",
                ".zip",
            ):
                try:
                    os.remove(self.name)
                except:
                    pass

        self._call_back = None
        self.groups.clear()
        self.header = None
        self.identification = None
        self.channels_db.clear()
        self.masters_db.clear()
        self._master_channel_metadata.clear()
        self._si_map.clear()
        self._cc_map.clear()

    def extend(self, index: int, signals: list[tuple[NDArray[Any], None]]) -> None:
        """
        Extend a group with new samples. *signals* contains (values, invalidation_bits)
        pairs for each extended signal. Since MDF3 does not support invalidation
        bits, the second item of each pair must be None. The first pair is the master channel's pair, and the
        next pairs must respect the same order in which the signals were appended. The samples must have raw
        or physical values according to the *Signals* used for the initial append.

        Parameters
        ----------
        index : int
            group index
        signals : list
            list of (numpy.ndarray, None) objects

        Examples
        --------
        >>> # case 1 conversion type None
        >>> s1 = np.array([1, 2, 3, 4, 5])
        >>> s2 = np.array([-1, -2, -3, -4, -5])
        >>> s3 = np.array([0.1, 0.04, 0.09, 0.16, 0.25])
        >>> t = np.array([0.001, 0.002, 0.003, 0.004, 0.005])
        >>> names = ['Positive', 'Negative', 'Float']
        >>> units = ['+', '-', '.f']
        >>> s1 = Signal(samples=s1, timestamps=t, unit='+', name='Positive')
        >>> s2 = Signal(samples=s2, timestamps=t, unit='-', name='Negative')
        >>> s3 = Signal(samples=s3, timestamps=t, unit='flts', name='Floats')
        >>> mdf = MDF3('new.mdf')
        >>> mdf.append([s1, s2, s3], comment='created by asammdf v1.1.0')
        >>> t = np.array([0.006, 0.007, 0.008, 0.009, 0.010])
        >>> mdf2.extend(0, [(t, None), (s1.samples, None), (s2.samples, None), (s3.samples, None)])

        """
        new_group_offset = 0
        gp = self.groups[index]
        if not signals:
            message = '"append" requires a non-empty list of Signal objects'
            raise MdfException(message)

        if gp.data_location == v23c.LOCATION_ORIGINAL_FILE:
            stream = self._file
        else:
            stream = self._tempfile

        canopen_time_fields = ("ms", "days")
        canopen_date_fields = (
            "ms",
            "min",
            "hour",
            "day",
            "month",
            "year",
            "summer_time",
            "day_of_week",
        )

        fields = []
        types = []

        cycles_nr = len(signals[0][0])
        string_counter = 0

        for k_i, ((signal, invalidation_bits), sig_type) in enumerate(
            zip(signals, gp.signal_types)
        ):
            sig = signal
            names = sig.dtype.names

            if sig_type == v23c.SIGNAL_TYPE_SCALAR:
                if signal.dtype.kind == "S":
                    str_dtype = gp.string_dtypes[string_counter]
                    signal = signal.astype(str_dtype)
                    string_counter += 1

                fields.append(signal)

                if signal.shape[1:]:
                    types.append(("", signal.dtype, signal.shape[1:]))
                else:
                    types.append(("", signal.dtype))

            # second, add the composed signals
            elif sig_type in (
                v23c.SIGNAL_TYPE_CANOPEN,
                v23c.SIGNAL_TYPE_STRUCTURE_COMPOSITION,
            ):
                new_group_offset += 1
                new_gp = self.groups[index + new_group_offset]

                new_fields = []
                new_types = []

                names = signal.dtype.names
                for name in names:
                    new_fields.append(signal[name])
                    new_types.append(("", signal.dtype))

                # data block
                new_types = dtype(new_types)

                samples = fromarrays(new_fields, dtype=new_types)
                samples = samples.tobytes()

                record_size = new_gp.channel_group.samples_byte_nr
                extended_size = cycles_nr * record_size

                if samples:
                    stream.seek(0, 2)
                    data_address = stream.tell()
                    stream.write(samples)

                    new_gp.data_blocks.append(
                        DataBlockInfo(
                            address=data_address,
                            original_size=extended_size,
                            compressed_size=extended_size,
                            block_type=0,
                            param=0,
                        )
                    )

            else:

                names = signal.dtype.names

                component_samples = []
                if names:
                    samples = signal[names[0]]
                else:
                    samples = signal

                shape = samples.shape[1:]
                dims = [list(range(size)) for size in shape]

                for indexes in product(*dims):
                    subarray = samples
                    for idx in indexes:
                        subarray = subarray[:, idx]
                    component_samples.append(subarray)

                if names:
                    new_samples = [signal[fld] for fld in names[1:]]
                    component_samples.extend(new_samples)

                for samples in component_samples:
                    shape = samples.shape[1:]

                    fields.append(samples)
                    types.append(("", samples.dtype, shape))

        record_size = gp.channel_group.samples_byte_nr
        extended_size = cycles_nr * record_size

        # data block
        types = dtype(types)

        samples = fromarrays(fields, dtype=types)
        samples = samples.tobytes()

        if cycles_nr:
            stream.seek(0, 2)
            data_address = stream.tell()
            stream.write(samples)
            gp.channel_group.cycles_nr += cycles_nr

            gp.data_blocks.append(
                DataBlockInfo(
                    address=data_address,
                    block_type=0,
                    original_size=extended_size,
                    compressed_size=extended_size,
                    param=0,
                )
            )

        virtual_channel_group = self.virtual_groups[index]
        virtual_channel_group.cycles_nr += cycles_nr

    def get_channel_name(self, group: int, index: int) -> str:
        """Gets channel name.

        Parameters
        ----------
        group : int
            0-based group index
        index : int
            0-based channel index

        Returns
        -------
        name : str
            found channel name

        """
        gp_nr, ch_nr = self._validate_channel_selection(None, group, index)

        grp = self.groups[gp_nr]
        if grp.data_location == v23c.LOCATION_ORIGINAL_FILE:
            stream = self._file
        else:
            stream = self._tempfile

        channel = grp.channels[ch_nr]

        return channel.name

    def get_channel_metadata(
        self,
        name: str | None = None,
        group: int | None = None,
        index: int | None = None,
    ) -> Channel:
        gp_nr, ch_nr = self._validate_channel_selection(name, group, index)

        grp = self.groups[gp_nr]

        if grp.data_location == v23c.LOCATION_ORIGINAL_FILE:
            stream = self._file
        else:
            stream = self._tempfile

        channel = grp.channels[ch_nr]
        channel = deepcopy(channel)

        return channel

    def get_channel_unit(
        self,
        name: str | None = None,
        group: int | None = None,
        index: int | None = None,
    ) -> str:
        """Gets channel unit.

        Channel can be specified in two ways:

        * using the first positional argument *name*

            * if there are multiple occurrences for this channel then the
              *group* and *index* arguments can be used to select a specific
              group.
            * if there are multiple occurrences for this channel and either the
              *group* or *index* arguments is None then a warning is issued

        * using the group number (keyword argument *group*) and the channel
          number (keyword argument *index*). Use *info* method for group and
          channel numbers


        If the *raster* keyword argument is not *None* the output is
        interpolated accordingly.

        Parameters
        ----------
        name : string
            name of channel
        group : int
            0-based group index
        index : int
            0-based channel index

        Returns
        -------
        unit : str
            found channel unit

        """
        gp_nr, ch_nr = self._validate_channel_selection(name, group, index)

        grp = self.groups[gp_nr]
        if grp.data_location == v23c.LOCATION_ORIGINAL_FILE:
            stream = self._file
        else:
            stream = self._tempfile

        channel = grp.channels[ch_nr]

        if channel.conversion:
            unit = channel.conversion.unit
        else:
            unit = ""

        return unit

    def get_channel_comment(
        self,
        name: str | None = None,
        group: int | None = None,
        index: int | None = None,
    ) -> str:
        """Gets channel comment.
        Channel can be specified in two ways:

        * using the first positional argument *name*

            * if there are multiple occurrences for this channel then the
              *group* and *index* arguments can be used to select a specific
              group.
            * if there are multiple occurrences for this channel and either the
              *group* or *index* arguments is None then a warning is issued

        * using the group number (keyword argument *group*) and the channel
          number (keyword argument *index*). Use *info* method for group and
          channel numbers


        If the *raster* keyword argument is not *None* the output is
        interpolated accordingly.

        Parameters
        ----------
        name : string
            name of channel
        group : int
            0-based group index
        index : int
            0-based channel index

        Returns
        -------
        comment : str
            found channel comment

        """
        gp_nr, ch_nr = self._validate_channel_selection(name, group, index)

        grp = self.groups[gp_nr]
        if grp.data_location == v23c.LOCATION_ORIGINAL_FILE:
            stream = self._file
        else:
            stream = self._tempfile

        channel = grp.channels[ch_nr]

        return channel.comment

    @overload
    def get(
        self,
        name: str | None = ...,
        group: int | None = ...,
        index: int | None = ...,
        raster: RasterType | None = ...,
        samples_only: Literal[False] = ...,
        data: bytes | None = ...,
        raw: bool = ...,
        ignore_invalidation_bits: bool = ...,
        record_offset: int = ...,
        record_count: int | None = ...,
        skip_channel_validation: bool = ...,
    ) -> Signal:
        ...

    @overload
    def get(
        self,
        name: str | None = ...,
        group: int | None = ...,
        index: int | None = ...,
        raster: RasterType | None = ...,
        samples_only: Literal[True] = ...,
        data: bytes | None = ...,
        raw: bool = ...,
        ignore_invalidation_bits: bool = ...,
        record_offset: int = ...,
        record_count: int | None = ...,
        skip_channel_validation: bool = ...,
    ) -> tuple[NDArray[Any], None]:
        ...

    def get(
        self,
        name: str | None = None,
        group: int | None = None,
        index: int | None = None,
        raster: RasterType | None = None,
        samples_only: bool = False,
        data: bytes | None = None,
        raw: bool = False,
        ignore_invalidation_bits: bool = False,
        record_offset: int = 0,
        record_count: int | None = None,
        skip_channel_validation: bool = False,
    ) -> Signal | tuple[NDArray[Any], None]:
        """Gets channel samples.
        Channel can be specified in two ways:

        * using the first positional argument *name*

            * if there are multiple occurrences for this channel then the
              *group* and *index* arguments can be used to select a specific
              group.
            * if there are multiple occurrences for this channel and either the
              *group* or *index* arguments is None then a warning is issued

        * using the group number (keyword argument *group*) and the channel
          number (keyword argument *index*). Use *info* method for group and
          channel numbers


        If the *raster* keyword argument is not *None* the output is
        interpolated accordingly.

        Parameters
        ----------
        name : string
            name of channel
        group : int
            0-based group index
        index : int
            0-based channel index
        raster : float
            time raster in seconds
        samples_only : bool
            if *True* return only the channel samples as numpy array; if
            *False* return a *Signal* object
        data : bytes
            prevent redundant data read by providing the raw data group samples
        raw : bool
            return channel samples without applying the conversion rule; default
            `False`
        ignore_invalidation_bits : bool
            only defined to have the same API with the MDF v4
        record_offset : int
            if *data=None* use this to select the record offset from which the
            group data should be loaded
        skip_channel_validation (False) : bool
            skip validation of channel name, group index and channel index; defualt
            *False*. If *True*, the caller has to make sure that the *group* and *index*
            arguments are provided and are correct.

            ..versionadded:: 7.0.0

        Returns
        -------
        res : (numpy.array, None) | Signal
            returns *Signal* if *samples_only*=*False* (default option),
            otherwise returns a (numpy.array, None) tuple (for compatibility
            with MDF v4 class.

            The *Signal* samples are

                * numpy recarray for channels that have CDBLOCK or BYTEARRAY
                  type channels
                * numpy array for all the rest

        Raises
        ------
        MdfException :

        * if the channel name is not found
        * if the group index is out of range
        * if the channel index is out of range

        Examples
        --------
        >>> from asammdf import MDF, Signal
        >>> import numpy as np
        >>> t = np.arange(5)
        >>> s = np.ones(5)
        >>> mdf = MDF(version='3.30')
        >>> for i in range(4):
        ...     sigs = [Signal(s*(i*10+j), t, name='Sig') for j in range(1, 4)]
        ...     mdf.append(sigs)
        ...
        >>> # first group and channel index of the specified channel name
        ...
        >>> mdf.get('Sig')
        UserWarning: Multiple occurrences for channel "Sig". Using first occurrence from data group 4. Provide both "group" and "index" arguments to select another data group
        <Signal Sig:
                samples=[ 1.  1.  1.  1.  1.]
                timestamps=[0 1 2 3 4]
                unit=""
                info=None
                comment="">
        >>> # first channel index in the specified group
        ...
        >>> mdf.get('Sig', 1)
        <Signal Sig:
                samples=[ 11.  11.  11.  11.  11.]
                timestamps=[0 1 2 3 4]
                unit=""
                info=None
                comment="">
        >>> # channel named Sig from group 1 channel index 2
        ...
        >>> mdf.get('Sig', 1, 2)
        <Signal Sig:
                samples=[ 12.  12.  12.  12.  12.]
                timestamps=[0 1 2 3 4]
                unit=""
                info=None
                comment="">
        >>> # channel index 1 or group 2
        ...
        >>> mdf.get(None, 2, 1)
        <Signal Sig:
                samples=[ 21.  21.  21.  21.  21.]
                timestamps=[0 1 2 3 4]
                unit=""
                info=None
                comment="">
        >>> mdf.get(group=2, index=1)
        <Signal Sig:
                samples=[ 21.  21.  21.  21.  21.]
                timestamps=[0 1 2 3 4]
                unit=""
                info=None
                comment="">

        """

        if skip_channel_validation:
            gp_nr, ch_nr = group, index
        else:
            gp_nr, ch_nr = self._validate_channel_selection(name, group, index)

        original_data = data

        grp = self.groups[gp_nr]

        channel = grp.channels[ch_nr]

        conversion = channel.conversion
        name = channel.name
        display_names = channel.display_names

        bit_count = channel.bit_count or 64

        dep = grp.channel_dependencies[ch_nr]
        cycles_nr = grp.channel_group.cycles_nr

        encoding = "latin-1"

        # get data group record
        if data is None:
            data = self._load_data(
                grp, record_offset=record_offset, record_count=record_count
            )
        else:
            data = (data,)

        # check if this is a channel array
        if dep:
            if dep.dependency_type == v23c.DEPENDENCY_TYPE_VECTOR:
                arrays = []
                types = []

                for dg_nr, ch_nr in dep.referenced_channels:

                    sig = self.get(
                        group=dg_nr,
                        index=ch_nr,
                        raw=raw,
                        data=original_data,
                        record_offset=record_offset,
                        record_count=record_count,
                    )

                    arrays.append(sig.samples)
                    types.append((sig.name, sig.samples.dtype))

                vals = fromarrays(arrays, dtype=types)

            elif dep.dependency_type >= v23c.DEPENDENCY_TYPE_NDIM:
                shape = []
                i = 0
                while True:
                    try:
                        dim = dep[f"dim_{i}"]
                        shape.append(dim)
                        i += 1
                    except (KeyError, AttributeError):
                        break
                shape = shape[::-1]

                record_shape = tuple(shape)

                arrays = [
                    self.get(
                        group=dg_nr,
                        index=ch_nr,
                        samples_only=True,
                        raw=raw,
                        data=original_data,
                        record_offset=record_offset,
                        record_count=record_count,
                    )[0]
                    for dg_nr, ch_nr in dep.referenced_channels
                ]
                shape.insert(0, cycles_nr)

                vals = column_stack(arrays).flatten().reshape(tuple(shape))

                arrays = [vals]
                types = [(channel.name, vals.dtype, record_shape)]

                types = dtype(types)
                vals = fromarrays(arrays, dtype=types)

            if not samples_only or raster:
                timestamps = self.get_master(
                    gp_nr,
                    original_data,
                    record_offset=record_offset,
                    record_count=record_count,
                )
                if raster and len(timestamps) > 1:
                    num = float(float32((timestamps[-1] - timestamps[0]) / raster))
                    if num.is_integer():
                        t = linspace(timestamps[0], timestamps[-1], int(num))
                    else:
                        t = arange(timestamps[0], timestamps[-1], raster)

                    vals = (
                        Signal(vals, timestamps, name="_")
                        .interp(
                            t,
                            integer_interpolation_mode=self._integer_interpolation,
                            float_interpolation_mode=self._float_interpolation,
                        )
                        .samples
                    )

                    timestamps = t

        else:
            # get channel values
            channel_values = []
            timestamps = []
            count = 0
            for fragment in data:
                data_bytes, _offset, _count = fragment
                info = grp.record[ch_nr]

                bits = channel.bit_count

                if info is not None:
                    dtype_, byte_size, byte_offset, bit_offset = info

                    buffer = get_channel_raw_bytes(
                        data_bytes,
                        grp.channel_group.samples_byte_nr,
                        byte_offset,
                        byte_size,
                    )

                    vals = frombuffer(buffer, dtype=dtype_)
                    data_type = channel.data_type
                    size = byte_size

                    vals_dtype = vals.dtype.kind
                    if vals_dtype not in "ui" and (bit_offset or not bits == size * 8):
                        vals = self._get_not_byte_aligned_data(data_bytes, grp, ch_nr)
                    else:
                        dtype_ = vals.dtype
                        kind_ = dtype_.kind

                        if data_type in v23c.INT_TYPES:

                            dtype_fmt = get_fmt_v3(
                                data_type, bits, self.identification.byte_order
                            )
                            channel_dtype = dtype(dtype_fmt.split(")")[-1])

                            if channel_dtype.byteorder == "|" and data_type in (
                                v23c.DATA_TYPE_SIGNED_MOTOROLA,
                                v23c.DATA_TYPE_UNSIGNED_MOTOROLA,
                            ):
                                view = f">u{vals.itemsize}"
                            else:
                                view = f"{channel_dtype.byteorder}u{vals.itemsize}"

                            vals = vals.view(view)

                            if bit_offset:
                                vals = vals >> bit_offset

                            if bits != size * 8:
                                if data_type in v23c.SIGNED_INT:
                                    vals = as_non_byte_sized_signed_int(vals, bits)
                                else:
                                    mask = (1 << bits) - 1
                                    vals = vals & mask
                            elif data_type in v23c.SIGNED_INT:
                                view = f"{channel_dtype.byteorder}i{vals.itemsize}"
                                vals = vals.view(view)
                        else:
                            if bits != size * 8:
                                vals = self._get_not_byte_aligned_data(
                                    data_bytes, grp, ch_nr
                                )

                else:
                    vals = self._get_not_byte_aligned_data(data_bytes, grp, ch_nr)

                if not samples_only or raster:
                    timestamps.append(self.get_master(gp_nr, fragment))

                if bits == 1 and self._single_bit_uint_as_bool:
                    vals = array(vals, dtype=bool)

                channel_values.append(vals)
                count += 1

            if count > 1:
                vals = concatenate(channel_values)
            elif count == 1:
                vals = channel_values[0]
            else:
                vals = []

            if not samples_only or raster:
                if count > 1:
                    timestamps = concatenate(timestamps)
                else:
                    timestamps = timestamps[0]

                if raster and len(timestamps) > 1:
                    num = float(float32((timestamps[-1] - timestamps[0]) / raster))
                    if num.is_integer():
                        t = linspace(timestamps[0], timestamps[-1], int(num))
                    else:
                        t = arange(timestamps[0], timestamps[-1], raster)

                    vals = (
                        Signal(vals, timestamps, name="_")
                        .interp(
                            t,
                            integer_interpolation_mode=self._integer_interpolation,
                            float_interpolation_mode=self._float_interpolation,
                        )
                        .samples
                    )

                    timestamps = t

            if not raw:
                if conversion:
                    vals = conversion.convert(vals)
                    conversion = None

        if vals.dtype.kind == "S":
            encoding = "latin-1"

        if samples_only:
            res = vals, None
        else:
            if channel.conversion:
                unit = channel.conversion.unit
            else:
                unit = ""

            comment = channel.comment

            description = channel.description.decode("latin-1").strip(" \t\n\0")
            if comment:
                comment = f"{comment}\n{description}"
            else:
                comment = description

            source = channel.source

            if source:
                source = Source.from_source(source)

            master_metadata = self._master_channel_metadata.get(gp_nr, None)

            res = Signal(
                samples=vals,
                timestamps=timestamps,
                unit=unit,
                name=channel.name,
                comment=comment,
                conversion=conversion,
                raw=raw,
                master_metadata=master_metadata,
                display_names=display_names,
                source=source,
                bit_count=bit_count,
                encoding=encoding,
                group_index=gp_nr,
                channel_index=ch_nr,
            )

        return res

    def get_master(
        self,
        index: int,
        data: bytes | None = None,
        raster: RasterType | None = None,
        record_offset: int = 0,
        record_count: int | None = None,
        one_piece: bool = False,
    ) -> NDArray[Any]:
        """returns master channel samples for given group

        Parameters
        ----------
        index : int
            group index
        data : (bytes, int)
            (data block raw bytes, fragment offset); default None
        raster : float
            raster to be used for interpolation; default None

            .. deprecated:: 5.13.0

        record_offset : int
            if *data=None* use this to select the record offset from which the
            group data should be loaded


        Returns
        -------
        t : numpy.array
            master channel samples

        """
        if self._master is not None:
            return self._master

        if raster is not None:
            PendingDeprecationWarning(
                "the argument raster is deprecated since version 5.13.0 "
                "and will be removed in a future release"
            )

        fragment = data
        if fragment:
            data_bytes, offset, _count = fragment
        else:
            offset = 0

        group = self.groups[index]

        time_ch_nr = self.masters_db.get(index, None)
        cycles_nr = group.channel_group.cycles_nr

        if time_ch_nr is None:
            if fragment:
                count = len(data_bytes) // group.channel_group.samples_byte_nr
            else:
                count = cycles_nr
            t = arange(count, dtype=float64)
            metadata = ("time", 1)
        else:
            time_ch = group.channels[time_ch_nr]

            metadata = (time_ch.name, 1)

            if time_ch.bit_count == 0:
                if time_ch.sampling_rate:
                    sampling_rate = time_ch.sampling_rate
                else:
                    sampling_rate = 1
                t = arange(cycles_nr, dtype=float64) * sampling_rate
            else:

                # get data group record
                if data is None:
                    data = self._load_data(
                        group, record_offset=record_offset, record_count=record_count
                    )
                    _count = record_count
                else:
                    data = (data,)

                time_values = []
                count = 0
                for fragment in data:
                    data_bytes, offset, _count = fragment
                    dtype_, byte_size, byte_offset, bit_offset = group.record[
                        time_ch_nr
                    ]

                    buffer = get_channel_raw_bytes(
                        data_bytes,
                        group.channel_group.samples_byte_nr,
                        byte_offset,
                        byte_size,
                    )

                    t = frombuffer(buffer, dtype=dtype_)

                    time_values.append(t)
                    count += 1

                if count > 1:
                    t = concatenate(time_values)
                elif count == 1:
                    t = time_values[0]
                else:
                    t = array([], dtype=float64)

                if time_ch.data_type in v23c.INT_TYPES:

                    dtype_fmt = get_fmt_v3(
                        time_ch.data_type,
                        time_ch.bit_count,
                        self.identification.byte_order,
                    )
                    channel_dtype = dtype(dtype_fmt.split(")")[-1])

                    if channel_dtype.byteorder == "|" and time_ch.data_type in (
                        v23c.DATA_TYPE_SIGNED_MOTOROLA,
                        v23c.DATA_TYPE_UNSIGNED_MOTOROLA,
                    ):
                        view = f">u{t.itemsize}"
                    else:
                        view = f"{channel_dtype.byteorder}u{t.itemsize}"

                    if bit_offset:
                        t >>= bit_offset

                    if time_ch.bit_count != t.itemsize * 8:
                        if time_ch.data_type in v23c.SIGNED_INT:
                            t = as_non_byte_sized_signed_int(t, time_ch.bit_count)
                        else:
                            mask = (1 << time_ch.bit_count) - 1
                            t &= mask
                    elif time_ch.data_type in v23c.SIGNED_INT:
                        view = f"{channel_dtype.byteorder}i{t.itemsize}"
                        t = t.view(view)

                # get timestamps
                conversion = time_ch.conversion
                if conversion is None:
                    time_conv_type = v23c.CONVERSION_TYPE_NONE
                else:
                    time_conv_type = conversion.conversion_type
                if time_conv_type == v23c.CONVERSION_TYPE_LINEAR:
                    time_a = conversion.a
                    time_b = conversion.b
                    t = t * time_a
                    if time_b:
                        t += time_b

        if not t.dtype == float64:
            t = t.astype(float64)

        self._master_channel_metadata[index] = metadata

        if raster:
            timestamps = t
            if len(t) > 1:
                num = float(float32((timestamps[-1] - timestamps[0]) / raster))
                if int(num) == num:
                    timestamps = linspace(t[0], t[-1], int(num))
                else:
                    timestamps = arange(t[0], t[-1], raster)
        else:
            timestamps = t

        return timestamps

    def iter_get_triggers(self) -> Iterator[TriggerInfoDict]:
        """generator that yields triggers

        Returns
        -------
        trigger_info : dict
            trigger information with the following keys:

                * comment : trigger comment
                * time : trigger time
                * pre_time : trigger pre time
                * post_time : trigger post time
                * index : trigger index
                * group : data group index of trigger
        """
        for i, gp in enumerate(self.groups):
            trigger = gp.trigger
            if trigger:

                for j in range(trigger["trigger_events_nr"]):
                    trigger_info = {
                        "comment": trigger.comment,
                        "index": j,
                        "group": i,
                        "time": trigger[f"trigger_{j}_time"],
                        "pre_time": trigger[f"trigger_{j}_pretime"],
                        "post_time": trigger[f"trigger_{j}_posttime"],
                    }
                    yield trigger_info

    def info(self) -> dict[str, Any]:
        """get MDF information as a dict

        Examples
        --------
        >>> mdf = MDF3('test.mdf')
        >>> mdf.info()

        """
        info = {}
        for key in ("author", "department", "project", "subject"):
            value = self.header[key]
            info[key] = value
        info["version"] = self.version
        info["groups"] = len(self.groups)
        for i, gp in enumerate(self.groups):
            if gp.data_location == v23c.LOCATION_ORIGINAL_FILE:
                stream = self._file
            elif gp.data_location == v23c.LOCATION_TEMPORARY_FILE:
                stream = self._tempfile
            inf = {}
            info[f"group {i}"] = inf
            inf["cycles"] = gp.channel_group.cycles_nr
            inf["comment"] = gp.channel_group.comment
            inf["channels count"] = len(gp.channels)
            for j, channel in enumerate(gp.channels):
                name = channel.name

                if channel.channel_type == v23c.CHANNEL_TYPE_MASTER:
                    ch_type = "master"
                else:
                    ch_type = "value"
                inf[f"channel {j}"] = f'name="{name}" type={ch_type}'

        return info

    def save(
        self,
        dst: StrPathType,
        overwrite: bool = False,
        compression: CompressionType = 0,
    ) -> Path | None:
        """Save MDF to *dst*. If overwrite is *True* then the destination file
        is overwritten, otherwise the file name is appended with '.<cntr>',
        were '<cntr>' is the first counter that produces a new file name (that
        does not already exist in the filesystem).

        Parameters
        ----------
        dst : str | pathlib.Path
            destination file name
        overwrite : bool
            overwrite flag, default *False*
        compression : int
            does nothing for mdf version3; introduced here to share the same
            API as mdf version 4 files

        Returns
        -------
        output_file : str
            output file name

        """

        dst = Path(dst).with_suffix(".mdf")

        destination_dir = dst.parent
        destination_dir.mkdir(parents=True, exist_ok=True)

        if overwrite is False:
            if dst.is_file():
                cntr = 0
                while True:
                    name = dst.with_suffix(f".{cntr}.mdf")
                    if not name.exists():
                        break
                    else:
                        cntr += 1
                message = (
                    f'Destination file "{dst}" already exists '
                    f'and "overwrite" is False. Saving MDF file as "{name}"'
                )
                logger.warning(message)
                dst = name

        if not self.header.comment:
            self.header.comment = """<FHcomment>
<TX>created</TX>
<tool_id>asammdf</tool_id>
<tool_vendor> </tool_vendor>
<tool_version>{__version__}</tool_version>
</FHcomment>"""
        else:

            old_history = self.header.comment
            timestamp = time.asctime()

            text = f"{old_history}\n{timestamp}: updated by asammdf {__version__}"
            self.header.comment = text

        defined_texts, cc_map, si_map = {}, {}, {}

        if dst == self.name:
            destination = dst.with_suffix(".savetemp")
        else:
            destination = dst

        with open(destination, "wb+") as dst_:

            groups_nr = len(self.groups)

            write = dst_.write
            seek = dst_.seek
            # list of all blocks
            blocks = []

            address = 0

            write(bytes(self.identification))
            address += v23c.ID_BLOCK_SIZE

            write(bytes(self.header))
            address += self.header.block_len

            if self.header.program:
                write(bytes(self.header.program))
                self.header.program_addr = address
                address += self.header.program.block_len
            else:
                self.header.program_addr = 0

            comment = TextBlock(text=self.header.comment)
            write(bytes(comment))
            self.header.comment_addr = address
            address += comment.block_len

            # DataGroup
            # put them first in the block list so they will be written first to
            # disk this way, in case of memory=False, we can safely
            # restore he original data block address
            gp_rec_ids = []

            original_data_block_addrs = [
                group.data_group.data_block_addr for group in self.groups
            ]

            for idx, gp in enumerate(self.groups):
                dg = gp.data_group
                gp_rec_ids.append(dg.record_id_len)
                dg.record_id_len = 0

                # DataBlock
                dim = 0
                for (data_bytes, _, __) in self._load_data(gp):
                    dim += len(data_bytes)
                    write(data_bytes)

                if gp.data_blocks:
                    gp.data_group.data_block_addr = address
                else:
                    gp.data_group.data_block_addr = 0
                address += dim

                if self._callback:
                    self._callback(int(33 * (idx + 1) / groups_nr), 100)
                if self._terminate:
                    dst_.close()
                    self.close()
                    return

            for gp in self.groups:
                dg = gp.data_group
                blocks.append(dg)
                dg.address = address
                address += dg.block_len

            if self.groups:
                for i, dg in enumerate(self.groups[:-1]):
                    addr = self.groups[i + 1].data_group.address
                    dg.data_group.next_dg_addr = addr
                self.groups[-1].data_group.next_dg_addr = 0

            for idx, gp in enumerate(self.groups):
                # Channel Dependency
                cd = gp.channel_dependencies
                for dep in cd:
                    if dep:
                        dep.address = address
                        blocks.append(dep)
                        address += dep.block_len

                for channel, dep in zip(gp.channels, gp.channel_dependencies):
                    if dep:
                        channel.component_addr = dep.address = address
                        blocks.append(dep)
                        address += dep.block_len
                    else:
                        channel.component_addr = 0
                    address = channel.to_blocks(
                        address, blocks, defined_texts, cc_map, si_map
                    )

                count = len(gp.channels)
                if count:
                    for i in range(count - 1):
                        gp.channels[i].next_ch_addr = gp.channels[i + 1].address
                    gp.channels[-1].next_ch_addr = 0

                # ChannelGroup
                cg = gp.channel_group
                if gp.channels:
                    cg.first_ch_addr = gp.channels[0].address
                else:
                    cg.first_ch_addr = 0
                cg.next_cg_addr = 0
                address = cg.to_blocks(address, blocks, defined_texts, si_map)

                # TriggerBLock
                trigger = gp.trigger
                if trigger:
                    address = trigger.to_blocks(address, blocks)

                if self._callback:
                    self._callback(int(33 * (idx + 1) / groups_nr) + 33, 100)
                if self._terminate:
                    dst_.close()
                    self.close()
                    return

            # update referenced channels addresses in the channel dependencies
            for gp in self.groups:
                for dep in gp.channel_dependencies:
                    if not dep:
                        continue

                    for i, pair_ in enumerate(dep.referenced_channels):
                        dg_nr, ch_nr = pair_
                        grp = self.groups[dg_nr]
                        ch = grp.channels[ch_nr]
                        dep[f"ch_{i}"] = ch.address
                        dep[f"cg_{i}"] = grp.channel_group.address
                        dep[f"dg_{i}"] = grp.data_group.address

            # DataGroup
            for gp in self.groups:
                gp.data_group.first_cg_addr = gp.channel_group.address
                if gp.trigger:
                    gp.data_group.trigger_addr = gp.trigger.address
                else:
                    gp.data_group.trigger_addr = 0

            if self.groups:
                address = self.groups[0].data_group.address
                self.header.first_dg_addr = address
                self.header.dg_nr = len(self.groups)

            if self._terminate:
                dst_.close()
                self.close()
                return

            if self._callback:
                blocks_nr = len(blocks)
                threshold = blocks_nr / 33
                count = 1
                for i, block in enumerate(blocks):
                    write(bytes(block))
                    if i >= threshold:
                        self._callback(66 + count, 100)
                        count += 1
                        threshold += blocks_nr / 33
            else:
                for block in blocks:
                    write(bytes(block))

            for gp, rec_id, original_address in zip(
                self.groups, gp_rec_ids, original_data_block_addrs
            ):
                gp.data_group.record_id_len = rec_id
                gp.data_group.data_block_addr = original_address

            seek(0)
            write(bytes(self.identification))
            write(bytes(self.header))

        if dst == self.name:
            self.close()
            Path.unlink(self.name)
            Path.rename(destination, self.name)

            self.groups.clear()
            self.header = None
            self.identification = None
            self.channels_db.clear()
            self.masters_db.clear()

            self._tempfile = TemporaryFile(dir=self.temporary_folder)
            self._file = open(self.name, "rb")
            self._read()

        if self._callback:
            self._callback(100, 100)

        if self.name == Path("__new__.mdf"):
            self.name = dst

        return dst

    def _sort(self) -> None:
        if self._file is None:
            return
        common = defaultdict(list)
        for i, group in enumerate(self.groups):

            if group.sorted:
                continue

            if group.data_blocks:
                address = group.data_blocks[0].address

                common[address].append((i, group.channel_group.record_id))

        read = self._file.read
        seek = self._file.seek

        self._tempfile.seek(0, 2)

        tell = self._tempfile.tell
        write = self._tempfile.write

        for address, groups in common.items():

            partial_records = {id_: [] for (_, id_) in groups}

            group = self.groups[groups[0][0]]

            record_id_nr = group.data_group.record_id_len
            cg_size = group.record_size

            for info in group.data_blocks:
                address, size, block_size, block_type, param = (
                    info.address,
                    info.original_size,
                    info.compressed_size,
                    info.block_type,
                    info.param,
                )
                seek(address)
                data = read(block_size)

                size = len(data)
                i = 0
                while i < size:
                    rec_id = data[i]
                    # skip record id
                    i += 1
                    rec_size = cg_size[rec_id]
                    partial_records[rec_id].append(data[i : i + rec_size])
                    # consider the second record ID if it exists
                    if record_id_nr == 2:
                        i += rec_size + 1
                    else:
                        i += rec_size

            for rec_id, new_data in partial_records.items():
                if new_data:
                    new_data = b"".join(new_data)
                    size = len(new_data)

                    address = tell()
                    write(bytes(new_data))
                    block_info = DataBlockInfo(
                        address=address,
                        block_type=0,
                        original_size=size,
                        compressed_size=size,
                        param=0,
                    )
                    partial_records[rec_id] = [block_info]

            for idx, rec_id in groups:
                group = self.groups[idx]

                group.data_location = v23c.LOCATION_TEMPORARY_FILE
                group.set_blocks_info(partial_records[rec_id])
                group.sorted = True

    def included_channels(
        self,
        index: int | None = None,
        channels: ChannelsType | None = None,
        skip_master: bool = True,
        minimal: bool = True,
    ) -> dict[int, dict[int, Sequence[int]]]:

        if channels is None:
            group = self.groups[index]
            gps = {}
            included_channels = set(range(len(group.channels)))
            master_index = self.masters_db.get(index, None)
            if master_index is not None:
                included_channels.remove(master_index)

            for dep in group.channel_dependencies:
                if dep is None:
                    continue
                for gp_nr, ch_nr in dep.referenced_channels:
                    if gp_nr == index:
                        included_channels.remove(ch_nr)

            if included_channels:
                gps[index] = sorted(included_channels)

            result = {index: gps}
        else:
            gps = {}
            for item in channels:
                if isinstance(item, (list, tuple)):
                    if len(item) not in (2, 3):
                        raise MdfException(
                            "The items used for filtering must be strings, "
                            "or they must match the first 3 arguments of the get "
                            "method"
                        )
                    else:
                        group, idx = self._validate_channel_selection(*item)
                        if group not in gps:
                            gps[group] = {idx}
                        else:
                            gps[group].add(idx)
                else:
                    name = item
                    group, idx = self._validate_channel_selection(name)
                    if group not in gps:
                        gps[group] = {idx}
                    else:
                        gps[group].add(idx)

            result = {}

            for group_index, channels in gps.items():
                group = self.groups[group_index]

                channel_dependencies = [
                    group.channel_dependencies[ch_nr] for ch_nr in channels
                ]

                if minimal:

                    for dep in channel_dependencies:
                        if dep is None:
                            continue
                        for gp_nr, ch_nr in dep.referenced_channels:
                            if gp_nr == group_index:
                                try:
                                    channels.remove(ch_nr)
                                except KeyError:
                                    pass

                gp_master = self.masters_db.get(group_index, None)
                if skip_master and gp_master is not None and gp_master in channels:
                    channels.remove(gp_master)

                result[group_index] = {group_index: sorted(channels)}

        return result

    def _yield_selected_signals(
        self,
        index: int,
        groups: dict[int, Sequence[int]] | None = None,
        record_offset: int = 0,
        record_count: int | None = None,
        skip_master: bool = True,
        version: str = "4.20",
    ) -> Iterator[Signal | tuple[NDArray[Any], None]]:

        if groups is None:
            groups = self.included_channels(index)[index]

        channels = groups[index]

        group = self.groups[index]

        encodings = [
            None,
        ]

        self._set_temporary_master(None)

        for idx, fragment in enumerate(
            self._load_data(
                group, record_offset=record_offset, record_count=record_count
            )
        ):

            self._set_temporary_master(self.get_master(index, data=fragment))

            self._prepare_record(group)

            # the first fragment triggers and append that will add the
            # metadata for all channels
            if idx == 0:
                signals = [
                    self.get(
                        group=index,
                        index=channel_index,
                        data=fragment,
                        raw=True,
                        ignore_invalidation_bits=True,
                        samples_only=False,
                    )
                    for channel_index in channels
                ]
            else:
                signals = [(self._master, None)]

                for channel_index in channels:
                    signals.append(
                        self.get(
                            group=index,
                            index=channel_index,
                            data=fragment,
                            raw=True,
                            ignore_invalidation_bits=True,
                            samples_only=True,
                        )
                    )

            if version < "4.00":
                if idx == 0:
                    for sig, channel_index in zip(signals, channels):
                        if sig.samples.dtype.kind == "S":
                            encodings.append(sig.encoding)
                            strsig = self.get(
                                group=index,
                                index=channel_index,
                                samples_only=True,
                                ignore_invalidation_bits=True,
                            )[0]
                            sig.samples = sig.samples.astype(strsig.dtype)
                            del strsig
                            if sig.encoding != "latin-1":

                                if sig.encoding == "utf-16-le":
                                    sig.samples = (
                                        sig.samples.view(uint16)
                                        .byteswap()
                                        .view(sig.samples.dtype)
                                    )
                                    sig.samples = encode(
                                        decode(sig.samples, "utf-16-be"), "latin-1"
                                    )
                                else:
                                    sig.samples = encode(
                                        decode(sig.samples, sig.encoding), "latin-1"
                                    )
                        else:
                            encodings.append(None)
                else:
                    for i, (sig, encoding) in enumerate(zip(signals, encodings)):

                        if encoding:
                            samples = sig[0]
                            if encoding != "latin-1":

                                if encoding == "utf-16-le":
                                    samples = (
                                        samples.view(uint16)
                                        .byteswap()
                                        .view(samples.dtype)
                                    )
                                    samples = encode(
                                        decode(samples, "utf-16-be"), "latin-1"
                                    )
                                else:
                                    samples = encode(
                                        decode(samples, encoding), "latin-1"
                                    )
                                signals[i] = (samples, sig[1])

            self._set_temporary_master(None)
            yield signals class MDF4(MDF_Common):
    """The *header* attibute is a *HeaderBlock*.

    The *groups* attribute is a list of dicts, each one with the following keys:

    * ``data_group`` - DataGroup object
    * ``channel_group`` - ChannelGroup object
    * ``channels`` - list of Channel objects with the same order as found in the mdf file
    * ``channel_dependencies`` - list of *ChannelArrayBlock* in case of channel arrays;
      list of Channel objects in case of structure channel composition
    * ``data_block`` - address of data block
    * ``data_location``- integer code for data location (original file, temporary file or
      memory)
    * ``data_block_addr`` - list of raw samples starting addresses
    * ``data_block_type`` - list of codes for data block type
    * ``data_block_size`` - list of raw samples block size
    * ``sorted`` - sorted indicator flag
    * ``record_size`` - dict that maps record ID's to record sizes in bytes (including invalidation bytes)
    * ``param`` - row size used for transposition, in case of transposed zipped blocks


    Parameters
    ----------
    name : string
        mdf file name (if provided it must be a real file name) or
        file-like object

    version : string
        mdf file version ('4.00', '4.10', '4.11', '4.20'); default '4.10'

    kwargs :

    callback : function
        keyword only argument: function to call to update the progress; the
        function must accept two arguments (the current progress and maximum
        progress value)
    use_display_names (True) : bool
        keyword only argument: for MDF4 files parse the XML channel comment to
        search for the display name; XML parsing is quite expensive so setting
        this to *False* can decrease the loading times very much; default
        *True*
    remove_source_from_channel_names (True) : bool

    copy_on_get (True) : bool
        copy channel values (np.array) to avoid high memory usage
    compact_vlsd (False) : bool
        use slower method to save the exact sample size for VLSD channels
    column_storage (True) : bool
        use column storage for MDF version >= 4.20
    password : bytes | str
        use this password to decode encrypted attachments

    Attributes
    ----------
    attachments : list
        list of file attachments
    channels_db : dict
        used for fast channel access by name; for each name key the value is a
        list of (group index, channel index) tuples
    events : list
        list event blocks
    file_comment : TextBlock
        file comment TextBlock
    file_history : list
        list of (FileHistory, TextBlock) pairs
    groups : list
        list of data group dicts
    header : HeaderBlock
        mdf file header
    identification : FileIdentificationBlock
        mdf file start block
    last_call_info : dict | None
        a dict to hold information about the last called method.

        .. versionadded:: 5.12.0

    masters_db : dict
        used for fast master channel access; for each group index key the value
         is the master channel index
    name : string
        mdf file name
    version : str
        mdf version

    """

    _terminate = False

    def __init__(
        self,
        name: BufferedReader | BytesIO | StrPathType | None = None,
        version: str = "4.10",
        channels: list[str] | None = None,
        **kwargs,
    ) -> None:

        self._kwargs = kwargs
        self.original_name = kwargs["original_name"]
        self.groups = []
        self.header = None
        self.identification = None
        self.file_history = []
        self.channels_db = ChannelsDB()
        self.masters_db = {}
        self.attachments = []
        self._attachments_cache = {}
        self.file_comment = None
        self.events = []
        self.bus_logging_map = {"CAN": {}, "ETHERNET": {}, "FLEXRAY": {}, "LIN": {}}

        self._attachments_map = {}
        self._ch_map = {}
        self._master_channel_metadata = {}
        self._invalidation_cache = {}
        self._external_dbc_cache = {}
        self._si_map = {}
        self._file_si_map = {}
        self._cc_map = {}
        self._file_cc_map = {}
        self._cg_map = {}
        self._cn_data_map = {}
        self._dbc_cache = {}
        self._interned_strings = {}

        self.temporary_folder = kwargs.get("temporary_folder", None)

        if channels is None:
            self.load_filter = set()
            self.use_load_filter = False
        else:
            self.load_filter = set(channels)
            self.use_load_filter = True

        self._tempfile = TemporaryFile(dir=self.temporary_folder)
        self._file = None

        self._read_fragment_size = get_global_option("read_fragment_size")
        self._write_fragment_size = get_global_option("write_fragment_size")
        self._single_bit_uint_as_bool = get_global_option("single_bit_uint_as_bool")
        self._integer_interpolation = get_global_option("integer_interpolation")
        self._float_interpolation = get_global_option("float_interpolation")
        self._raise_on_multiple_occurrences = kwargs.get(
            "raise_on_multiple_occurrences",
            get_global_option("raise_on_multiple_occurrences"),
        )
        self._use_display_names = kwargs.get(
            "use_display_names", get_global_option("use_display_names")
        )

        self._remove_source_from_channel_names = kwargs.get(
            "remove_source_from_channel_names", False
        )
        self._password = kwargs.get("password", None)
        self._force_attachment_encryption = kwargs.get(
            "force_attachment_encryption", False
        )
        self.copy_on_get = kwargs.get("copy_on_get", True)
        self.compact_vlsd = kwargs.get("compact_vlsd", False)

        self.virtual_groups = {}  # master group 2 referencing groups
        self.virtual_groups_map = {}  # group index 2 master group

        self._master = None

        self.last_call_info = None

        # make sure no appended block has the address 0
        self._tempfile.write(b"\0")

        self._callback = kwargs.get("callback", None)

        self._delete_on_close = False
        self._mapped_file = None

        if name:
            if is_file_like(name):
                self._file = name
                self.name = self.original_name = Path("From_FileLike.mf4")
                self._from_filelike = True
                self._read(mapped=False)
            else:

                with open(name, "rb") as stream:
                    identification = FileIdentificationBlock(stream=stream)
                    version = identification["version_str"]
                    version = version.decode("utf-8").strip(" \n\t\0")
                    flags = identification["unfinalized_standard_flags"]

                if version >= "4.10" and flags:
                    tmpdir = Path(gettempdir())
                    self.name = tmpdir / Path(name).name
                    shutil.copy(name, self.name)
                    self._file = open(self.name, "rb+")
                    self._from_filelike = False
                    self._delete_on_close = True
                    self._read(mapped=False)
                else:

                    if sys.maxsize < 2**32:
                        self.name = Path(name)
                        self._file = open(self.name, "rb")
                        self._from_filelike = False
                        self._read(mapped=False)
                    else:
                        self.name = Path(name)
                        self._mapped_file = open(self.name, "rb")
                        self._file = mmap.mmap(
                            self._mapped_file.fileno(), 0, access=mmap.ACCESS_READ
                        )
                        self._from_filelike = False
                        self._read(mapped=True)

                        # self._file.close()
                        # x.close()
                        #
                        # self._file = open(self.name, "rb")

        else:
            self._from_filelike = False
            version = validate_version_argument(version)
            self.header = HeaderBlock()
            self.identification = FileIdentificationBlock(version=version)
            self.version = version
            self.name = Path("__new__.mf4")

        if self.version >= "4.20":
            self._column_storage = kwargs.get("column_storage", True)
        else:
            self._column_storage = False

        self._parent = None

    def __del__(self) -> None:
        self.close()

    def _check_finalised(self) -> int:
        flags = self.identification["unfinalized_standard_flags"]

        if flags & 1:
            message = (
                f"Unfinalised file {self.name}:"
                " Update of cycle counters for CG/CA blocks required"
            )

            logger.info(message)
        if flags & 1 << 1:
            message = f"Unfinalised file {self.name}: Update of cycle counters for SR blocks required"

            logger.info(message)
        if flags & 1 << 2:
            message = f"Unfinalised file {self.name}: Update of length for last DT block required"

            logger.info(message)
        if flags & 1 * 8:
            message = f"Unfinalised file {self.name}: Update of length for last RD block required"

            logger.info(message)
        if flags & 1 << 4:
            message = (
                f"Unfinalised file {self.name}:"
                " Update of last DL block in each chained list"
                " of DL blocks required"
            )

            logger.info(message)
        if flags & 1 << 5:
            message = (
                f"Unfinalised file {self.name}:"
                " Update of cg_data_bytes and cg_inval_bytes"
                " in VLSD CG block required"
            )

            logger.info(message)
        if flags & 1 << 6:
            message = (
                f"Unfinalised file {self.name}:"
                " Update of offset values for VLSD channel required"
                " in case a VLSD CG block is used"
            )

            logger.info(message)

        return flags

    def _read(self, mapped: bool = False) -> None:

        stream = self._file
        self._mapped = mapped
        dg_cntr = 0

        stream.seek(0, 2)
        self.file_limit = stream.tell()
        stream.seek(0)

        cg_count, _ = count_channel_groups(stream)
        if self._callback:
            self._callback(0, cg_count)
        current_cg_index = 0

        self.identification = FileIdentificationBlock(stream=stream, mapped=mapped)
        version = self.identification["version_str"]
        self.version = version.decode("utf-8").strip(" \n\t\0")

        if self.version >= "4.10":
            # Check for finalization past version 4.10
            finalisation_flags = self._check_finalised()

            if finalisation_flags:
                message = f"Attempting finalization of {self.name}"
                logger.info(message)
                self._finalize()
                self._mapped = mapped = False

        stream = self._file

        self.header = HeaderBlock(address=0x40, stream=stream, mapped=mapped)

        # read file history
        fh_addr = self.header["file_history_addr"]
        while fh_addr:
            if (fh_addr + v4c.FH_BLOCK_SIZE) > self.file_limit:
                logger.warning(
                    f"File history address {fh_addr:X} is outside the file size {self.file_limit}"
                )
                break
            history_block = FileHistory(address=fh_addr, stream=stream, mapped=mapped)
            self.file_history.append(history_block)
            fh_addr = history_block.next_fh_addr

        # read attachments
        at_addr = self.header["first_attachment_addr"]
        index = 0
        while at_addr:
            if (at_addr + v4c.AT_COMMON_SIZE) > self.file_limit:
                logger.warning(
                    f"Attachment address {at_addr:X} is outside the file size {self.file_limit}"
                )
                break
            at_block = AttachmentBlock(address=at_addr, stream=stream, mapped=mapped)
            self._attachments_map[at_addr] = index
            self.attachments.append(at_block)
            at_addr = at_block.next_at_addr
            index += 1

        # go to first date group and read each data group sequentially
        dg_addr = self.header.first_dg_addr

        while dg_addr:
            if (dg_addr + v4c.DG_BLOCK_SIZE) > self.file_limit:
                logger.warning(
                    f"Data group address {dg_addr:X} is outside the file size {self.file_limit}"
                )
                break
            new_groups = []
            group = DataGroup(address=dg_addr, stream=stream, mapped=mapped)
            record_id_nr = group.record_id_len

            # go to first channel group of the current data group
            cg_addr = first_cg_addr = group.first_cg_addr

            cg_nr = 0

            cg_size = {}

            while cg_addr:
                if (cg_addr + v4c.CG_BLOCK_SIZE) > self.file_limit:
                    logger.warning(
                        f"Channel group address {cg_addr:X} is outside the file size {self.file_limit}"
                    )
                    break
                cg_nr += 1

                if cg_addr == first_cg_addr:
                    grp = Group(group)
                else:
                    grp = Group(group.copy())

                # read each channel group sequentially
                block = ChannelGroup(
                    address=cg_addr,
                    stream=stream,
                    mapped=mapped,
                    si_map=self._si_map,
                    version=self.version,
                    tx_map=self._interned_strings,
                )
                self._cg_map[cg_addr] = dg_cntr
                channel_group = grp.channel_group = block

                grp.record_size = cg_size

                if channel_group.flags & v4c.FLAG_CG_VLSD:
                    # VLDS flag
                    record_id = channel_group.record_id
                    cg_size[record_id] = 0
                elif channel_group.flags & v4c.FLAG_CG_BUS_EVENT:
                    samples_size = channel_group.samples_byte_nr
                    inval_size = channel_group.invalidation_bytes_nr
                    record_id = channel_group.record_id
                    cg_size[record_id] = samples_size + inval_size
                else:
                    # in case no `cg_flags` are set
                    samples_size = channel_group.samples_byte_nr
                    inval_size = channel_group.invalidation_bytes_nr
                    record_id = channel_group.record_id
                    cg_size[record_id] = samples_size + inval_size

                if record_id_nr:
                    grp.sorted = False
                else:
                    grp.sorted = True

                # go to first channel of the current channel group
                ch_addr = channel_group.first_ch_addr
                ch_cntr = 0

                # Read channels by walking recursively in the channel group
                # starting from the first channel
                self._read_channels(
                    ch_addr, grp, stream, dg_cntr, ch_cntr, mapped=mapped
                )

                cg_addr = channel_group.next_cg_addr

                dg_cntr += 1

                current_cg_index += 1
                if self._callback:
                    self._callback(current_cg_index, cg_count)

                if self._terminate:
                    self.close()
                    return

                new_groups.append(grp)

            # store channel groups record sizes dict in each
            # new group data belong to the initial unsorted group, and add
            # the key 'sorted' with the value False to use a flag;

            address = group.data_block_addr

            total_size = 0
            inval_total_size = 0
            block_type = b"##DT"

            for new_group in new_groups:

                channel_group = new_group.channel_group
                if channel_group.flags & v4c.FLAG_CG_REMOTE_MASTER:
                    block_type = b"##DV"
                    total_size += (
                        channel_group.samples_byte_nr * channel_group.cycles_nr
                    )
                    inval_total_size += (
                        channel_group.invalidation_bytes_nr * channel_group.cycles_nr
                    )
                else:
                    block_type = b"##DT"
                    total_size += (
                        channel_group.samples_byte_nr
                        + channel_group.invalidation_bytes_nr
                    ) * channel_group.cycles_nr

            if (
                self.identification["unfinalized_standard_flags"]
                & v4c.FLAG_UNFIN_UPDATE_CG_COUNTER
            ):
                total_size = int(10**12)
                inval_total_size = int(10**12)

            data_blocks_info = self._get_data_blocks_info(
                address=address,
                stream=stream,
                block_type=block_type,
                mapped=mapped,
                total_size=total_size,
                inval_total_size=inval_total_size,
            )
            data_blocks = []
            uses_ld = self._uses_ld(
                address=address,
                stream=stream,
                block_type=block_type,
                mapped=mapped,
            )

            for grp in new_groups:
                grp.data_location = v4c.LOCATION_ORIGINAL_FILE
                grp.data_blocks_info_generator = data_blocks_info
                grp.data_blocks = data_blocks
                grp.uses_ld = uses_ld
                self._prepare_record(grp)

            self.groups.extend(new_groups)

            dg_addr = group.next_dg_addr

        # all channels have been loaded so now we can link the
        # channel dependencies and load the signal data for VLSD channels
        for gp_index, grp in enumerate(self.groups):

            if (
                self.version >= "4.20"
                and grp.channel_group.flags & v4c.FLAG_CG_REMOTE_MASTER
            ):
                grp.channel_group.cg_master_index = self._cg_map[
                    grp.channel_group.cg_master_addr
                ]
                index = grp.channel_group.cg_master_index

            else:
                index = gp_index

            self.virtual_groups_map[gp_index] = index
            if index not in self.virtual_groups:
                self.virtual_groups[index] = VirtualChannelGroup()

            virtual_channel_group = self.virtual_groups[index]
            virtual_channel_group.groups.append(gp_index)
            virtual_channel_group.record_size += (
                grp.channel_group.samples_byte_nr
                + grp.channel_group.invalidation_bytes_nr
            )
            virtual_channel_group.cycles_nr = grp.channel_group.cycles_nr

            for ch_index, dep_list in enumerate(grp.channel_dependencies):
                if not dep_list:
                    continue

                for dep in dep_list:
                    if isinstance(dep, ChannelArrayBlock):

                        if dep.flags & v4c.FLAG_CA_DYNAMIC_AXIS:
                            for i in range(dep.dims):
                                ch_addr = dep[f"dynamic_size_{i}_ch_addr"]
                                if ch_addr:
                                    ref_channel = self._ch_map[ch_addr]
                                    dep.dynamic_size_channels.append(ref_channel)
                                else:
                                    dep.dynamic_size_channels.append(None)

                        if dep.flags & v4c.FLAG_CA_INPUT_QUANTITY:
                            for i in range(dep.dims):
                                ch_addr = dep[f"input_quantity_{i}_ch_addr"]
                                if ch_addr:
                                    ref_channel = self._ch_map[ch_addr]
                                    dep.input_quantity_channels.append(ref_channel)
                                else:
                                    dep.input_quantity_channels.append(None)

                        if dep.flags & v4c.FLAG_CA_OUTPUT_QUANTITY:
                            ch_addr = dep["output_quantity_ch_addr"]
                            if ch_addr:
                                ref_channel = self._ch_map[ch_addr]
                                dep.output_quantity_channel = ref_channel
                            else:
                                dep.output_quantity_channel = None

                        if dep.flags & v4c.FLAG_CA_COMPARISON_QUANTITY:
                            ch_addr = dep["comparison_quantity_ch_addr"]
                            if ch_addr:
                                ref_channel = self._ch_map[ch_addr]
                                dep.comparison_quantity_channel = ref_channel
                            else:
                                dep.comparison_quantity_channel = None

                        if dep.flags & v4c.FLAG_CA_AXIS:
                            for i in range(dep.dims):
                                cc_addr = dep[f"axis_conversion_{i}"]
                                if cc_addr:
                                    conv = ChannelConversion(
                                        stream=stream,
                                        address=cc_addr,
                                        mapped=mapped,
                                        tx_map={},
                                    )
                                    dep.axis_conversions.append(conv)
                                else:
                                    dep.axis_conversions.append(None)

                        if (dep.flags & v4c.FLAG_CA_AXIS) and not (
                            dep.flags & v4c.FLAG_CA_FIXED_AXIS
                        ):
                            for i in range(dep.dims):
                                ch_addr = dep[f"scale_axis_{i}_ch_addr"]
                                if ch_addr:
                                    ref_channel = self._ch_map[ch_addr]
                                    dep.axis_channels.append(ref_channel)
                                else:
                                    dep.axis_channels.append(None)
                    else:
                        break

        self._sort()

        for grp in self.groups:
            channels = grp.channels
            if (
                len(channels) == 1
                and channels[0].dtype_fmt.itemsize == grp.channel_group.samples_byte_nr
            ):
                grp.single_channel_dtype = channels[0].dtype_fmt

        self._process_bus_logging()

        # read events
        addr = self.header.first_event_addr
        ev_map = {}
        event_index = 0
        while addr:
            if (addr + v4c.COMMON_SIZE) > self.file_limit:
                logger.warning(
                    f"Event address {addr:X} is outside the file size {self.file_limit}"
                )
                break
            event = EventBlock(address=addr, stream=stream, mapped=mapped)
            event.update_references(self._ch_map, self._cg_map)
            self.events.append(event)
            ev_map[addr] = event_index
            event_index += 1

            addr = event.next_ev_addr

        for event in self.events:
            addr = event.parent_ev_addr
            if addr:
                parent = ev_map.get(addr, None)
                if parent is not None:
                    event.parent = parent
                else:
                    event.parent = None

            addr = event.range_start_ev_addr
            if addr:
                range_start_ev_addr = ev_map.get(addr, None)
                if range_start_ev_addr is not None:
                    event.parent = range_start_ev_addr
                else:
                    event.parent = None

        self._si_map.clear()
        self._ch_map.clear()
        self._cc_map.clear()

        self._interned_strings.clear()
        self._attachments_map.clear()

        self.progress = cg_count, cg_count

    def _read_channels(
        self,
        ch_addr: int,
        grp: Group,
        stream: ReadableBufferType,
        dg_cntr: int,
        ch_cntr: int,
        channel_composition: bool = False,
        mapped: bool = False,
    ) -> tuple[int, list[tuple[int, int]] | None, dtype | None]:

        filter_channels = self.use_load_filter
        use_display_names = self._use_display_names

        channels = grp.channels

        dependencies = grp.channel_dependencies

        unique_names = UniqueDB()

        if channel_composition:
            composition = []
            composition_channels = []

        if grp.channel_group.path_separator:
            path_separator = chr(grp.channel_group.path_separator)
        else:
            path_separator = "\\"

        while ch_addr:
            # read channel block and create channel object

            if (ch_addr + v4c.COMMON_SIZE) > self.file_limit:
                logger.warning(
                    f"Channel address {ch_addr:X} is outside the file size {self.file_limit}"
                )
                break

            if filter_channels:
                if mapped:
                    (
                        id_,
                        links_nr,
                        next_ch_addr,
                        component_addr,
                        name_addr,
                        comment_addr,
                    ) = v4c.CHANNEL_FILTER_uf(stream, ch_addr)
                    channel_type = stream[ch_addr + v4c.COMMON_SIZE + links_nr * 8]
                    name = get_text_v4(name_addr, stream, mapped=mapped)
                    if use_display_names:
                        comment = get_text_v4(comment_addr, stream, mapped=mapped)
                        display_names = extract_display_names(comment)
                    else:
                        display_names = {}
                        comment = None

                else:
                    stream.seek(ch_addr)
                    (
                        id_,
                        links_nr,
                        next_ch_addr,
                        component_addr,
                        name_addr,
                        comment_addr,
                    ) = v4c.CHANNEL_FILTER_u(stream.read(v4c.CHANNEL_FILTER_SIZE))
                    stream.seek(ch_addr + v4c.COMMON_SIZE + links_nr * 8)
                    channel_type = stream.read(1)[0]
                    name = get_text_v4(name_addr, stream, mapped=mapped)

                    if use_display_names:
                        comment = get_text_v4(comment_addr, stream, mapped=mapped)
                        display_names = extract_display_names(comment)
                    else:
                        display_names = {}
                        comment = None

                if id_ != b"##CN":
                    message = f'Expected "##CN" block @{hex(ch_addr)} but found "{id_}"'
                    raise MdfException(message)

                if (
                    channel_composition
                    or channel_type in v4c.MASTER_TYPES
                    or name in self.load_filter
                    or (
                        use_display_names
                        and any(
                            dsp_name in self.load_filter for dsp_name in display_names
                        )
                    )
                ):
                    if comment is None:
                        comment = get_text_v4(comment_addr, stream, mapped=mapped)
                    channel = Channel(
                        address=ch_addr,
                        stream=stream,
                        cc_map=self._cc_map,
                        si_map=self._si_map,
                        at_map=self._attachments_map,
                        use_display_names=use_display_names,
                        mapped=mapped,
                        tx_map=self._interned_strings,
                        file_limit=self.file_limit,
                        parsed_strings=(name, display_names, comment),
                    )

                elif not component_addr:
                    ch_addr = next_ch_addr
                    continue
                else:
                    if (component_addr + v4c.CC_ALG_BLOCK_SIZE) > self.file_limit:
                        logger.warning(
                            f"Channel component address {component_addr:X} is outside the file size {self.file_limit}"
                        )
                        break
                    # check if it is a CABLOCK or CNBLOCK
                    stream.seek(component_addr)
                    blk_id = stream.read(4)
                    if blk_id == b"##CN":
                        (ch_cntr, _1, _2,) = self._read_channels(
                            component_addr,
                            grp,
                            stream,
                            dg_cntr,
                            ch_cntr,
                            False,
                            mapped=mapped,
                        )

                    ch_addr = next_ch_addr
                    continue

            else:
                channel = Channel(
                    address=ch_addr,
                    stream=stream,
                    cc_map=self._cc_map,
                    si_map=self._si_map,
                    at_map=self._attachments_map,
                    use_display_names=use_display_names,
                    mapped=mapped,
                    tx_map=self._interned_strings,
                    file_limit=self.file_limit,
                    parsed_strings=None,
                )

            if channel.channel_type == v4c.CHANNEL_TYPE_SYNC:
                channel.attachment = self._attachments_map.get(
                    channel.data_block_addr,
                    None,
                )

            if self._remove_source_from_channel_names:
                channel.name = channel.name.split(path_separator, 1)[0]
                channel.display_names = {
                    _name.split(path_separator, 1)[0]: val
                    for _name, val in channel.display_names.items()
                }

            entry = (dg_cntr, ch_cntr)
            self._ch_map[ch_addr] = entry

            channels.append(channel)
            if channel_composition:
                composition.append(entry)
                composition_channels.append(channel)

            for _name in channel.display_names:
                self.channels_db.add(_name, entry)
            self.channels_db.add(channel.name, entry)

            # signal data
            cn_data_addr = channel.data_block_addr
            if cn_data_addr:
                grp.signal_data.append(
                    ([], self._get_signal_data_blocks_info(cn_data_addr, stream))
                )
            else:
                grp.signal_data.append(None)

            if cn_data_addr:
                self._cn_data_map[cn_data_addr] = entry

            if channel.channel_type in MASTER_CHANNELS:
                self.masters_db[dg_cntr] = ch_cntr

            ch_cntr += 1

            component_addr = channel.component_addr

            if component_addr:

                if (component_addr + 4) > self.file_limit:
                    logger.warning(
                        f"Channel component address {component_addr:X} is outside the file size {self.file_limit}"
                    )
                    break
                # check if it is a CABLOCK or CNBLOCK
                stream.seek(component_addr)
                blk_id = stream.read(4)
                if blk_id == b"##CN":
                    index = ch_cntr - 1
                    dependencies.append(None)
                    (
                        ch_cntr,
                        ret_composition,
                        ret_composition_dtype,
                    ) = self._read_channels(
                        component_addr,
                        grp,
                        stream,
                        dg_cntr,
                        ch_cntr,
                        True,
                        mapped=mapped,
                    )
                    dependencies[index] = ret_composition

                    channel.dtype_fmt = ret_composition_dtype

                else:
                    # only channel arrays with storage=CN_TEMPLATE are
                    # supported so far
                    first_dep = ca_block = ChannelArrayBlock(
                        address=component_addr, stream=stream, mapped=mapped
                    )
                    if ca_block.storage != v4c.CA_STORAGE_TYPE_CN_TEMPLATE:
                        logger.warning("Only CN template arrays are supported")
                    ca_list = [ca_block]
                    while ca_block.composition_addr:
                        ca_block = ChannelArrayBlock(
                            address=ca_block.composition_addr,
                            stream=stream,
                            mapped=mapped,
                        )
                        ca_list.append(ca_block)
                    dependencies.append(ca_list)

                    channel.dtype_fmt = dtype(
                        get_fmt_v4(
                            channel.data_type,
                            channel.bit_offset + channel.bit_count,
                            channel.channel_type,
                        )
                    )

            else:
                dependencies.append(None)

                channel.dtype_fmt = dtype(
                    get_fmt_v4(
                        channel.data_type,
                        channel.bit_offset + channel.bit_count,
                        channel.channel_type,
                    )
                )

            # go to next channel of the current channel group
            ch_addr = channel.next_ch_addr

        if channel_composition:
            composition_channels.sort()
            composition_dtype = dtype(
                [
                    (unique_names.get_unique_name(channel.name), channel.dtype_fmt)
                    for channel in composition_channels
                ]
            )

        else:
            composition = None
            composition_dtype = None

        return ch_cntr, composition, composition_dtype

    def _load_signal_data(
        self,
        group: Group | None = None,
        index: int | None = None,
        start_offset: int | None = None,
        end_offset: int | None = None,
    ) -> bytes:
        """this method is used to get the channel signal data, usually for
        VLSD channels

        Parameters
        ----------
        address : int
            address of referenced block
        stream : handle
            file IO stream handle

        Returns
        -------
        data : bytes
            signal data bytes

        """

        data = []

        if group is not None and index is not None:
            info_blocks = group.signal_data[index]

            if info_blocks is not None:

                if start_offset is None and end_offset is None:

                    for info in group.get_signal_data_blocks(index):
                        address, original_size, compressed_size, block_type, param = (
                            info.address,
                            info.original_size,
                            info.compressed_size,
                            info.block_type,
                            info.param,
                        )

                        if not info.original_size:
                            continue
                        if info.location == v4c.LOCATION_TEMPORARY_FILE:
                            stream = self._tempfile
                        else:
                            stream = self._file

                        stream.seek(address)
                        new_data = stream.read(compressed_size)
                        if block_type == v4c.DZ_BLOCK_DEFLATE:
                            new_data = decompress(new_data, bufsize=original_size)
                        elif block_type == v4c.DZ_BLOCK_TRANSPOSED:
                            new_data = decompress(new_data, bufsize=original_size)
                            cols = param
                            lines = original_size // cols

                            nd = frombuffer(new_data[: lines * cols], dtype=uint8)
                            nd = nd.reshape((cols, lines))
                            new_data = nd.T.ravel().tobytes() + new_data[lines * cols :]
                        elif block_type == v4c.DZ_BLOCK_LZ:
                            new_data = lz_decompress(new_data)

                        data.append(new_data)

                else:
                    start_offset = int(start_offset)
                    end_offset = int(end_offset)

                    current_offset = 0

                    for info in group.get_signal_data_blocks(index):
                        address, original_size, compressed_size, block_type, param = (
                            info.address,
                            info.original_size,
                            info.compressed_size,
                            info.block_type,
                            info.param,
                        )

                        if not info.original_size:
                            continue
                        if info.location == v4c.LOCATION_TEMPORARY_FILE:
                            stream = self._tempfile
                        else:
                            stream = self._file

                        if current_offset + original_size < start_offset:
                            current_offset += original_size
                            continue

                        stream.seek(address)
                        new_data = stream.read(compressed_size)
                        if block_type == v4c.DZ_BLOCK_DEFLATE:
                            new_data = decompress(new_data, bufsize=original_size)
                        elif block_type == v4c.DZ_BLOCK_TRANSPOSED:
                            new_data = decompress(new_data, bufsize=original_size)
                            cols = param
                            lines = original_size // cols

                            nd = frombuffer(new_data[: lines * cols], dtype=uint8)
                            nd = nd.reshape((cols, lines))
                            new_data = nd.T.ravel().tobytes() + new_data[lines * cols :]
                        elif block_type == v4c.DZ_BLOCK_LZ:
                            new_data = lz_decompress(new_data)

                        if current_offset + original_size > end_offset:
                            start_index = max(0, start_offset - current_offset)
                            (last_sample_size,) = UINT32_uf(
                                new_data, end_offset - current_offset
                            )
                            data.append(
                                new_data[
                                    start_index : end_offset
                                    - current_offset
                                    + last_sample_size
                                    + 4
                                ]
                            )

                            break

                        else:
                            if start_offset > current_offset:
                                data.append(new_data[start_offset - current_offset :])
                            else:
                                data.append(new_data)

                            current_offset += original_size

                data = b"".join(data)
            else:
                data = b""
        else:
            data = b""

        return data

    def _load_data(
        self,
        group: Group,
        record_offset: int = 0,
        record_count: int | None = None,
        optimize_read: bool = False,
    ) -> Iterator[tuple[bytes, int, int, bytes | None]]:
        """get group's data block bytes"""

        offset = 0
        invalidation_offset = 0
        has_yielded = False
        _count = 0
        data_group = group.data_group
        data_blocks_info_generator = group.data_blocks_info_generator
        channel_group = group.channel_group

        if group.data_location == v4c.LOCATION_ORIGINAL_FILE:
            stream = self._file
        else:
            stream = self._tempfile

        read = stream.read
        seek = stream.seek

        if group.uses_ld:
            samples_size = channel_group.samples_byte_nr
            invalidation_size = channel_group.invalidation_bytes_nr
            invalidation_record_offset = record_offset * invalidation_size
            rm = True
        else:
            rm = False
            samples_size = (
                channel_group.samples_byte_nr + channel_group.invalidation_bytes_nr
            )
            invalidation_size = channel_group.invalidation_bytes_nr

        record_offset *= samples_size

        finished = False
        if record_count is not None:
            invalidation_record_count = record_count * invalidation_size
            record_count *= samples_size

        if not samples_size:
            if rm:
                yield b"", offset, _count, b""
            else:
                yield b"", offset, _count, None
        else:

            if group.read_split_count:
                split_size = group.read_split_count * samples_size
                invalidation_split_size = group.read_split_count * invalidation_size
            else:

                if self._read_fragment_size:
                    split_size = self._read_fragment_size // samples_size
                    invalidation_split_size = split_size * invalidation_size
                    split_size *= samples_size

                else:
                    channels_nr = len(group.channels)

                    y_axis = CONVERT

                    idx = searchsorted(CHANNEL_COUNT, channels_nr, side="right") - 1
                    if idx < 0:
                        idx = 0
                    split_size = y_axis[idx]

                    split_size = split_size // samples_size
                    invalidation_split_size = split_size * invalidation_size
                    split_size *= samples_size

            if split_size == 0:
                split_size = samples_size
                invalidation_split_size = invalidation_size

            split_size = int(split_size)
            invalidation_split_size = int(invalidation_split_size)

            blocks = iter(group.data_blocks)

            cur_size = 0
            data = []

            cur_invalidation_size = 0
            invalidation_data = []

            while True:
                try:
                    info = next(blocks)
                    (
                        address,
                        original_size,
                        compressed_size,
                        block_type,
                        param,
                        block_limit,
                    ) = (
                        info.address,
                        info.original_size,
                        info.compressed_size,
                        info.block_type,
                        info.param,
                        info.block_limit,
                    )

                    if rm and invalidation_size:
                        invalidation_info = info.invalidation_block
                    else:
                        invalidation_info = None
                except StopIteration:
                    try:
                        info = next(data_blocks_info_generator)
                        (
                            address,
                            original_size,
                            compressed_size,
                            block_type,
                            param,
                            block_limit,
                        ) = (
                            info.address,
                            info.original_size,
                            info.compressed_size,
                            info.block_type,
                            info.param,
                            info.block_limit,
                        )

                        if rm and invalidation_size:
                            invalidation_info = info.invalidation_block
                        else:
                            invalidation_info = None
                        group.data_blocks.append(info)
                    except StopIteration:
                        break

                if offset + original_size < record_offset + 1:
                    offset += original_size
                    if rm and invalidation_size:
                        if invalidation_info.all_valid:
                            count = original_size // samples_size
                            invalidation_offset += count * invalidation_size
                        else:
                            invalidation_offset += invalidation_info.original_size
                    continue

                seek(address)
                new_data = read(compressed_size)

                if block_type == v4c.DZ_BLOCK_DEFLATE:
                    new_data = decompress(new_data, bufsize=original_size)
                elif block_type == v4c.DZ_BLOCK_TRANSPOSED:
                    new_data = decompress(new_data, bufsize=original_size)
                    cols = param
                    lines = original_size // cols

                    nd = frombuffer(new_data[: lines * cols], dtype=uint8)
                    nd = nd.reshape((cols, lines))
                    new_data = nd.T.ravel().tobytes() + new_data[lines * cols :]
                elif block_type == v4c.DZ_BLOCK_LZ:
                    new_data = lz_decompress(new_data)

                if block_limit is not None:
                    new_data = new_data[:block_limit]

                if len(data) > split_size - cur_size:
                    new_data = memoryview(new_data)

                if rm and invalidation_size:

                    if invalidation_info.all_valid:
                        count = original_size // samples_size
                        new_invalidation_data = b"\0" * (count * invalidation_size)

                    else:
                        seek(invalidation_info.address)
                        new_invalidation_data = read(invalidation_info.size)
                        if invalidation_info.block_type == v4c.DZ_BLOCK_DEFLATE:
                            new_invalidation_data = decompress(
                                new_invalidation_data,
                                bufsize=invalidation_info.original_size,
                            )
                        elif invalidation_info.block_type == v4c.DZ_BLOCK_TRANSPOSED:
                            new_invalidation_data = decompress(
                                new_invalidation_data,
                                bufsize=invalidation_info.original_size,
                            )
                            cols = invalidation_info.param
                            lines = invalidation_info.original_size // cols

                            nd = frombuffer(
                                new_invalidation_data[: lines * cols], dtype=uint8
                            )
                            nd = nd.reshape((cols, lines))
                            new_invalidation_data = (
                                nd.T.ravel().tobytes()
                                + new_invalidation_data[lines * cols :]
                            )
                        if invalidation_info.block_limit is not None:
                            new_invalidation_data = new_invalidation_data[
                                : invalidation_info.block_limit
                            ]

                    inv_size = len(new_invalidation_data)

                if offset < record_offset:
                    delta = record_offset - offset
                    new_data = new_data[delta:]
                    original_size -= delta
                    offset = record_offset

                    if rm and invalidation_size:
                        delta = invalidation_record_offset - invalidation_offset
                        new_invalidation_data = new_invalidation_data[delta:]
                        inv_size -= delta
                        invalidation_offset = invalidation_record_offset

                while original_size >= split_size - cur_size:
                    if data:
                        data.append(new_data[: split_size - cur_size])
                        new_data = new_data[split_size - cur_size :]
                        data_ = b"".join(data)

                        if rm and invalidation_size:
                            invalidation_data.append(
                                new_invalidation_data[
                                    : invalidation_split_size - cur_invalidation_size
                                ]
                            )
                            new_invalidation_data = new_invalidation_data[
                                invalidation_split_size - cur_invalidation_size :
                            ]
                            invalidation_data_ = b"".join(invalidation_data)

                        if record_count is not None:
                            if rm and invalidation_size:
                                __data = data_[:record_count]
                                _count = len(__data) // samples_size
                                yield __data, offset // samples_size, _count, invalidation_data_[
                                    :invalidation_record_count
                                ]
                                invalidation_record_count -= len(invalidation_data_)
                            else:
                                __data = data_[:record_count]
                                _count = len(__data) // samples_size
                                yield __data, offset // samples_size, _count, None
                            has_yielded = True
                            record_count -= len(data_)
                            if record_count <= 0:
                                finished = True
                                break
                        else:
                            if rm and invalidation_size:
                                _count = len(data_) // samples_size
                                yield data_, offset // samples_size, _count, invalidation_data_
                            else:
                                _count = len(data_) // samples_size
                                yield data_, offset // samples_size, _count, None
                            has_yielded = True

                        data = []

                    else:
                        data_, new_data = (
                            new_data[:split_size],
                            new_data[split_size:],
                        )
                        if rm and invalidation_size:
                            invalidation_data_ = new_invalidation_data[
                                :invalidation_split_size
                            ]
                            new_invalidation_data = new_invalidation_data[
                                invalidation_split_size:
                            ]

                        if record_count is not None:
                            if rm and invalidation_size:
                                yield data_[
                                    :record_count
                                ], offset // samples_size, _count, invalidation_data_[
                                    :invalidation_record_count
                                ]
                                invalidation_record_count -= len(invalidation_data_)
                            else:
                                __data = data_[:record_count]
                                _count = len(__data) // samples_size
                                yield __data, offset // samples_size, _count, None
                            has_yielded = True
                            record_count -= len(data_)
                            if record_count <= 0:
                                finished = True
                                break
                        else:
                            if rm and invalidation_size:
                                _count = len(data_) // samples_size
                                yield data_, offset // samples_size, _count, invalidation_data_
                            else:
                                _count = len(data_) // samples_size
                                yield data_, offset // samples_size, _count, None
                            has_yielded = True

                    offset += split_size
                    original_size -= split_size - cur_size
                    data = []
                    cur_size = 0

                    if rm and invalidation_size:
                        invalidation_offset += invalidation_split_size
                        invalidation_data = []
                        cur_invalidation_size = 0
                        inv_size -= invalidation_split_size - cur_invalidation_size

                if finished:
                    data = []
                    if rm and invalidation_size:
                        invalidation_data = []
                    break

                if original_size:
                    data.append(new_data)
                    cur_size += original_size
                    original_size = 0

                    if rm and invalidation_size:
                        invalidation_data.append(new_invalidation_data)
                        cur_invalidation_size += inv_size

            if data:
                data_ = b"".join(data)
                if rm and invalidation_size:
                    invalidation_data_ = b"".join(invalidation_data)
                if record_count is not None:
                    if rm and invalidation_size:
                        __data = data_[:record_count]
                        _count = len(__data) // samples_size
                        yield __data, offset // samples_size, _count, invalidation_data_[
                            :invalidation_record_count
                        ]
                        invalidation_record_count -= len(invalidation_data_)
                    else:
                        __data = data_[:record_count]
                        _count = len(__data) // samples_size
                        yield __data, offset // samples_size, _count, None
                    has_yielded = True
                    record_count -= len(data_)
                else:
                    if rm and invalidation_size:
                        _count = len(data_) // samples_size
                        yield data_, offset // samples_size, _count, invalidation_data_
                    else:
                        _count = len(data_) // samples_size
                        yield data_, offset // samples_size, _count, None
                    has_yielded = True
                data = []

            if not has_yielded:
                if rm and invalidation_size:
                    yield b"", 0, 0, b""
                else:
                    yield b"", 0, 0, None

    def _prepare_record(
        self, group: Group
    ) -> tuple[dict[int, tuple[str, int]], dtype[Any]]:
        """compute record

        Parameters
        ----------
        group : dict
            MDF group dict

        Returns
        -------
        record : list
            mapping of channels to records fields, records fields dtype

        """

        record = group.record

        if record is None:
            channels = group.channels

            record = []

            for idx, new_ch in enumerate(channels):
                start_offset = new_ch.byte_offset
                bit_offset = new_ch.bit_offset
                data_type = new_ch.data_type
                bit_count = new_ch.bit_count
                ch_type = new_ch.channel_type
                dependency_list = group.channel_dependencies[idx]

                if ch_type not in v4c.VIRTUAL_TYPES and not dependency_list:

                    if not new_ch.dtype_fmt:
                        new_ch.dtype_fmt = dtype(
                            get_fmt_v4(data_type, bit_count, ch_type)
                        )

                    # adjust size to 1, 2, 4 or 8 bytes
                    size = bit_offset + bit_count

                    byte_size, rem = size // 8, size % 8
                    if rem:
                        byte_size += 1
                    bit_size = byte_size * 8

                    if data_type in (
                        v4c.DATA_TYPE_SIGNED_MOTOROLA,
                        v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
                    ):
                        if size > 32:
                            bit_offset += 64 - bit_size
                        elif size > 16:
                            bit_offset += 32 - bit_size
                        elif size > 8:
                            bit_offset += 16 - bit_size

                    if (
                        bit_offset
                        or new_ch.dtype_fmt.kind in "ui"
                        and size < 64
                        and size not in (8, 16, 32)
                    ):
                        new_ch.standard_C_size = False

                    record.append(
                        (
                            new_ch.dtype_fmt,
                            new_ch.dtype_fmt.itemsize,
                            start_offset,
                            bit_offset,
                        )
                    )
                else:
                    record.append(None)

            group.record = record

        return record

    def _uses_ld(
        self,
        address: int,
        stream: ReadableBufferType,
        block_type: bytes = b"##DT",
        mapped: bool = False,
    ) -> bool:
        info = []
        mapped = mapped or not is_file_like(stream)
        uses_ld = False

        if mapped:
            if address:
                id_string, block_len = COMMON_SHORT_uf(stream, address)

                if id_string == b"##LD":
                    uses_ld = True
                # or a header list
                elif id_string == b"##HL":
                    hl = HeaderList(address=address, stream=stream, mapped=mapped)
                    address = hl.first_dl_addr

                    uses_ld = self._uses_ld(
                        address,
                        stream,
                        block_type,
                        mapped,
                    )
        else:

            if address:
                stream.seek(address)
                id_string, block_len = COMMON_SHORT_u(stream.read(COMMON_SHORT_SIZE))

                # can be a DataBlock
                if id_string == b"##LD":
                    uses_ld = True

                # or a header list
                elif id_string == b"##HL":
                    hl = HeaderList(address=address, stream=stream)
                    address = hl.first_dl_addr

                    uses_ld = self._uses_ld(
                        address,
                        stream,
                        block_type,
                        mapped,
                    )

        return uses_ld

    def _get_data_blocks_info(
        self,
        address: int,
        stream: ReadableBufferType,
        block_type: bytes = b"##DT",
        mapped: bool = False,
        total_size: int = 0,
        inval_total_size: int = 0,
    ) -> Iterator[DataBlockInfo]:
        mapped = mapped or not is_file_like(stream)

        if mapped:
            if address:
                id_string, block_len = COMMON_SHORT_uf(stream, address)

                # can be a DataBlock
                if id_string == block_type:
                    size = block_len - 24
                    if size:
                        if total_size < size:
                            block_limit = total_size
                        else:
                            block_limit = None
                        total_size -= size
                        yield DataBlockInfo(
                            address=address + COMMON_SIZE,
                            block_type=v4c.DT_BLOCK,
                            original_size=size,
                            compressed_size=size,
                            param=0,
                            block_limit=block_limit,
                        )

                # or a DataZippedBlock
                elif id_string == b"##DZ":
                    (zip_type, param, original_size, zip_size,) = v4c.DZ_COMMON_INFO_uf(
                        stream, address + v4c.DZ_INFO_COMMON_OFFSET
                    )

                    if original_size:
                        if zip_type == v4c.FLAG_DZ_DEFLATE:
                            block_type_ = v4c.DZ_BLOCK_DEFLATE
                            param = 0
                        else:
                            block_type_ = v4c.DZ_BLOCK_TRANSPOSED
                        if total_size < original_size:
                            block_limit = total_size
                        else:
                            block_limit = None
                        total_size -= original_size
                        yield DataBlockInfo(
                            address=address + v4c.DZ_COMMON_SIZE,
                            block_type=block_type_,
                            original_size=original_size,
                            compressed_size=zip_size,
                            param=param,
                            block_limit=block_limit,
                        )

                # or a DataList
                elif id_string == b"##DL":
                    while address:
                        dl = DataList(address=address, stream=stream, mapped=mapped)
                        for i in range(dl.data_block_nr):
                            addr = dl[f"data_block_addr{i}"]

                            id_string, block_len = COMMON_SHORT_uf(stream, addr)
                            # can be a DataBlock
                            if id_string == block_type:
                                size = block_len - 24
                                if size:
                                    if total_size < size:
                                        block_limit = total_size
                                    else:
                                        block_limit = None
                                    total_size -= size
                                    yield DataBlockInfo(
                                        address=addr + COMMON_SIZE,
                                        block_type=v4c.DT_BLOCK,
                                        original_size=size,
                                        compressed_size=size,
                                        param=0,
                                        block_limit=block_limit,
                                    )

                            # or a DataZippedBlock
                            elif id_string == b"##DZ":
                                (
                                    zip_type,
                                    param,
                                    original_size,
                                    zip_size,
                                ) = v4c.DZ_COMMON_INFO_uf(
                                    stream, addr + v4c.DZ_INFO_COMMON_OFFSET
                                )

                                if original_size:
                                    if zip_type == v4c.FLAG_DZ_DEFLATE:
                                        block_type_ = v4c.DZ_BLOCK_DEFLATE
                                        param = 0
                                    else:
                                        block_type_ = v4c.DZ_BLOCK_TRANSPOSED
                                    if total_size < original_size:
                                        block_limit = total_size
                                    else:
                                        block_limit = None
                                    total_size -= original_size
                                    yield DataBlockInfo(
                                        address=addr + v4c.DZ_COMMON_SIZE,
                                        block_type=block_type_,
                                        original_size=original_size,
                                        compressed_size=zip_size,
                                        param=param,
                                        block_limit=block_limit,
                                    )
                        address = dl.next_dl_addr

                # or a ListData
                elif id_string == b"##LD":
                    uses_ld = True
                    while address:
                        ld = ListData(address=address, stream=stream, mapped=mapped)
                        has_invalidation = ld.flags & v4c.FLAG_LD_INVALIDATION_PRESENT
                        for i in range(ld.data_block_nr):
                            addr = ld[f"data_block_addr_{i}"]

                            id_string, block_len = COMMON_SHORT_uf(stream, addr)
                            # can be a DataBlock
                            if id_string == b"##DV":
                                size = block_len - 24
                                if size:
                                    if total_size < size:
                                        block_limit = total_size
                                    else:
                                        block_limit = None
                                    total_size -= size
                                    data_info = DataBlockInfo(
                                        address=addr + COMMON_SIZE,
                                        block_type=v4c.DT_BLOCK,
                                        original_size=size,
                                        compressed_size=size,
                                        param=0,
                                        block_limit=block_limit,
                                    )

                            # or a DataZippedBlock
                            elif id_string == b"##DZ":
                                (
                                    zip_type,
                                    param,
                                    original_size,
                                    zip_size,
                                ) = v4c.DZ_COMMON_INFO_uf(
                                    stream, addr + v4c.DZ_INFO_COMMON_OFFSET
                                )

                                if original_size:
                                    if zip_type == v4c.FLAG_DZ_DEFLATE:
                                        block_type_ = v4c.DZ_BLOCK_DEFLATE
                                        param = 0
                                    else:
                                        block_type_ = v4c.DZ_BLOCK_TRANSPOSED
                                    if total_size < original_size:
                                        block_limit = total_size
                                    else:
                                        block_limit = None
                                    total_size -= original_size
                                    data_info = DataBlockInfo(
                                        address=addr + v4c.DZ_COMMON_SIZE,
                                        block_type=block_type_,
                                        original_size=original_size,
                                        compressed_size=zip_size,
                                        param=param,
                                        block_limit=block_limit,
                                    )

                            if has_invalidation:
                                inval_addr = ld[f"invalidation_bits_addr_{i}"]
                                if inval_addr:
                                    id_string, block_len = COMMON_SHORT_uf(
                                        stream, inval_addr
                                    )
                                    if id_string == b"##DI":
                                        size = block_len - 24
                                        if size:
                                            if inval_total_size < size:
                                                block_limit = inval_total_size
                                            else:
                                                block_limit = None
                                            inval_total_size -= size
                                            data_info.invalidation_block = (
                                                InvalidationBlockInfo(
                                                    address=inval_addr + COMMON_SIZE,
                                                    block_type=v4c.DT_BLOCK,
                                                    original_size=size,
                                                    compressed_size=size,
                                                    param=0,
                                                    block_limit=block_limit,
                                                )
                                            )
                                    else:
                                        (
                                            zip_type,
                                            param,
                                            original_size,
                                            zip_size,
                                        ) = v4c.DZ_COMMON_INFO_uf(
                                            stream,
                                            inval_addr + v4c.DZ_INFO_COMMON_OFFSET,
                                        )

                                        if original_size:
                                            if zip_type == v4c.FLAG_DZ_DEFLATE:
                                                block_type_ = v4c.DZ_BLOCK_DEFLATE
                                                param = 0
                                            else:
                                                block_type_ = v4c.DZ_BLOCK_TRANSPOSED
                                            if inval_total_size < original_size:
                                                block_limit = inval_total_size
                                            else:
                                                block_limit = None
                                            inval_total_size -= original_size
                                            data_info.invalidation_block = (
                                                InvalidationBlockInfo(
                                                    address=inval_addr
                                                    + v4c.DZ_COMMON_SIZE,
                                                    block_type=block_type_,
                                                    original_size=original_size,
                                                    compressed_size=zip_size,
                                                    param=param,
                                                    block_limit=block_limit,
                                                )
                                            )
                                else:
                                    data_info.invalidation_block = (
                                        InvalidationBlockInfo(
                                            address=0,
                                            block_type=v4c.DT_BLOCK,
                                            original_size=None,
                                            compressed_size=None,
                                            param=None,
                                            all_valid=True,
                                        )
                                    )

                            yield data_info

                        address = ld.next_ld_addr

                # or a header list
                elif id_string == b"##HL":
                    hl = HeaderList(address=address, stream=stream, mapped=mapped)
                    address = hl.first_dl_addr

                    yield from self._get_data_blocks_info(
                        address,
                        stream,
                        block_type,
                        mapped,
                        total_size,
                        inval_total_size,
                    )
        else:

            if address:
                stream.seek(address)
                id_string, block_len = COMMON_SHORT_u(stream.read(COMMON_SHORT_SIZE))

                # can be a DataBlock
                if id_string == block_type:
                    size = block_len - 24
                    if size:
                        if total_size < size:
                            block_limit = total_size
                        else:
                            block_limit = None
                        total_size -= size
                        yield DataBlockInfo(
                            address=address + COMMON_SIZE,
                            block_type=v4c.DT_BLOCK,
                            original_size=size,
                            compressed_size=size,
                            param=0,
                            block_limit=block_limit,
                        )

                # or a DataZippedBlock
                elif id_string == b"##DZ":
                    stream.seek(address + v4c.DZ_INFO_COMMON_OFFSET)
                    (
                        zip_type,
                        param,
                        original_size,
                        zip_size,
                    ) = v4c.DZ_COMMON_INFO_u(stream.read(v4c.DZ_COMMON_INFO_SIZE))

                    if original_size:
                        if zip_type == v4c.FLAG_DZ_DEFLATE:
                            block_type_ = v4c.DZ_BLOCK_DEFLATE
                            param = 0
                        else:
                            block_type_ = v4c.DZ_BLOCK_TRANSPOSED
                        if total_size < original_size:
                            block_limit = total_size
                        else:
                            block_limit = None
                        total_size -= original_size
                        yield DataBlockInfo(
                            address=address + v4c.DZ_COMMON_SIZE,
                            block_type=block_type_,
                            original_size=original_size,
                            compressed_size=zip_size,
                            param=param,
                            block_limit=block_limit,
                        )

                # or a DataList
                elif id_string == b"##DL":
                    while address:
                        dl = DataList(address=address, stream=stream)
                        for i in range(dl.data_block_nr):

                            addr = dl[f"data_block_addr{i}"]

                            stream.seek(addr)
                            id_string, block_len = COMMON_SHORT_u(
                                stream.read(COMMON_SHORT_SIZE)
                            )

                            # can be a DataBlock
                            if id_string == block_type:
                                size = block_len - 24
                                if size:
                                    if total_size < size:
                                        block_limit = total_size
                                    else:
                                        block_limit = None
                                    total_size -= size
                                    yield DataBlockInfo(
                                        address=addr + COMMON_SIZE,
                                        block_type=v4c.DT_BLOCK,
                                        original_size=size,
                                        compressed_size=size,
                                        param=0,
                                        block_limit=block_limit,
                                    )

                            # or a DataZippedBlock
                            elif id_string == b"##DZ":
                                stream.seek(addr + v4c.DZ_INFO_COMMON_OFFSET)
                                (
                                    zip_type,
                                    param,
                                    original_size,
                                    zip_size,
                                ) = v4c.DZ_COMMON_INFO_u(
                                    stream.read(v4c.DZ_COMMON_INFO_SIZE)
                                )

                                if original_size:
                                    if zip_type == v4c.FLAG_DZ_DEFLATE:
                                        block_type_ = v4c.DZ_BLOCK_DEFLATE
                                        param = 0
                                    else:
                                        block_type_ = v4c.DZ_BLOCK_TRANSPOSED
                                    if total_size < original_size:
                                        block_limit = total_size
                                    else:
                                        block_limit = None
                                    total_size -= original_size
                                    yield DataBlockInfo(
                                        address=addr + v4c.DZ_COMMON_SIZE,
                                        block_type=block_type_,
                                        original_size=original_size,
                                        compressed_size=zip_size,
                                        param=param,
                                        block_limit=block_limit,
                                    )

                        address = dl.next_dl_addr

                # or a DataList
                elif id_string == b"##LD":
                    uses_ld = True
                    while address:
                        ld = ListData(address=address, stream=stream)
                        has_invalidation = ld.flags & v4c.FLAG_LD_INVALIDATION_PRESENT
                        for i in range(ld.data_block_nr):
                            addr = ld[f"data_block_addr{i}"]

                            stream.seek(addr)
                            id_string, block_len = COMMON_SHORT_u(
                                stream.read(COMMON_SHORT_SIZE)
                            )
                            # can be a DataBlock
                            if id_string == b"##DV":
                                size = block_len - 24
                                if size:
                                    if total_size < size:
                                        block_limit = total_size
                                    else:
                                        block_limit = None
                                    total_size -= size
                                    data_info = DataBlockInfo(
                                        address=addr + COMMON_SIZE,
                                        block_type=v4c.DT_BLOCK,
                                        original_size=size,
                                        compressed_size=size,
                                        param=0,
                                        block_limit=block_limit,
                                    )

                            # or a DataZippedBlock
                            elif id_string == b"##DZ":
                                stream.seek(addr + v4c.DZ_INFO_COMMON_OFFSET)
                                (
                                    zip_type,
                                    param,
                                    original_size,
                                    zip_size,
                                ) = v4c.DZ_COMMON_INFO_u(
                                    stream.read(v4c.DZ_COMMON_INFO_SIZE)
                                )

                                if original_size:
                                    if zip_type == v4c.FLAG_DZ_DEFLATE:
                                        block_type_ = v4c.DZ_BLOCK_DEFLATE
                                        param = 0
                                    else:
                                        block_type_ = v4c.DZ_BLOCK_TRANSPOSED
                                    if total_size < original_size:
                                        block_limit = total_size
                                    else:
                                        block_limit = None
                                    total_size -= original_size
                                    data_info = DataBlockInfo(
                                        address=addr + v4c.DZ_COMMON_SIZE,
                                        block_type=block_type_,
                                        original_size=original_size,
                                        compressed_size=zip_size,
                                        param=param,
                                        block_limit=block_limit,
                                    )

                            if has_invalidation:
                                inval_addr = ld[f"invalidation_bits_addr_{i}"]
                                if inval_addr:
                                    stream.seek(inval_addr)
                                    id_string, block_len = COMMON_SHORT_u(
                                        stream.read(COMMON_SHORT_SIZE)
                                    )
                                    if id_string == b"##DI":
                                        size = block_len - 24
                                        if size:
                                            if inval_total_size < size:
                                                block_limit = inval_total_size
                                            else:
                                                block_limit = None
                                            inval_total_size -= size
                                            data_info.invalidation_block = (
                                                InvalidationBlockInfo(
                                                    address=inval_addr + COMMON_SIZE,
                                                    block_type=v4c.DT_BLOCK,
                                                    original_size=size,
                                                    compressed_size=size,
                                                    param=0,
                                                    block_limit=block_limit,
                                                )
                                            )
                                    else:
                                        stream.seek(
                                            inval_addr + v4c.DZ_INFO_COMMON_OFFSET
                                        )
                                        (
                                            zip_type,
                                            param,
                                            original_size,
                                            zip_size,
                                        ) = v4c.DZ_COMMON_INFO_u(
                                            stream.read(v4c.DZ_COMMON_INFO_SIZE)
                                        )

                                        if original_size:
                                            if zip_type == v4c.FLAG_DZ_DEFLATE:
                                                block_type_ = v4c.DZ_BLOCK_DEFLATE
                                                param = 0
                                            else:
                                                block_type_ = v4c.DZ_BLOCK_TRANSPOSED
                                            if inval_total_size < original_size:
                                                block_limit = inval_total_size
                                            else:
                                                block_limit = None
                                            inval_total_size -= original_size
                                            data_info.invalidation_block = (
                                                InvalidationBlockInfo(
                                                    address=inval_addr
                                                    + v4c.DZ_COMMON_SIZE,
                                                    block_type=block_type_,
                                                    original_size=original_size,
                                                    compressed_size=zip_size,
                                                    param=param,
                                                    block_limit=block_limit,
                                                )
                                            )
                                else:
                                    data_info.invalidation_block = (
                                        InvalidationBlockInfo(
                                            address=0,
                                            block_type=v4c.DT_BLOCK,
                                            original_size=0,
                                            compressed_size=0,
                                            param=0,
                                            all_valid=True,
                                        )
                                    )

                            yield data_info
                        address = ld.next_ld_addr

                # or a header list
                elif id_string == b"##HL":
                    hl = HeaderList(address=address, stream=stream)
                    address = hl.first_dl_addr

                    yield from self._get_data_blocks_info(
                        address,
                        stream,
                        block_type,
                        mapped,
                        total_size,
                        inval_total_size,
                    )

    def _get_signal_data_blocks_info(
        self,
        address: int,
        stream: ReadableBufferType,
    ) -> Iterator[SignalDataBlockInfo]:

        if not address:
            raise MdfException(
                f"Expected non-zero SDBLOCK address but got 0x{address:X}"
            )

        stream.seek(address)
        id_string, block_len = COMMON_SHORT_u(stream.read(COMMON_SHORT_SIZE))

        # can be a DataBlock
        if id_string == b"##SD":
            size = block_len - 24
            if size:
                yield SignalDataBlockInfo(
                    address=address + COMMON_SIZE,
                    compressed_size=size,
                    original_size=size,
                    block_type=v4c.DT_BLOCK,
                )

        # or a DataZippedBlock
        elif id_string == b"##DZ":
            stream.seek(address + v4c.DZ_INFO_COMMON_OFFSET)
            (
                zip_type,
                param,
                original_size,
                zip_size,
            ) = v4c.DZ_COMMON_INFO_u(stream.read(v4c.DZ_COMMON_INFO_SIZE))

            if original_size:
                if zip_type == v4c.FLAG_DZ_DEFLATE:
                    block_type_ = v4c.DZ_BLOCK_DEFLATE
                    param = 0
                else:
                    block_type_ = v4c.DZ_BLOCK_TRANSPOSED

                yield SignalDataBlockInfo(
                    address=address + v4c.DZ_COMMON_SIZE,
                    block_type=block_type_,
                    original_size=original_size,
                    compressed_size=zip_size,
                    param=param,
                )

        # or a DataList
        elif id_string == b"##DL":
            while address:
                dl = DataList(address=address, stream=stream)
                for i in range(dl.data_block_nr):

                    addr = dl[f"data_block_addr{i}"]

                    stream.seek(addr)
                    id_string, block_len = COMMON_SHORT_u(
                        stream.read(COMMON_SHORT_SIZE)
                    )

                    # can be a DataBlock
                    if id_string == b"##SD":
                        size = block_len - 24
                        if size:
                            yield SignalDataBlockInfo(
                                address=addr + COMMON_SIZE,
                                compressed_size=size,
                                original_size=size,
                                block_type=v4c.DT_BLOCK,
                            )

                    # or a DataZippedBlock
                    elif id_string == b"##DZ":
                        stream.seek(addr + v4c.DZ_INFO_COMMON_OFFSET)
                        (
                            zip_type,
                            param,
                            original_size,
                            zip_size,
                        ) = v4c.DZ_COMMON_INFO_u(stream.read(v4c.DZ_COMMON_INFO_SIZE))

                        if original_size:
                            if zip_type == v4c.FLAG_DZ_DEFLATE:
                                block_type_ = v4c.DZ_BLOCK_DEFLATE
                                param = 0
                            else:
                                block_type_ = v4c.DZ_BLOCK_TRANSPOSED
                            yield SignalDataBlockInfo(
                                address=addr + v4c.DZ_COMMON_SIZE,
                                block_type=block_type_,
                                original_size=original_size,
                                compressed_size=zip_size,
                                param=param,
                            )

                address = dl.next_dl_addr

        # or a header list
        elif id_string == b"##HL":
            hl = HeaderList(address=address, stream=stream)
            address = hl.first_dl_addr

            yield from self._get_signal_data_blocks_info(
                address,
                stream,
            )

    def _filter_occurrences(
        self,
        occurrences: Sequence[tuple[int, int]],
        source_name: str | None = None,
        source_path: str | None = None,
        acq_name: str | None = None,
    ) -> Iterator[tuple[int, int]]:
        if source_name is not None:
            occurrences = (
                (gp_idx, cn_idx)
                for gp_idx, cn_idx in occurrences
                if (
                    self.groups[gp_idx].channels[cn_idx].source is not None
                    and self.groups[gp_idx].channels[cn_idx].source.name == source_name
                )
                or (
                    self.groups[gp_idx].channel_group.acq_source is not None
                    and self.groups[gp_idx].channel_group.acq_source.name == source_name
                )
            )

        if source_path is not None:
            occurrences = (
                (gp_idx, cn_idx)
                for gp_idx, cn_idx in occurrences
                if (
                    self.groups[gp_idx].channels[cn_idx].source is not None
                    and self.groups[gp_idx].channels[cn_idx].source.path == source_path
                )
                or (
                    self.groups[gp_idx].channel_group.acq_source is not None
                    and self.groups[gp_idx].channel_group.acq_source.path == source_path
                )
            )

        if acq_name is not None:
            occurrences = (
                (gp_idx, cn_idx)
                for gp_idx, cn_idx in occurrences
                if self.groups[gp_idx].channel_group.acq_name == acq_name
            )

        return occurrences

    def get_invalidation_bits(
        self,
        group_index: int,
        channel: Channel,
        fragment: tuple[bytes, int, int, ReadableBufferType | None],
    ) -> NDArray[bool_]:
        """get invalidation indexes for the channel

        Parameters
        ----------
        group_index : int
            group index
        channel : Channel
            channel object
        fragment : (bytes, int)
            (fragment bytes, fragment offset)

        Returns
        -------
        invalidation_bits : iterable
            iterable of valid channel indexes; if all are valid `None` is
            returned

        """
        group = self.groups[group_index]

        data_bytes, offset, _count, invalidation_bytes = fragment
        try:
            invalidation = self._invalidation_cache[(group_index, offset, _count)]
        except KeyError:
            size = group.channel_group.invalidation_bytes_nr

            if invalidation_bytes is None:
                record = group.record
                if record is None:
                    self._prepare_record(group)

                invalidation_bytes = get_channel_raw_bytes(
                    data_bytes,
                    group.channel_group.samples_byte_nr
                    + group.channel_group.invalidation_bytes_nr,
                    group.channel_group.samples_byte_nr,
                    size,
                )

            invalidation = frombuffer(invalidation_bytes, dtype=f"({size},)u1")
            self._invalidation_cache[(group_index, offset, _count)] = invalidation

        ch_invalidation_pos = channel.pos_invalidation_bit
        pos_byte, pos_offset = ch_invalidation_pos // 8, ch_invalidation_pos % 8

        mask = 1 << pos_offset

        invalidation_bits = invalidation[:, pos_byte] & mask
        invalidation_bits = invalidation_bits.astype(bool)

        return invalidation_bits

    def append(
        self,
        signals: list[Signal] | Signal | DataFrame,
        acq_name: str | None = None,
        acq_source: Source | None = None,
        comment: str = "Python",
        common_timebase: bool = False,
        units: dict[str, str | bytes] | None = None,
    ) -> int | None:
        """
        Appends a new data group.

        For channel dependencies type Signals, the *samples* attribute must be
        a numpy.recarray

        Parameters
        ----------
        signals : list | Signal | pandas.DataFrame
            list of *Signal* objects, or a single *Signal* object, or a pandas
            *DataFrame* object. All bytes columns in the pandas *DataFrame*
            must be *utf-8* encoded
        acq_name : str
            channel group acquisition name
        acq_source : Source
            channel group acquisition source
        comment : str
            channel group comment; default 'Python'
        common_timebase : bool
            flag to hint that the signals have the same timebase. Only set this
            if you know for sure that all appended channels share the same
            time base
        units : dict
            will contain the signal units mapped to the signal names when
            appending a pandas DataFrame

        Examples
        --------
        >>> # case 1 conversion type None
        >>> s1 = np.array([1, 2, 3, 4, 5])
        >>> s2 = np.array([-1, -2, -3, -4, -5])
        >>> s3 = np.array([0.1, 0.04, 0.09, 0.16, 0.25])
        >>> t = np.array([0.001, 0.002, 0.003, 0.004, 0.005])
        >>> names = ['Positive', 'Negative', 'Float']
        >>> units = ['+', '-', '.f']
        >>> info = {}
        >>> s1 = Signal(samples=s1, timestamps=t, unit='+', name='Positive')
        >>> s2 = Signal(samples=s2, timestamps=t, unit='-', name='Negative')
        >>> s3 = Signal(samples=s3, timestamps=t, unit='flts', name='Floats')
        >>> mdf = MDF4('new.mdf')
        >>> mdf.append([s1, s2, s3], comment='created by asammdf v4.0.0')
        >>> # case 2: VTAB conversions from channels inside another file
        >>> mdf1 = MDF4('in.mf4')
        >>> ch1 = mdf1.get("Channel1_VTAB")
        >>> ch2 = mdf1.get("Channel2_VTABR")
        >>> sigs = [ch1, ch2]
        >>> mdf2 = MDF4('out.mf4')
        >>> mdf2.append(sigs, comment='created by asammdf v4.0.0')
        >>> mdf2.append(ch1, comment='just a single channel')
        >>> df = pd.DataFrame.from_dict({'s1': np.array([1, 2, 3, 4, 5]), 's2': np.array([-1, -2, -3, -4, -5])})
        >>> units = {'s1': 'V', 's2': 'A'}
        >>> mdf2.append(df, units=units)

        """
        source_block = (
            SourceInformation.from_common_source(acq_source)
            if acq_source
            else acq_source
        )

        if isinstance(signals, Signal):
            signals = [signals]
        elif isinstance(signals, DataFrame):
            self._append_dataframe(
                signals,
                acq_name=acq_name,
                acq_source=source_block,
                comment=comment,
                units=units,
            )
            return

        if not signals:
            return

        # check if the signals have a common timebase
        # if not interpolate the signals using the union of all timebases
        if signals:
            t_ = signals[0].timestamps
            if not common_timebase:
                for s in signals[1:]:
                    if not array_equal(s.timestamps, t_):
                        different = True
                        break
                else:
                    different = False

                if different:
                    times = [s.timestamps for s in signals]
                    t = unique(concatenate(times)).astype(float64)
                    signals = [
                        s.interp(
                            t,
                            integer_interpolation_mode=self._integer_interpolation,
                            float_interpolation_mode=self._float_interpolation,
                        )
                        for s in signals
                    ]
                else:
                    t = t_
            else:
                t = t_
        else:
            t = []

        if self.version >= "4.20" and (self._column_storage or 1):
            return self._append_column_oriented(
                signals, acq_name=acq_name, acq_source=source_block, comment=comment
            )

        dg_cntr = len(self.groups)

        gp = Group(None)
        gp.signal_data = gp_sdata = []
        gp.channels = gp_channels = []
        gp.channel_dependencies = gp_dep = []
        gp.signal_types = gp_sig_types = []

        cycles_nr = len(t)

        # channel group
        kwargs = {"cycles_nr": cycles_nr, "samples_byte_nr": 0}
        gp.channel_group = ChannelGroup(**kwargs)
        gp.channel_group.acq_name = acq_name
        gp.channel_group.acq_source = source_block
        gp.channel_group.comment = comment
        gp.record = record = []

        if any(sig.invalidation_bits is not None for sig in signals):
            invalidation_bytes_nr = 1
            gp.channel_group.invalidation_bytes_nr = invalidation_bytes_nr

            inval_bits = []

        else:
            invalidation_bytes_nr = 0
            inval_bits = []
        inval_cntr = 0

        self.groups.append(gp)

        fields = []

        ch_cntr = 0
        offset = 0

        defined_texts = {}
        si_map = self._si_map

        # setup all blocks related to the time master channel

        file = self._tempfile
        tell = file.tell
        seek = file.seek

        seek(0, 2)

        if signals:
            master_metadata = signals[0].master_metadata
        else:
            master_metadata = None
        if master_metadata:
            time_name, sync_type = master_metadata
            if sync_type in (0, 1):
                time_unit = "s"
            elif sync_type == 2:
                time_unit = "deg"
            elif sync_type == 3:
                time_unit = "m"
            elif sync_type == 4:
                time_unit = "index"
        else:
            time_name, sync_type = "time", v4c.SYNC_TYPE_TIME
            time_unit = "s"

        gp.channel_group.acq_source = source_block

        if signals:

            # time channel
            t_type, t_size = fmt_to_datatype_v4(t.dtype, t.shape)
            kwargs = {
                "channel_type": v4c.CHANNEL_TYPE_MASTER,
                "data_type": t_type,
                "sync_type": sync_type,
                "byte_offset": 0,
                "bit_offset": 0,
                "bit_count": t_size,
            }

            ch = Channel(**kwargs)
            ch.unit = time_unit
            ch.name = time_name
            ch.source = source_block
            ch.dtype_fmt = t.dtype
            name = time_name

            gp_channels.append(ch)

            gp_sdata.append(None)
            self.channels_db.add(name, (dg_cntr, ch_cntr))
            self.masters_db[dg_cntr] = 0

            record.append(
                (
                    t.dtype,
                    t.dtype.itemsize,
                    0,
                    0,
                )
            )

            # time channel doesn't have channel dependencies
            gp_dep.append(None)

            fields.append((t.tobytes(), t.itemsize))

            offset += t_size // 8
            ch_cntr += 1

            gp_sig_types.append(0)

        for signal in signals:

            sig = signal
            samples = sig.samples
            sig_dtype = samples.dtype
            sig_shape = samples.shape
            names = sig_dtype.names
            name = signal.name

            if names is None:
                sig_type = v4c.SIGNAL_TYPE_SCALAR
                if sig_dtype.kind in "SV":
                    sig_type = v4c.SIGNAL_TYPE_STRING
            else:
                prepare_record = False
                if names in (v4c.CANOPEN_TIME_FIELDS, v4c.CANOPEN_DATE_FIELDS):
                    sig_type = v4c.SIGNAL_TYPE_CANOPEN
                elif names[0] != sig.name:
                    sig_type = v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION
                else:
                    sig_type = v4c.SIGNAL_TYPE_ARRAY

            gp_sig_types.append(sig_type)

            # first add the signals in the simple signal list
            if sig_type == v4c.SIGNAL_TYPE_SCALAR:

                # compute additional byte offset for large records size
                s_type, s_size = fmt_to_datatype_v4(sig_dtype, sig_shape)

                byte_size = s_size // 8 or 1
                data_block_addr = 0

                if sig_dtype.kind == "u" and signal.bit_count <= 4:
                    s_size = signal.bit_count

                if signal.stream_sync:
                    channel_type = v4c.CHANNEL_TYPE_SYNC
                    if signal.attachment:
                        at_data, at_name, hash_sum = signal.attachment
                        attachment_index = self.attach(
                            at_data, at_name, hash_sum, mime="video/avi", embedded=False
                        )
                        attachment = attachment_index
                    else:
                        attachment = None

                    sync_type = v4c.SYNC_TYPE_TIME
                else:
                    channel_type = v4c.CHANNEL_TYPE_VALUE
                    sync_type = v4c.SYNC_TYPE_NONE

                    if signal.attachment:
                        at_data, at_name, hash_sum = signal.attachment

                        attachment_index = self.attach(at_data, at_name, hash_sum)
                        attachment = attachment_index
                    else:
                        attachment = None

                kwargs = {
                    "channel_type": channel_type,
                    "sync_type": sync_type,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "data_block_addr": data_block_addr,
                    "flags": 0,
                }

                if attachment is not None:
                    kwargs["attachment_addr"] = 0

                if invalidation_bytes_nr and signal.invalidation_bits is not None:
                    inval_bits.append(signal.invalidation_bits)
                    kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
                    kwargs["pos_invalidation_bit"] = inval_cntr
                    inval_cntr += 1

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = signal.unit
                ch.comment = signal.comment
                ch.display_names = signal.display_names
                if len(sig_shape) > 1:
                    ch.dtype_fmt = dtype((sig_dtype, sig_shape[1:]))
                else:
                    ch.dtype_fmt = sig_dtype
                ch.attachment = attachment

                # conversions for channel
                if signal.raw:
                    ch.conversion = conversion_transfer(signal.conversion, version=4)

                # source for channel

                source = signal.source
                if source:
                    if source in si_map:
                        ch.source = si_map[source]
                    else:
                        new_source = SourceInformation(
                            source_type=source.source_type, bus_type=source.bus_type
                        )
                        new_source.name = source.name
                        new_source.path = source.path
                        new_source.comment = source.comment

                        si_map[source] = new_source

                        ch.source = new_source

                gp_channels.append(ch)

                record.append(
                    (
                        ch.dtype_fmt,
                        ch.dtype_fmt.itemsize,
                        offset,
                        0,
                    )
                )

                offset += byte_size

                fields.append((samples.tobytes(), byte_size))

                gp_sdata.append(None)
                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in ch.display_names:
                    self.channels_db.add(_name, entry)

                ch_cntr += 1

                # simple channels don't have channel dependencies
                gp_dep.append(None)

            elif sig_type == v4c.SIGNAL_TYPE_CANOPEN:

                if names == v4c.CANOPEN_TIME_FIELDS:
                    record.append(
                        (
                            dtype("V6"),
                            6,
                            offset,
                            0,
                        )
                    )

                    vals = signal.samples.tobytes()

                    fields.append((vals, 6))
                    byte_size = 6
                    s_type = v4c.DATA_TYPE_CANOPEN_TIME
                    s_dtype = dtype("V6")

                else:
                    record.append(
                        (
                            dtype("V7"),
                            7,
                            offset,
                            0,
                        )
                    )

                    vals = []
                    for field in ("ms", "min", "hour", "day", "month", "year"):
                        if field == "hour":
                            vals.append(
                                signal.samples[field]
                                + (signal.samples["summer_time"] << 7)
                            )
                        elif field == "day":
                            vals.append(
                                signal.samples[field]
                                + (signal.samples["day_of_week"] << 4)
                            )
                        else:
                            vals.append(signal.samples[field])
                    vals = fromarrays(vals).tobytes()

                    fields.append((vals, 7))
                    byte_size = 7
                    s_type = v4c.DATA_TYPE_CANOPEN_DATE
                    s_dtype = dtype("V7")

                s_size = byte_size * 8

                # there is no channel dependency
                gp_dep.append(None)

                # add channel block
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VALUE,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "flags": 0,
                }
                if invalidation_bytes_nr and signal.invalidation_bits is not None:
                    inval_bits.append(signal.invalidation_bits)
                    kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
                    kwargs["pos_invalidation_bit"] = inval_cntr
                    inval_cntr += 1

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = signal.unit
                ch.comment = signal.comment
                ch.display_names = signal.display_names
                ch.dtype_fmt = s_dtype

                # source for channel
                source = signal.source
                if source:
                    if source in si_map:
                        ch.source = si_map[source]
                    else:
                        new_source = SourceInformation(
                            source_type=source.source_type, bus_type=source.bus_type
                        )
                        new_source.name = source.name
                        new_source.path = source.path
                        new_source.comment = source.comment

                        si_map[source] = new_source

                        ch.source = new_source

                gp_channels.append(ch)

                offset += byte_size

                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in ch.display_names:
                    self.channels_db.add(_name, entry)

                gp_sdata.append(None)

                ch_cntr += 1

            elif sig_type == v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:
                (
                    offset,
                    dg_cntr,
                    ch_cntr,
                    struct_self,
                    new_fields,
                    inval_cntr,
                ) = self._append_structure_composition(
                    gp,
                    signal,
                    offset,
                    dg_cntr,
                    ch_cntr,
                    defined_texts,
                    invalidation_bytes_nr,
                    inval_bits,
                    inval_cntr,
                )
                fields.extend(new_fields)

            elif sig_type == v4c.SIGNAL_TYPE_ARRAY:
                # here we have channel arrays or mdf v3 channel dependencies
                samples = signal.samples[names[0]]
                shape = samples.shape[1:]

                if len(names) > 1 or len(shape) > 1:
                    # add channel dependency block for composed parent channel
                    dims_nr = len(shape)
                    names_nr = len(names)

                    if names_nr == 0:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_LOOKUP,
                            "flags": v4c.FLAG_CA_FIXED_AXIS,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    elif len(names) == 1:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_ARRAY,
                            "flags": 0,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    else:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_LOOKUP,
                            "flags": v4c.FLAG_CA_AXIS,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    parent_dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([parent_dep])

                else:
                    # add channel dependency block for composed parent channel
                    kwargs = {
                        "dims": 1,
                        "ca_type": v4c.CA_TYPE_ARRAY,
                        "flags": 0,
                        "byte_offset_base": samples.dtype.itemsize,
                        "dim_size_0": shape[0],
                    }
                    parent_dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([parent_dep])

                # first we add the structure channel
                s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape, True)

                # add channel block
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VALUE,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "flags": 0,
                }

                if invalidation_bytes_nr:
                    if signal.invalidation_bits is not None:
                        inval_bits.append(signal.invalidation_bits)
                        kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
                        kwargs["pos_invalidation_bit"] = inval_cntr
                        inval_cntr += 1

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = signal.unit
                ch.comment = signal.comment
                ch.display_names = signal.display_names
                ch.dtype_fmt = samples.dtype

                record.append(
                    (
                        samples.dtype,
                        samples.dtype.itemsize,
                        offset,
                        0,
                    )
                )

                # source for channel
                source = signal.source
                if source:
                    if source in si_map:
                        ch.source = si_map[source]
                    else:
                        new_source = SourceInformation(
                            source_type=source.source_type, bus_type=source.bus_type
                        )
                        new_source.name = source.name
                        new_source.path = source.path
                        new_source.comment = source.comment

                        si_map[source] = new_source

                        ch.source = new_source

                gp_channels.append(ch)

                size = s_size // 8
                for dim in shape:
                    size *= dim
                offset += size

                fields.append((samples.tobytes(), size))

                gp_sdata.append(None)
                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in ch.display_names:
                    self.channels_db.add(_name, entry)

                ch_cntr += 1

                for name in names[1:]:

                    samples = signal.samples[name]
                    shape = samples.shape[1:]

                    # add channel dependency block
                    kwargs = {
                        "dims": 1,
                        "ca_type": v4c.CA_TYPE_SCALE_AXIS,
                        "flags": 0,
                        "byte_offset_base": samples.dtype.itemsize,
                        "dim_size_0": shape[0],
                    }
                    dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([dep])

                    # add components channel
                    s_type, s_size = fmt_to_datatype_v4(samples.dtype, ())
                    byte_size = s_size // 8 or 1
                    kwargs = {
                        "channel_type": v4c.CHANNEL_TYPE_VALUE,
                        "bit_count": s_size,
                        "byte_offset": offset,
                        "bit_offset": 0,
                        "data_type": s_type,
                        "flags": 0,
                    }

                    if invalidation_bytes_nr:
                        if signal.invalidation_bits is not None:
                            inval_bits.append(signal.invalidation_bits)
                            kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
                            kwargs["pos_invalidation_bit"] = inval_cntr
                            inval_cntr += 1

                    ch = Channel(**kwargs)
                    ch.name = name
                    ch.unit = signal.unit
                    ch.comment = signal.comment
                    ch.display_names = signal.display_names
                    ch.dtype_fmt = samples.dtype

                    record.append(
                        (
                            samples.dtype,
                            samples.dtype.itemsize,
                            offset,
                            0,
                        )
                    )

                    gp_channels.append(ch)

                    entry = dg_cntr, ch_cntr
                    parent_dep.axis_channels.append(entry)
                    for dim in shape:
                        byte_size *= dim
                    offset += byte_size

                    fields.append((samples.tobytes(), byte_size))

                    gp_sdata.append(None)
                    self.channels_db.add(name, entry)

                    ch_cntr += 1

            else:

                encoding = signal.encoding
                samples = signal.samples
                sig_dtype = samples.dtype

                if encoding == "utf-8":
                    data_type = v4c.DATA_TYPE_STRING_UTF_8
                elif encoding == "latin-1":
                    data_type = v4c.DATA_TYPE_STRING_LATIN_1
                elif encoding == "utf-16-be":
                    data_type = v4c.DATA_TYPE_STRING_UTF_16_BE
                elif encoding == "utf-16-le":
                    data_type = v4c.DATA_TYPE_STRING_UTF_16_LE
                else:
                    raise MdfException(f'wrong encoding "{encoding}" for string signal')

                if self.compact_vlsd:
                    data = []
                    offsets = []
                    off = 0
                    if encoding == "utf-16-le":
                        for elem in samples:
                            offsets.append(off)
                            size = len(elem)
                            if size % 2:
                                size += 1
                                elem = elem + b"\0"
                            data.append(UINT32_p(size))
                            data.append(elem)
                            off += size + 4
                    else:
                        for elem in samples:
                            offsets.append(off)
                            size = len(elem)
                            data.append(UINT32_p(size))
                            data.append(elem)
                            off += size + 4

                    data_size = off
                    offsets = array(offsets, dtype=uint64)
                    if data_size:
                        data_addr = tell()
                        info = SignalDataBlockInfo(
                            address=data_addr,
                            compressed_size=data_size,
                            original_size=data_size,
                            location=v4c.LOCATION_TEMPORARY_FILE,
                        )
                        gp_sdata.append(
                            (
                                [info],
                                iter(EMPTY_TUPLE),
                            )
                        )
                        file.seek(0, 2)
                        file.write(b"".join(data))
                    else:
                        data_addr = 0
                        gp_sdata.append(
                            (
                                [],
                                iter(EMPTY_TUPLE),
                            )
                        )
                else:

                    offsets = arange(len(samples), dtype=uint64) * (
                        signal.samples.itemsize + 4
                    )

                    values = [
                        full(len(samples), samples.itemsize, dtype=uint32),
                        samples,
                    ]

                    types_ = [("o", uint32), ("s", sig_dtype)]

                    data = fromarrays(values, dtype=types_)

                    data_size = len(data) * data.itemsize
                    if data_size:
                        data_addr = tell()
                        info = SignalDataBlockInfo(
                            address=data_addr,
                            compressed_size=data_size,
                            original_size=data_size,
                            location=v4c.LOCATION_TEMPORARY_FILE,
                        )
                        gp_sdata.append(
                            (
                                [info],
                                iter(EMPTY_TUPLE),
                            )
                        )
                        data.tofile(file)
                    else:
                        data_addr = 0
                        gp_sdata.append(
                            (
                                [],
                                iter(EMPTY_TUPLE),
                            )
                        )

                # compute additional byte offset for large records size
                byte_size = 8
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VLSD,
                    "bit_count": 64,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": data_type,
                    "data_block_addr": data_addr,
                    "flags": 0,
                }

                if invalidation_bytes_nr:
                    if signal.invalidation_bits is not None:
                        inval_bits.append(signal.invalidation_bits)
                        kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
                        kwargs["pos_invalidation_bit"] = inval_cntr
                        inval_cntr += 1

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = signal.unit
                ch.comment = signal.comment
                ch.display_names = signal.display_names
                ch.dtype_fmt = dtype("<u8")

                # conversions for channel
                conversion = conversion_transfer(signal.conversion, version=4)
                if signal.raw:
                    ch.conversion = conversion

                # source for channel
                source = signal.source
                if source:
                    if source in si_map:
                        ch.source = si_map[source]
                    else:
                        new_source = SourceInformation(
                            source_type=source.source_type, bus_type=source.bus_type
                        )
                        new_source.name = source.name
                        new_source.path = source.path
                        new_source.comment = source.comment

                        si_map[source] = new_source

                        ch.source = new_source

                gp_channels.append(ch)

                record.append(
                    (
                        uint64,
                        8,
                        offset,
                        0,
                    )
                )

                offset += byte_size

                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in ch.display_names:
                    self.channels_db.add(_name, entry)

                fields.append((offsets.tobytes(), 8))

                ch_cntr += 1

                # simple channels don't have channel dependencies
                gp_dep.append(None)

        if invalidation_bytes_nr:
            invalidation_bytes_nr = len(inval_bits)

            for _ in range(8 - invalidation_bytes_nr % 8):
                inval_bits.append(zeros(cycles_nr, dtype=bool))

            inval_bits.reverse()
            invalidation_bytes_nr = len(inval_bits) // 8

            gp.channel_group.invalidation_bytes_nr = invalidation_bytes_nr

            inval_bits = fliplr(
                packbits(array(inval_bits).T).reshape(
                    (cycles_nr, invalidation_bytes_nr)
                )
            )

            if self.version < "4.20":

                fields.append((inval_bits.tobytes(), invalidation_bytes_nr))

        gp.channel_group.cycles_nr = cycles_nr
        gp.channel_group.samples_byte_nr = offset

        virtual_group = VirtualChannelGroup()
        self.virtual_groups[dg_cntr] = virtual_group
        self.virtual_groups_map[dg_cntr] = dg_cntr
        virtual_group.groups.append(dg_cntr)
        virtual_group.record_size = offset + invalidation_bytes_nr
        virtual_group.cycles_nr = cycles_nr

        # data group
        gp.data_group = DataGroup()

        gp.sorted = True

        samples = data_block_from_arrays(fields, cycles_nr)
        size = len(samples)
        samples = memoryview(samples)

        del fields

        if size:
            if self.version < "4.20":

                block_size = self._write_fragment_size or 20 * 1024 * 1024

                count = ceil(size / block_size)

                for i in range(count):
                    data_ = samples[i * block_size : (i + 1) * block_size]
                    raw_size = len(data_)
                    data_ = lz_compress(data_)

                    size = len(data_)
                    data_address = self._tempfile.tell()
                    self._tempfile.write(data_)

                    gp.data_blocks.append(
                        DataBlockInfo(
                            address=data_address,
                            block_type=v4c.DZ_BLOCK_LZ,
                            original_size=raw_size,
                            compressed_size=size,
                            param=0,
                        )
                    )

            else:
                data_address = self._tempfile.tell()
                gp.uses_ld = True
                data_address = tell()

                data = samples
                raw_size = len(data)
                data = lz_compress(data)

                size = len(data)
                self._tempfile.write(data)

                gp.data_blocks.append(
                    DataBlockInfo(
                        address=data_address,
                        block_type=v4c.DZ_BLOCK_LZ,
                        original_size=raw_size,
                        compressed_size=size,
                        param=0,
                    )
                )

                if inval_bits is not None:
                    addr = tell()
                    data = inval_bits.tobytes()
                    raw_size = len(data)
                    data = lz_compress(data)
                    size = len(data)
                    self._tempfile.write(data)

                    gp.data_blocks[-1].invalidation_block(
                        InvalidationBlockInfo(
                            address=addr,
                            block_type=v4c.DZ_BLOCK_LZ,
                            original_size=raw_size,
                            compressed_size=size,
                            param=None,
                        )
                    )

        gp.data_location = v4c.LOCATION_TEMPORARY_FILE

        return dg_cntr

    def _append_column_oriented(
        self,
        signals: list[Signal],
        acq_name: str | None = None,
        acq_source: Source | None = None,
        comment: str | None = None,
    ) -> int:
        defined_texts = {}
        si_map = self._si_map

        # setup all blocks related to the time master channel

        file = self._tempfile
        tell = file.tell
        seek = file.seek
        write = file.write

        seek(0, 2)

        dg_cntr = initial_dg_cntr = len(self.groups)

        # add the master group

        gp = Group(None)
        gp.signal_data = gp_sdata = []
        gp.channels = gp_channels = []
        gp.channel_dependencies = gp_dep = []
        gp.signal_types = gp_sig_types = []
        gp.uses_ld = True
        gp.data_group = DataGroup()
        gp.sorted = True
        gp.record = record = []

        samples = signals[0].timestamps

        cycles_nr = len(samples)

        # channel group
        kwargs = {"cycles_nr": cycles_nr, "samples_byte_nr": 0}
        gp.channel_group = remote_master_channel_group = ChannelGroup(**kwargs)
        gp.channel_group.acq_name = acq_name
        gp.channel_group.acq_source = acq_source
        gp.channel_group.comment = comment

        self.groups.append(gp)

        ch_cntr = 0
        types = []
        ch_cntr = 0
        offset = 0

        prepare_record = True
        source_block = None

        master_metadata = signals[0].master_metadata
        if master_metadata:
            time_name, sync_type = master_metadata
            if sync_type in (0, 1):
                time_unit = "s"
            elif sync_type == 2:
                time_unit = "deg"
            elif sync_type == 3:
                time_unit = "m"
            elif sync_type == 4:
                time_unit = "index"
        else:
            time_name, sync_type = "time", v4c.SYNC_TYPE_TIME
            time_unit = "s"

        gp.channel_group.acq_source = source_block
        # time channel
        t_type, t_size = fmt_to_datatype_v4(samples.dtype, samples.shape)
        kwargs = {
            "channel_type": v4c.CHANNEL_TYPE_MASTER,
            "data_type": t_type,
            "sync_type": sync_type,
            "byte_offset": 0,
            "bit_offset": 0,
            "bit_count": t_size,
        }

        ch = Channel(**kwargs)
        ch.unit = time_unit
        ch.name = time_name
        ch.source = source_block
        ch.dtype_fmt = samples.dtype
        name = time_name

        gp_channels.append(ch)

        gp_sdata.append(None)
        self.channels_db.add(name, (dg_cntr, ch_cntr))
        self.masters_db[dg_cntr] = 0

        record.append(
            (
                samples.dtype,
                samples.dtype.itemsize,
                offset,
                0,
            )
        )

        # time channel doesn't have channel dependencies
        gp_dep.append(None)

        types.append((name, samples.dtype))

        offset += t_size // 8
        ch_cntr += 1

        gp_sig_types.append(0)

        gp.channel_group.samples_byte_nr = offset

        # data group
        gp.data_group = DataGroup()

        # data block
        types = dtype(types)

        gp.sorted = True

        size = cycles_nr * samples.itemsize

        cg_master_index = dg_cntr

        virtual_group = VirtualChannelGroup()
        self.virtual_groups[cg_master_index] = virtual_group
        self.virtual_groups_map[dg_cntr] = dg_cntr
        virtual_group.groups.append(dg_cntr)
        virtual_group.record_size = offset
        virtual_group.cycles_nr = cycles_nr

        dg_cntr += 1

        if size:
            data_address = tell()
            gp.data_location = v4c.LOCATION_TEMPORARY_FILE
            write(samples.tobytes())

            chunk = self._write_fragment_size // samples.itemsize
            chunk *= samples.itemsize

            while size:

                if size > chunk:
                    gp.data_blocks.append(
                        DataBlockInfo(
                            address=data_address,
                            block_type=v4c.DT_BLOCK,
                            original_size=chunk,
                            compressed_size=chunk,
                            param=0,
                        )
                    )
                    data_address += chunk
                    size -= chunk
                else:
                    gp.data_blocks.append(
                        DataBlockInfo(
                            address=data_address,
                            block_type=v4c.DT_BLOCK,
                            original_size=size,
                            compressed_size=size,
                            param=0,
                        )
                    )
                    size = 0
        else:
            gp.data_location = v4c.LOCATION_TEMPORARY_FILE

        for signal in signals:
            gp = Group(None)
            gp.signal_data = gp_sdata = []
            gp.channels = gp_channels = []
            gp.channel_dependencies = gp_dep = []
            gp.signal_types = gp_sig_types = []
            gp.data_group = DataGroup()
            gp.sorted = True
            gp.uses_ld = True
            gp.record = record = []

            # channel group
            kwargs = {
                "cycles_nr": cycles_nr,
                "samples_byte_nr": 0,
                "flags": v4c.FLAG_CG_REMOTE_MASTER,
            }
            gp.channel_group = ChannelGroup(**kwargs)
            gp.channel_group.acq_name = acq_name
            gp.channel_group.acq_source = acq_source
            gp.channel_group.comment = remote_master_channel_group.comment
            gp.channel_group.cg_master_index = cg_master_index

            self.groups.append(gp)

            types = []
            ch_cntr = 0
            offset = 0
            field_names = UniqueDB()

            sig = signal
            samples = sig.samples
            sig_dtype = samples.dtype
            sig_shape = samples.shape
            names = sig_dtype.names
            name = signal.name

            if names is None:
                sig_type = v4c.SIGNAL_TYPE_SCALAR
                if sig_dtype.kind in "SV":
                    sig_type = v4c.SIGNAL_TYPE_STRING
            else:
                if names in (v4c.CANOPEN_TIME_FIELDS, v4c.CANOPEN_DATE_FIELDS):
                    sig_type = v4c.SIGNAL_TYPE_CANOPEN
                elif names[0] != sig.name:
                    sig_type = v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION
                else:
                    sig_type = v4c.SIGNAL_TYPE_ARRAY

            gp_sig_types.append(sig_type)

            # first add the signals in the simple signal list
            if sig_type == v4c.SIGNAL_TYPE_SCALAR:

                # compute additional byte offset for large records size
                s_type, s_size = fmt_to_datatype_v4(sig_dtype, sig_shape)

                byte_size = s_size // 8 or 1

                if sig_dtype.kind == "u" and signal.bit_count <= 4:
                    s_size = signal.bit_count

                if signal.stream_sync:
                    channel_type = v4c.CHANNEL_TYPE_SYNC
                    if signal.attachment:
                        at_data, at_name, hash_sum = signal.attachment
                        attachment_addr = self.attach(
                            at_data, at_name, hash_sum, mime="video/avi", embedded=False
                        )
                        data_block_addr = attachment_addr
                    else:
                        data_block_addr = 0

                    sync_type = v4c.SYNC_TYPE_TIME
                else:
                    channel_type = v4c.CHANNEL_TYPE_VALUE
                    data_block_addr = 0
                    sync_type = v4c.SYNC_TYPE_NONE

                kwargs = {
                    "channel_type": channel_type,
                    "sync_type": sync_type,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "data_block_addr": data_block_addr,
                    "flags": 0,
                }

                if signal.invalidation_bits is not None:
                    invalidation_bits = signal.invalidation_bits
                    kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
                    kwargs["pos_invalidation_bit"] = 0
                else:
                    invalidation_bits = None

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = signal.unit
                ch.comment = signal.comment
                ch.display_names = signal.display_names

                # conversions for channel
                if signal.raw:
                    ch.conversion = conversion_transfer(signal.conversion, version=4)

                # source for channel
                source = signal.source
                if source:
                    if source in si_map:
                        ch.source = si_map[source]
                    else:
                        new_source = SourceInformation(
                            source_type=source.source_type, bus_type=source.bus_type
                        )
                        new_source.name = source.name
                        new_source.path = source.path
                        new_source.comment = source.comment

                        si_map[source] = new_source

                        ch.source = new_source

                gp_channels.append(ch)

                gp_sdata.append(None)
                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in ch.display_names:
                    self.channels_db.add(_name, entry)

                _shape = sig_shape[1:]
                types.append((name, sig_dtype, _shape))
                gp.single_channel_dtype = ch.dtype_fmt = dtype((sig_dtype, _shape))

                record.append(
                    (
                        ch.dtype_fmt,
                        ch.dtype_fmt.itemsize,
                        0,
                        0,
                    )
                )

                offset = byte_size

                # simple channels don't have channel dependencies
                gp_dep.append(None)

            elif sig_type == v4c.SIGNAL_TYPE_CANOPEN:

                if names == v4c.CANOPEN_TIME_FIELDS:
                    record.append(
                        (
                            dtype("V6"),
                            6,
                            0,
                            0,
                        )
                    )

                    types.append((name, "V6"))
                    gp.single_channel_dtype = dtype("V6")
                    byte_size = 6
                    s_type = v4c.DATA_TYPE_CANOPEN_TIME

                else:
                    record.append(
                        (
                            dtype("V7"),
                            7,
                            0,
                            0,
                        )
                    )
                    vals = []
                    for field in ("ms", "min", "hour", "day", "month", "year"):
                        if field == "hour":
                            vals.append(
                                signal.samples[field]
                                + (signal.samples["summer_time"] << 7)
                            )
                        elif field == "day":
                            vals.append(
                                signal.samples[field]
                                + (signal.samples["day_of_week"] << 4)
                            )
                        else:
                            vals.append(signal.samples[field])
                    samples = fromarrays(vals)

                    types.append((name, "V7"))
                    gp.single_channel_dtype = dtype("V7")
                    byte_size = 7
                    s_type = v4c.DATA_TYPE_CANOPEN_DATE

                s_size = byte_size * 8

                # there is no channel dependency
                gp_dep.append(None)

                # add channel block
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VALUE,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "flags": 0,
                }
                if signal.invalidation_bits is not None:
                    invalidation_bits = signal.invalidation_bits
                    kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
                    kwargs["pos_invalidation_bit"] = 0
                else:
                    invalidation_bits = None

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = signal.unit
                ch.comment = signal.comment
                ch.display_names = signal.display_names
                ch.dtype_fmt = gp.single_channel_dtype

                # source for channel
                source = signal.source
                if source:
                    if source in si_map:
                        ch.source = si_map[source]
                    else:
                        new_source = SourceInformation(
                            source_type=source.source_type, bus_type=source.bus_type
                        )
                        new_source.name = source.name
                        new_source.path = source.path
                        new_source.comment = source.comment

                        si_map[source] = new_source

                        ch.source = new_source

                gp_channels.append(ch)

                offset = byte_size

                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in ch.display_names:
                    self.channels_db.add(_name, entry)

                gp_sdata.append(None)

            elif sig_type == v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:
                (
                    offset,
                    dg_cntr,
                    ch_cntr,
                    struct_self,
                    new_fields,
                    new_types,
                ) = self._append_structure_composition_column_oriented(
                    gp,
                    signal,
                    field_names,
                    offset,
                    dg_cntr,
                    ch_cntr,
                    defined_texts,
                )

                if signal.invalidation_bits is not None:
                    invalidation_bits = signal.invalidation_bits
                else:
                    invalidation_bits = None

                gp["types"] = dtype(new_types)
                offset = gp["types"].itemsize

                samples = signal.samples

            elif sig_type == v4c.SIGNAL_TYPE_ARRAY:
                fields = []
                # here we have channel arrays or mdf v3 channel dependencies
                samples = signal.samples[names[0]]
                shape = samples.shape[1:]

                if len(names) > 1 or len(shape) > 1:
                    # add channel dependency block for composed parent channel
                    dims_nr = len(shape)
                    names_nr = len(names)

                    if names_nr == 0:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_LOOKUP,
                            "flags": v4c.FLAG_CA_FIXED_AXIS,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    elif len(names) == 1:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_ARRAY,
                            "flags": 0,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    else:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_LOOKUP,
                            "flags": v4c.FLAG_CA_AXIS,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    parent_dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([parent_dep])

                else:
                    # add channel dependency block for composed parent channel
                    kwargs = {
                        "dims": 1,
                        "ca_type": v4c.CA_TYPE_SCALE_AXIS,
                        "flags": 0,
                        "byte_offset_base": samples.dtype.itemsize,
                        "dim_size_0": shape[0],
                    }
                    parent_dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([parent_dep])

                field_name = field_names.get_unique_name(name)

                fields.append(samples)
                dtype_pair = field_name, samples.dtype, shape
                types.append(dtype_pair)

                record.append(
                    (
                        samples.dtype,
                        samples.dtype.itemsize,
                        offset,
                        0,
                    )
                )

                # first we add the structure channel
                s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape, True)

                # add channel block
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VALUE,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "flags": 0,
                }

                if signal.invalidation_bits is not None:
                    invalidation_bits = signal.invalidation_bits
                    kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
                    kwargs["pos_invalidation_bit"] = 0
                else:
                    invalidation_bits = None

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = signal.unit
                ch.comment = signal.comment
                ch.display_names = signal.display_names
                ch.dtype_fmt = samples.dtype

                # source for channel
                source = signal.source
                if source:
                    if source in si_map:
                        ch.source = si_map[source]
                    else:
                        new_source = SourceInformation(
                            source_type=source.source_type, bus_type=source.bus_type
                        )
                        new_source.name = source.name
                        new_source.path = source.path
                        new_source.comment = source.comment

                        si_map[source] = new_source

                        ch.source = new_source

                gp_channels.append(ch)

                size = s_size // 8
                for dim in shape:
                    size *= dim
                offset += size

                gp_sdata.append(None)
                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in ch.display_names:
                    self.channels_db.add(_name, entry)

                ch_cntr += 1

                for name in names[1:]:
                    field_name = field_names.get_unique_name(name)

                    samples = signal.samples[name]
                    shape = samples.shape[1:]
                    fields.append(samples)
                    types.append((field_name, samples.dtype, shape))

                    record.append(
                        (
                            samples.dtype,
                            samples.dtype.itemsize,
                            offset,
                            0,
                        )
                    )

                    # add channel dependency block
                    kwargs = {
                        "dims": 1,
                        "ca_type": v4c.CA_TYPE_SCALE_AXIS,
                        "flags": 0,
                        "byte_offset_base": samples.dtype.itemsize,
                        "dim_size_0": shape[0],
                    }
                    dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([dep])

                    # add components channel
                    s_type, s_size = fmt_to_datatype_v4(samples.dtype, ())
                    byte_size = s_size // 8 or 1
                    kwargs = {
                        "channel_type": v4c.CHANNEL_TYPE_VALUE,
                        "bit_count": s_size,
                        "byte_offset": offset,
                        "bit_offset": 0,
                        "data_type": s_type,
                        "flags": 0,
                    }

                    if signal.invalidation_bits is not None:
                        invalidation_bits = signal.invalidation_bits
                        kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
                        kwargs["pos_invalidation_bit"] = 0
                    else:
                        invalidation_bits = None

                    ch = Channel(**kwargs)
                    ch.name = name
                    ch.unit = signal.unit
                    ch.comment = signal.comment
                    ch.display_names = signal.display_names
                    ch.dtype_fmt = samples.dtype

                    gp_channels.append(ch)

                    entry = dg_cntr, ch_cntr
                    parent_dep.axis_channels.append(entry)
                    for dim in shape:
                        byte_size *= dim
                    offset += byte_size

                    gp_sdata.append(None)
                    self.channels_db.add(name, entry)

                    ch_cntr += 1

                gp["types"] = dtype(types)

                samples = signal.samples

            else:

                encoding = signal.encoding
                samples = signal.samples
                sig_dtype = samples.dtype

                if encoding == "utf-8":
                    data_type = v4c.DATA_TYPE_STRING_UTF_8
                elif encoding == "latin-1":
                    data_type = v4c.DATA_TYPE_STRING_LATIN_1
                elif encoding == "utf-16-be":
                    data_type = v4c.DATA_TYPE_STRING_UTF_16_BE
                elif encoding == "utf-16-le":
                    data_type = v4c.DATA_TYPE_STRING_UTF_16_LE
                else:
                    raise MdfException(f'wrong encoding "{encoding}" for string signal')

                offsets = arange(len(samples), dtype=uint64) * (
                    signal.samples.itemsize + 4
                )

                values = [full(len(samples), samples.itemsize, dtype=uint32), samples]

                types_ = [("o", uint32), ("s", sig_dtype)]

                data = fromarrays(values, dtype=types_)

                data_size = len(data) * data.itemsize
                if data_size:
                    data_addr = tell()
                    info = SignalDataBlockInfo(
                        address=data_addr,
                        compressed_size=data_size,
                        original_size=data_size,
                        location=v4c.LOCATION_TEMPORARY_FILE,
                    )
                    gp_sdata.append(
                        (
                            [info],
                            iter(EMPTY_TUPLE),
                        )
                    )
                    data.tofile(file)
                else:
                    data_addr = 0
                    gp_sdata.append(
                        (
                            [],
                            iter(EMPTY_TUPLE),
                        )
                    )

                # compute additional byte offset for large records size
                byte_size = 8
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VLSD,
                    "bit_count": 64,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": data_type,
                    "data_block_addr": data_addr,
                    "flags": 0,
                }

                if signal.invalidation_bits is not None:
                    invalidation_bits = signal.invalidation_bits
                    kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
                    kwargs["pos_invalidation_bit"] = 0
                else:
                    invalidation_bits = None

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = signal.unit
                ch.comment = signal.comment
                ch.display_names = signal.display_names

                # conversions for channel
                conversion = conversion_transfer(signal.conversion, version=4)
                if signal.raw:
                    ch.conversion = conversion

                # source for channel
                source = signal.source
                if source:
                    if source in si_map:
                        ch.source = si_map[source]
                    else:
                        new_source = SourceInformation(
                            source_type=source.source_type, bus_type=source.bus_type
                        )
                        new_source.name = source.name
                        new_source.path = source.path
                        new_source.comment = source.comment

                        si_map[source] = new_source

                        ch.source = new_source

                gp_channels.append(ch)

                record.append(
                    (
                        uint64,
                        8,
                        offset,
                        0,
                    )
                )

                offset = byte_size

                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in ch.display_names:
                    self.channels_db.add(_name, entry)

                types.append((name, uint64))
                gp.single_channel_dtype = ch.dtype_fmt = uint64

                samples = offsets

                # simple channels don't have channel dependencies
                gp_dep.append(None)

            gp.channel_group.samples_byte_nr = offset
            if invalidation_bits is not None:
                gp.channel_group.invalidation_bytes_nr = 1

            virtual_group.groups.append(dg_cntr)
            self.virtual_groups_map[dg_cntr] = cg_master_index

            virtual_group.record_size += offset
            if signal.invalidation_bits:
                virtual_group.record_size += 1

            dg_cntr += 1
            size = cycles_nr * samples.itemsize
            if size:

                data_address = tell()

                data = samples.tobytes()
                raw_size = len(data)
                data = lz_compress(data)

                size = len(data)
                write(data)

                gp.data_blocks.append(
                    DataBlockInfo(
                        address=data_address,
                        block_type=v4c.DZ_BLOCK_LZ,
                        original_size=raw_size,
                        compressed_size=size,
                        param=0,
                    )
                )

                if invalidation_bits is not None:
                    addr = tell()
                    data = invalidation_bits.tobytes()
                    raw_size = len(data)
                    data = lz_compress(data)
                    size = len(data)
                    write(data)

                    gp.data_blocks[-1].invalidation_block(
                        InvalidationBlockInfo(
                            address=addr,
                            block_type=v4c.DZ_BLOCK_LZ,
                            original_size=raw_size,
                            compressed_size=size,
                            param=None,
                        )
                    )

            gp.data_location = v4c.LOCATION_TEMPORARY_FILE

        return initial_dg_cntr

    def _append_dataframe(
        self,
        df: DataFrame,
        acq_name: str | None = None,
        acq_source: Source | None = None,
        comment: str | None = None,
        units: dict[str, str | bytes] = None,
    ) -> None:
        """
        Appends a new data group from a Pandas data frame.

        """
        units = units or {}

        if df.shape == (0, 0):
            return

        t = df.index
        index_name = df.index.name
        time_name = index_name or "time"
        sync_type = v4c.SYNC_TYPE_TIME
        time_unit = "s"

        dg_cntr = len(self.groups)

        gp = Group(None)
        gp.signal_data = gp_sdata = []
        gp.channels = gp_channels = []
        gp.channel_dependencies = gp_dep = []
        gp.signal_types = gp_sig_types = []
        gp.record = record = []

        cycles_nr = len(t)

        # channel group
        kwargs = {"cycles_nr": cycles_nr, "samples_byte_nr": 0}
        gp.channel_group = ChannelGroup(**kwargs)
        gp.channel_group.acq_name = acq_name
        gp.channel_group.acq_source = acq_source
        gp.channel_group.comment = comment

        self.groups.append(gp)

        fields = []
        types = []
        ch_cntr = 0
        offset = 0
        field_names = UniqueDB()

        # setup all blocks related to the time master channel

        file = self._tempfile
        tell = file.tell
        seek = file.seek

        seek(0, 2)

        virtual_group = VirtualChannelGroup()
        self.virtual_groups[dg_cntr] = virtual_group
        self.virtual_groups_map[dg_cntr] = dg_cntr
        virtual_group.groups.append(dg_cntr)
        virtual_group.cycles_nr = cycles_nr

        # time channel
        t_type, t_size = fmt_to_datatype_v4(t.dtype, t.shape)
        kwargs = {
            "channel_type": v4c.CHANNEL_TYPE_MASTER,
            "data_type": t_type,
            "sync_type": sync_type,
            "byte_offset": 0,
            "bit_offset": 0,
            "bit_count": t_size,
            "min_raw_value": t[0] if cycles_nr else 0,
            "max_raw_value": t[-1] if cycles_nr else 0,
            "lower_limit": t[0] if cycles_nr else 0,
            "upper_limit": t[-1] if cycles_nr else 0,
            "flags": v4c.FLAG_PHY_RANGE_OK | v4c.FLAG_VAL_RANGE_OK,
        }
        ch = Channel(**kwargs)
        ch.unit = time_unit
        ch.name = time_name
        ch.dtype_fmt = t.dtype
        name = time_name
        gp_channels.append(ch)

        gp_sdata.append(None)
        self.channels_db.add(name, (dg_cntr, ch_cntr))
        self.masters_db[dg_cntr] = 0

        record.append(
            (
                t.dtype,
                t.dtype.itemsize,
                offset,
                0,
            )
        )

        # time channel doesn't have channel dependencies
        gp_dep.append(None)

        fields.append(t)
        types.append((name, t.dtype))
        field_names.get_unique_name(name)

        offset += t_size // 8
        ch_cntr += 1

        gp_sig_types.append(0)

        for signal in df:
            if index_name == signal:
                continue

            sig = df[signal]
            name = signal

            sig_type = v4c.SIGNAL_TYPE_SCALAR
            if sig.dtype.kind in "SV":
                sig_type = v4c.SIGNAL_TYPE_STRING

            gp_sig_types.append(sig_type)

            # first add the signals in the simple signal list
            if sig_type == v4c.SIGNAL_TYPE_SCALAR:

                # compute additional byte offset for large records size
                if sig.dtype.kind == "O":
                    sig = encode(sig.values.astype(str), "utf-8")

                s_type, s_size = fmt_to_datatype_v4(sig.dtype, sig.shape)

                byte_size = s_size // 8 or 1

                channel_type = v4c.CHANNEL_TYPE_VALUE
                data_block_addr = 0
                sync_type = v4c.SYNC_TYPE_NONE

                kwargs = {
                    "channel_type": channel_type,
                    "sync_type": sync_type,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "data_block_addr": data_block_addr,
                }

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = units.get(name, "")
                ch.dtype_fmt = dtype((sig.dtype, sig.shape[1:]))

                record.append(
                    (
                        ch.dtype_fmt,
                        ch.dtype_fmt.itemsize,
                        offset,
                        0,
                    )
                )

                gp_channels.append(ch)

                offset += byte_size

                gp_sdata.append(None)
                self.channels_db.add(name, (dg_cntr, ch_cntr))

                field_name = field_names.get_unique_name(name)

                fields.append(sig)
                types.append((field_name, sig.dtype, sig.shape[1:]))

                ch_cntr += 1

                # simple channels don't have channel dependencies
                gp_dep.append(None)

            elif sig_type == v4c.SIGNAL_TYPE_STRING:
                offsets = arange(len(sig), dtype=uint64) * (sig.dtype.itemsize + 4)

                values = [full(len(sig), sig.dtype.itemsize, dtype=uint32), sig.values]

                types_ = [("", uint32), ("", sig.dtype)]

                data = fromarrays(values, dtype=types_)

                data_size = len(data) * data.itemsize
                if data_size:
                    data_addr = tell()
                    info = SignalDataBlockInfo(
                        address=data_addr,
                        compressed_size=data_size,
                        original_size=data_size,
                        location=v4c.LOCATION_TEMPORARY_FILE,
                    )
                    gp_sdata.append(
                        (
                            [info],
                            iter(EMPTY_TUPLE),
                        )
                    )
                    data.tofile(file)
                else:
                    data_addr = 0
                    gp_sdata.append(
                        (
                            [],
                            iter(EMPTY_TUPLE),
                        )
                    )

                # compute additional byte offset for large records size
                byte_size = 8
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VLSD,
                    "bit_count": 64,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": v4c.DATA_TYPE_STRING_UTF_8,
                    "min_raw_value": 0,
                    "max_raw_value": 0,
                    "lower_limit": 0,
                    "upper_limit": 0,
                    "flags": 0,
                    "data_block_addr": data_addr,
                }

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = units.get(name, "")
                ch.dtype_fmt = dtype("<u8")

                gp_channels.append(ch)

                record.append(
                    (
                        uint64,
                        8,
                        offset,
                        0,
                    )
                )

                offset += byte_size

                self.channels_db.add(name, (dg_cntr, ch_cntr))

                field_name = field_names.get_unique_name(name)

                fields.append(offsets)
                types.append((field_name, uint64))

                ch_cntr += 1

                # simple channels don't have channel dependencies
                gp_dep.append(None)

        virtual_group.record_size = offset
        virtual_group.cycles_nr = cycles_nr

        gp.channel_group.cycles_nr = cycles_nr
        gp.channel_group.samples_byte_nr = offset

        # data group
        gp.data_group = DataGroup()

        # data block
        types = dtype(types)

        gp.sorted = True

        if df.shape[0]:
            samples = fromarrays(fields, dtype=types)
        else:
            samples = array([])

        size = len(samples) * samples.itemsize
        if size:
            data_address = self._tempfile.tell()
            gp.data_location = v4c.LOCATION_TEMPORARY_FILE

            samples.tofile(self._tempfile)

            self._tempfile.write(samples.tobytes())

            gp.data_blocks.append(
                DataBlockInfo(
                    address=data_address,
                    block_type=v4c.DT_BLOCK,
                    original_size=size,
                    compressed_size=size,
                    param=0,
                )
            )
        else:
            gp.data_location = v4c.LOCATION_TEMPORARY_FILE

    def _append_structure_composition(
        self,
        grp: Group,
        signal: Signal,
        offset: int,
        dg_cntr: int,
        ch_cntr: int,
        defined_texts: dict[str, int],
        invalidation_bytes_nr: int,
        inval_bits: list[NDArray[Any]],
        inval_cntr: int,
    ) -> tuple[
        int,
        int,
        int,
        tuple[int, int],
        list[NDArray[Any]],
        list[tuple[str, dtype[Any], tuple[int, ...]]],
        int,
    ]:
        si_map = self._si_map

        fields = []

        file = self._tempfile
        seek = file.seek
        seek(0, 2)

        gp = grp
        gp_sdata = gp.signal_data
        gp_channels = gp.channels
        gp_dep = gp.channel_dependencies
        record = gp.record

        name = signal.name
        names = signal.samples.dtype.names

        # first we add the structure channel

        if signal.attachment and signal.attachment[0]:
            at_data, at_name, hash_sum = signal.attachment
            if at_name is not None:
                suffix = Path(at_name).suffix.lower().strip(".")
            else:
                suffix = "dbc"
            if suffix == "a2l":
                mime = "applciation/A2L"
            else:
                mime = f"application/x-{suffix}"
            attachment_index = self.attach(
                at_data, at_name, hash_sum=hash_sum, mime=mime
            )
            attachment = attachment_index
        else:
            attachment = None

        # add channel block
        kwargs = {
            "channel_type": v4c.CHANNEL_TYPE_VALUE,
            "bit_count": signal.samples.dtype.itemsize * 8,
            "byte_offset": offset,
            "bit_offset": 0,
            "data_type": v4c.DATA_TYPE_BYTEARRAY,
            "precision": 0,
        }

        if attachment is not None:
            kwargs["attachment_addr"] = 0

        source_bus = signal.source and signal.source.source_type == v4c.SOURCE_BUS

        if source_bus:
            kwargs["flags"] = v4c.FLAG_CN_BUS_EVENT
            flags_ = v4c.FLAG_CN_BUS_EVENT
            grp.channel_group.flags |= (
                v4c.FLAG_CG_BUS_EVENT | v4c.FLAG_CG_PLAIN_BUS_EVENT
            )
        else:
            kwargs["flags"] = 0
            flags_ = 0

        if invalidation_bytes_nr and signal.invalidation_bits is not None:
            inval_bits.append(signal.invalidation_bits)
            kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
            kwargs["pos_invalidation_bit"] = inval_cntr
            inval_cntr += 1

        ch = Channel(**kwargs)
        ch.name = name
        ch.unit = signal.unit
        ch.comment = signal.comment
        ch.display_names = signal.display_names
        ch.attachment = attachment
        ch.dtype_fmt = signal.samples.dtype

        record.append((ch.dtype_fmt, ch.dtype_fmt.itemsize, offset, 0))

        if source_bus and grp.channel_group.acq_source is None:
            grp.channel_group.acq_source = SourceInformation.from_common_source(
                signal.source
            )

            if signal.source.bus_type == v4c.BUS_TYPE_CAN:
                grp.channel_group.path_separator = 46
                grp.channel_group.acq_name = "CAN"
            elif signal.source.bus_type == v4c.BUS_TYPE_FLEXRAY:
                grp.channel_group.path_separator = 46
                grp.channel_group.acq_name = "FLEXRAY"
            elif signal.source.bus_type == v4c.BUS_TYPE_ETHERNET:
                grp.channel_group.path_separator = 46
                grp.channel_group.acq_name = "ETHERNET"
            elif signal.source.bus_type == v4c.BUS_TYPE_K_LINE:
                grp.channel_group.path_separator = 46
                grp.channel_group.acq_name = "K_LINE"
            elif signal.source.bus_type == v4c.BUS_TYPE_MOST:
                grp.channel_group.path_separator = 46
                grp.channel_group.acq_name = "MOST"
            elif signal.source.bus_type == v4c.BUS_TYPE_LIN:
                grp.channel_group.path_separator = 46
                grp.channel_group.acq_name = "LIN"

        # source for channel
        source = signal.source
        if source:
            if source in si_map:
                ch.source = si_map[source]
            else:
                new_source = SourceInformation(
                    source_type=source.source_type, bus_type=source.bus_type
                )
                new_source.name = source.name
                new_source.path = source.path
                new_source.comment = source.comment

                si_map[source] = new_source

                ch.source = new_source

        entry = dg_cntr, ch_cntr
        gp_channels.append(ch)
        struct_self = entry

        gp_sdata.append(None)
        self.channels_db.add(name, entry)
        for _name in ch.display_names:
            self.channels_db.add(_name, entry)

        ch_cntr += 1

        dep_list = []
        gp_dep.append(dep_list)

        # then we add the fields
        for name in names:

            samples = signal.samples[name]
            fld_names = samples.dtype.names

            if fld_names is None:
                sig_type = v4c.SIGNAL_TYPE_SCALAR
                if samples.dtype.kind in "SV":
                    sig_type = v4c.SIGNAL_TYPE_STRING
            else:
                if fld_names in (v4c.CANOPEN_TIME_FIELDS, v4c.CANOPEN_DATE_FIELDS):
                    sig_type = v4c.SIGNAL_TYPE_CANOPEN
                elif fld_names[0] != name:
                    sig_type = v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION
                else:
                    sig_type = v4c.SIGNAL_TYPE_ARRAY

            if sig_type in (v4c.SIGNAL_TYPE_SCALAR, v4c.SIGNAL_TYPE_STRING):

                s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape)
                byte_size = s_size // 8 or 1

                # add channel block
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VALUE,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "flags": flags_,
                }

                if invalidation_bytes_nr:
                    if signal.invalidation_bits is not None:
                        inval_bits.append(signal.invalidation_bits)
                        kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
                        kwargs["pos_invalidation_bit"] = inval_cntr
                        inval_cntr += 1

                ch = Channel(**kwargs)
                ch.name = name
                ch.dtype_fmt = dtype((samples.dtype, samples.shape[1:]))

                record.append(
                    (
                        ch.dtype_fmt,
                        ch.dtype_fmt.itemsize,
                        offset,
                        0,
                    )
                )

                entry = (dg_cntr, ch_cntr)
                gp_channels.append(ch)
                dep_list.append(entry)

                offset += byte_size

                fields.append((samples.tobytes(), byte_size))

                gp_sdata.append(None)
                self.channels_db.add(name, entry)

                ch_cntr += 1
                gp_dep.append(None)

            elif sig_type == v4c.SIGNAL_TYPE_ARRAY:
                # here we have channel arrays or mdf v3 channel dependencies
                array_samples = samples
                names = samples.dtype.names
                samples = array_samples[names[0]]
                shape = samples.shape[1:]

                if len(names) > 1:
                    # add channel dependency block for composed parent channel
                    dims_nr = len(shape)
                    names_nr = len(names)

                    if names_nr == 0:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_LOOKUP,
                            "flags": v4c.FLAG_CA_FIXED_AXIS,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    elif len(names) == 1:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_ARRAY,
                            "flags": 0,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    else:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_LOOKUP,
                            "flags": v4c.FLAG_CA_AXIS,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    parent_dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([parent_dep])

                else:
                    # add channel dependency block for composed parent channel
                    kwargs = {
                        "dims": 1,
                        "ca_type": v4c.CA_TYPE_SCALE_AXIS,
                        "flags": 0,
                        "byte_offset_base": samples.dtype.itemsize,
                        "dim_size_0": shape[0],
                    }
                    parent_dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([parent_dep])

                record.append(
                    (
                        samples.dtype,
                        samples.dtype.itemsize,
                        offset,
                        0,
                    )
                )

                # first we add the structure channel
                s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape, True)

                # add channel block
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VALUE,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "flags": 0,
                }

                if invalidation_bytes_nr:
                    if signal.invalidation_bits is not None:
                        inval_bits.append(signal.invalidation_bits)
                        kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
                        kwargs["pos_invalidation_bit"] = inval_cntr
                        inval_cntr += 1

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = signal.unit
                ch.comment = signal.comment
                ch.display_names = signal.display_names
                ch.dtype_fmt = samples.dtype

                # source for channel
                source = signal.source
                if source:
                    if source in si_map:
                        ch.source = si_map[source]
                    else:
                        new_source = SourceInformation(
                            source_type=source.source_type, bus_type=source.bus_type
                        )
                        new_source.name = source.name
                        new_source.path = source.path
                        new_source.comment = source.comment

                        si_map[source] = new_source

                        ch.source = new_source

                gp_channels.append(ch)

                size = s_size // 8
                for dim in shape:
                    size *= dim
                offset += size

                fields.append((samples.tobytes(), size))

                gp_sdata.append(None)
                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in ch.display_names:
                    self.channels_db.add(_name, entry)

                ch_cntr += 1

                for name in names[1:]:

                    samples = array_samples[name]
                    shape = samples.shape[1:]

                    record.append(
                        (
                            samples.dtype,
                            samples.dtype.itemsize,
                            offset,
                            0,
                        )
                    )

                    # add channel dependency block
                    kwargs = {
                        "dims": 1,
                        "ca_type": v4c.CA_TYPE_SCALE_AXIS,
                        "flags": 0,
                        "byte_offset_base": samples.dtype.itemsize,
                        "dim_size_0": shape[0],
                    }
                    dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([dep])

                    # add components channel
                    s_type, s_size = fmt_to_datatype_v4(samples.dtype, ())
                    byte_size = s_size // 8 or 1
                    kwargs = {
                        "channel_type": v4c.CHANNEL_TYPE_VALUE,
                        "bit_count": s_size,
                        "byte_offset": offset,
                        "bit_offset": 0,
                        "data_type": s_type,
                        "flags": 0,
                    }

                    if invalidation_bytes_nr:
                        if signal.invalidation_bits is not None:
                            inval_bits.append(signal.invalidation_bits)
                            kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
                            kwargs["pos_invalidation_bit"] = inval_cntr
                            inval_cntr += 1

                    ch = Channel(**kwargs)
                    ch.name = name
                    ch.unit = signal.unit
                    ch.comment = signal.comment
                    ch.display_names = signal.display_names
                    ch.dtype_fmt = samples.dtype

                    gp_channels.append(ch)

                    entry = dg_cntr, ch_cntr
                    parent_dep.axis_channels.append(entry)
                    for dim in shape:
                        byte_size *= dim
                    offset += byte_size

                    fields.append((samples.tobytes(), byte_size))

                    gp_sdata.append(None)
                    self.channels_db.add(name, entry)

                    ch_cntr += 1

            elif sig_type == v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:
                struct = Signal(
                    samples,
                    samples,
                    name=name,
                    invalidation_bits=signal.invalidation_bits,
                )
                (
                    offset,
                    dg_cntr,
                    ch_cntr,
                    sub_structure,
                    new_fields,
                    inval_cntr,
                ) = self._append_structure_composition(
                    grp,
                    struct,
                    offset,
                    dg_cntr,
                    ch_cntr,
                    defined_texts,
                    invalidation_bytes_nr,
                    inval_bits,
                    inval_cntr,
                )
                dep_list.append(sub_structure)
                fields.extend(new_fields)

        return offset, dg_cntr, ch_cntr, struct_self, fields, inval_cntr

    def _append_structure_composition_column_oriented(
        self,
        grp: Group,
        signal: Signal,
        field_names: UniqueDB,
        offset: int,
        dg_cntr: int,
        ch_cntr: int,
        defined_texts: dict[str, int],
    ) -> tuple[
        int,
        int,
        int,
        tuple[int, int],
        list[NDArray[Any]],
        list[tuple[str, dtype[Any], tuple[int, ...]]],
    ]:
        si_map = self._si_map

        fields = []
        types = []

        file = self._tempfile
        seek = file.seek
        seek(0, 2)

        gp = grp
        gp_sdata = gp.signal_data
        gp_channels = gp.channels
        gp_dep = gp.channel_dependencies
        record = gp.record

        name = signal.name
        names = signal.samples.dtype.names

        field_name = field_names.get_unique_name(name)

        # first we add the structure channel

        if signal.attachment and signal.attachment[0]:
            at_data, at_name, hash_sum = signal.attachment
            if at_name is not None:
                suffix = Path(at_name).suffix.strip(".")
            else:
                suffix = "dbc"
            attachment_index = self.attach(
                at_data, at_name, hash_sum=hash_sum, mime=f"application/x-{suffix}"
            )
            attachment = attachment_index
        else:
            attachment = None

        # add channel block
        kwargs = {
            "channel_type": v4c.CHANNEL_TYPE_VALUE,
            "bit_count": signal.samples.dtype.itemsize * 8,
            "byte_offset": offset,
            "bit_offset": 0,
            "data_type": v4c.DATA_TYPE_BYTEARRAY,
            "precision": 0,
        }

        if attachment is not None:
            kwargs["attachment_addr"] = 0

        source_bus = signal.source and signal.source.source_type == v4c.SOURCE_BUS

        if source_bus:
            kwargs["flags"] = v4c.FLAG_CN_BUS_EVENT
            flags_ = v4c.FLAG_CN_BUS_EVENT
            grp.channel_group.flags |= v4c.FLAG_CG_BUS_EVENT
        else:
            kwargs["flags"] = 0
            flags_ = 0

        if signal.invalidation_bits is not None:
            kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
            kwargs["pos_invalidation_bit"] = 0

        ch = Channel(**kwargs)
        ch.name = name
        ch.unit = signal.unit
        ch.comment = signal.comment
        ch.display_names = signal.display_names
        ch.attachment = attachment
        ch.dtype_fmt = signal.samples.dtype

        if source_bus:
            grp.channel_group.acq_source = SourceInformation.from_common_source(
                signal.source
            )

            if signal.source.bus_type == v4c.BUS_TYPE_CAN:
                grp.channel_group.path_separator = 46
                grp.channel_group.acq_name = "CAN"
            elif signal.source.bus_type == v4c.BUS_TYPE_FLEXRAY:
                grp.channel_group.path_separator = 46
                grp.channel_group.acq_name = "FLEXRAY"
            elif signal.source.bus_type == v4c.BUS_TYPE_ETHERNET:
                grp.channel_group.path_separator = 46
                grp.channel_group.acq_name = "ETHERNET"

        # source for channel
        source = signal.source
        if source:
            if source in si_map:
                ch.source = si_map[source]
            else:
                new_source = SourceInformation(
                    source_type=source.source_type, bus_type=source.bus_type
                )
                new_source.name = source.name
                new_source.path = source.path
                new_source.comment = source.comment

                si_map[source] = new_source

                ch.source = new_source

        entry = dg_cntr, ch_cntr
        gp_channels.append(ch)
        struct_self = entry

        gp_sdata.append(None)
        self.channels_db.add(name, entry)
        for _name in ch.display_names:
            self.channels_db.add(_name, entry)

        ch_cntr += 1

        dep_list = []
        gp_dep.append(dep_list)

        record.append((ch.dtype_fmt, ch.dtype_fmt.itemsize, offset, 0))

        # then we add the fields

        for name in names:
            field_name = field_names.get_unique_name(name)

            samples = signal.samples[name]
            fld_names = samples.dtype.names

            if fld_names is None:
                sig_type = v4c.SIGNAL_TYPE_SCALAR
                if samples.dtype.kind in "SV":
                    sig_type = v4c.SIGNAL_TYPE_STRING
            else:
                if fld_names in (v4c.CANOPEN_TIME_FIELDS, v4c.CANOPEN_DATE_FIELDS):
                    sig_type = v4c.SIGNAL_TYPE_CANOPEN
                elif fld_names[0] != name:
                    sig_type = v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION
                else:
                    sig_type = v4c.SIGNAL_TYPE_ARRAY

            if sig_type in (v4c.SIGNAL_TYPE_SCALAR, v4c.SIGNAL_TYPE_STRING):

                s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape)
                byte_size = s_size // 8 or 1

                fields.append(samples)
                types.append((field_name, samples.dtype, samples.shape[1:]))

                # add channel block
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VALUE,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "flags": flags_,
                }

                if signal.invalidation_bits is not None:
                    kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
                    kwargs["pos_invalidation_bit"] = 0

                ch = Channel(**kwargs)
                ch.name = name
                ch.dtype_fmt = dtype((samples.dtype, samples.shape[1:]))

                record.append(
                    (
                        ch.dtype_fmt,
                        ch.dtype_fmt.itemsize,
                        offset,
                        0,
                    )
                )

                entry = (dg_cntr, ch_cntr)
                gp_channels.append(ch)
                dep_list.append(entry)

                offset += byte_size

                gp_sdata.append(None)
                self.channels_db.add(name, entry)

                ch_cntr += 1
                gp_dep.append(None)

            elif sig_type == v4c.SIGNAL_TYPE_ARRAY:
                # here we have channel arrays or mdf v3 channel dependencies
                array_samples = samples
                names = samples.dtype.names
                samples = array_samples[names[0]]
                shape = samples.shape[1:]

                record.append(
                    (
                        samples.dtype,
                        samples.dtype.itemsize,
                        offset,
                        0,
                    )
                )

                if len(names) > 1:
                    # add channel dependency block for composed parent channel
                    dims_nr = len(shape)
                    names_nr = len(names)

                    if names_nr == 0:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_LOOKUP,
                            "flags": v4c.FLAG_CA_FIXED_AXIS,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    elif len(names) == 1:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_ARRAY,
                            "flags": 0,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    else:
                        kwargs = {
                            "dims": dims_nr,
                            "ca_type": v4c.CA_TYPE_LOOKUP,
                            "flags": v4c.FLAG_CA_AXIS,
                            "byte_offset_base": samples.dtype.itemsize,
                        }
                        for i in range(dims_nr):
                            kwargs[f"dim_size_{i}"] = shape[i]

                    parent_dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([parent_dep])

                else:
                    # add channel dependency block for composed parent channel
                    kwargs = {
                        "dims": 1,
                        "ca_type": v4c.CA_TYPE_SCALE_AXIS,
                        "flags": 0,
                        "byte_offset_base": samples.dtype.itemsize,
                        "dim_size_0": shape[0],
                    }
                    parent_dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([parent_dep])

                field_name = field_names.get_unique_name(name)

                fields.append(samples)
                dtype_pair = field_name, samples.dtype, shape
                types.append(dtype_pair)

                # first we add the structure channel
                s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape, True)

                # add channel block
                kwargs = {
                    "channel_type": v4c.CHANNEL_TYPE_VALUE,
                    "bit_count": s_size,
                    "byte_offset": offset,
                    "bit_offset": 0,
                    "data_type": s_type,
                    "flags": 0,
                }

                if signal.invalidation_bits is not None:
                    kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
                    kwargs["pos_invalidation_bit"] = 0

                ch = Channel(**kwargs)
                ch.name = name
                ch.unit = signal.unit
                ch.comment = signal.comment
                ch.display_names = signal.display_names
                ch.dtype_fmt = samples.dtype

                # source for channel
                source = signal.source
                if source:
                    if source in si_map:
                        ch.source = si_map[source]
                    else:
                        new_source = SourceInformation(
                            source_type=source.source_type, bus_type=source.bus_type
                        )
                        new_source.name = source.name
                        new_source.path = source.path
                        new_source.comment = source.comment

                        si_map[source] = new_source

                        ch.source = new_source

                gp_channels.append(ch)

                size = s_size // 8
                for dim in shape:
                    size *= dim
                offset += size

                gp_sdata.append(None)
                entry = (dg_cntr, ch_cntr)
                self.channels_db.add(name, entry)
                for _name in ch.display_names:
                    self.channels_db.add(_name, entry)

                ch_cntr += 1

                for name in names[1:]:
                    field_name = field_names.get_unique_name(name)

                    samples = array_samples[name]
                    shape = samples.shape[1:]
                    fields.append(samples)
                    types.append((field_name, samples.dtype, shape))

                    record.append(
                        (
                            samples.dtype,
                            samples.dtype.itemsize,
                            offset,
                            0,
                        )
                    )

                    # add channel dependency block
                    kwargs = {
                        "dims": 1,
                        "ca_type": v4c.CA_TYPE_SCALE_AXIS,
                        "flags": 0,
                        "byte_offset_base": samples.dtype.itemsize,
                        "dim_size_0": shape[0],
                    }
                    dep = ChannelArrayBlock(**kwargs)
                    gp_dep.append([dep])

                    # add components channel
                    s_type, s_size = fmt_to_datatype_v4(samples.dtype, ())
                    byte_size = s_size // 8 or 1
                    kwargs = {
                        "channel_type": v4c.CHANNEL_TYPE_VALUE,
                        "bit_count": s_size,
                        "byte_offset": offset,
                        "bit_offset": 0,
                        "data_type": s_type,
                        "flags": 0,
                    }

                    if signal.invalidation_bits is not None:
                        kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
                        kwargs["pos_invalidation_bit"] = 0

                    ch = Channel(**kwargs)
                    ch.name = name
                    ch.unit = signal.unit
                    ch.comment = signal.comment
                    ch.display_names = signal.display_names
                    ch.dtype_fmt = samples.dtype

                    gp_channels.append(ch)

                    entry = dg_cntr, ch_cntr
                    parent_dep.axis_channels.append(entry)
                    for dim in shape:
                        byte_size *= dim
                    offset += byte_size

                    gp_sdata.append(None)
                    self.channels_db.add(name, entry)

                    ch_cntr += 1

            elif sig_type == v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:
                struct = Signal(
                    samples,
                    samples,
                    name=name,
                    invalidation_bits=signal.invalidation_bits,
                )
                (
                    offset,
                    dg_cntr,
                    ch_cntr,
                    sub_structure,
                    new_fields,
                    new_types,
                ) = self._append_structure_composition_column_oriented(
                    grp,
                    struct,
                    field_names,
                    offset,
                    dg_cntr,
                    ch_cntr,
                    defined_texts,
                )
                dep_list.append(sub_structure)
                fields.extend(new_fields)
                types.extend(new_types)

        return offset, dg_cntr, ch_cntr, struct_self, fields, types

    def extend(
        self, index: int, signals: list[tuple[NDArray[Any], NDArray[Any] | None]]
    ) -> None:
        """
        Extend a group with new samples. *signals* contains (values, invalidation_bits)
        pairs for each extended signal. The first pair is the master channel's pair, and the
        next pairs must respect the same order in which the signals were appended. The samples must have raw
        or physical values according to the *Signals* used for the initial append.

        Parameters
        ----------
        index : int
            group index
        signals : list
            list on (numpy.ndarray, numpy.ndarray) objects

        Examples
        --------
        >>> # case 1 conversion type None
        >>> s1 = np.array([1, 2, 3, 4, 5])
        >>> s2 = np.array([-1, -2, -3, -4, -5])
        >>> s3 = np.array([0.1, 0.04, 0.09, 0.16, 0.25])
        >>> t = np.array([0.001, 0.002, 0.003, 0.004, 0.005])
        >>> names = ['Positive', 'Negative', 'Float']
        >>> units = ['+', '-', '.f']
        >>> s1 = Signal(samples=s1, timestamps=t, unit='+', name='Positive')
        >>> s2 = Signal(samples=s2, timestamps=t, unit='-', name='Negative')
        >>> s3 = Signal(samples=s3, timestamps=t, unit='flts', name='Floats')
        >>> mdf = MDF4('new.mdf')
        >>> mdf.append([s1, s2, s3], comment='created by asammdf v1.1.0')
        >>> t = np.array([0.006, 0.007, 0.008, 0.009, 0.010])
        >>> # extend without invalidation bits
        >>> mdf2.extend(0, [(t, None), (s1, None), (s2, None), (s3, None)])
        >>> # some invaldiation btis
        >>> s1_inv = np.array([0,0,0,1,1], dtype=np.bool)
        >>> mdf2.extend(0, [(t, None), (s1.samples, None), (s2.samples, None), (s3.samples, None)])

        """
        if self.version >= "4.20" and (self._column_storage or 1):
            return self._extend_column_oriented(index, signals)
        gp = self.groups[index]
        if not signals:
            message = '"append" requires a non-empty list of Signal objects'
            raise MdfException(message)

        stream = self._tempfile

        fields = []
        inval_bits = []

        added_cycles = len(signals[0][0])

        invalidation_bytes_nr = gp.channel_group.invalidation_bytes_nr
        for i, ((signal, invalidation_bits), sig_type) in enumerate(
            zip(signals, gp.signal_types)
        ):

            # first add the signals in the simple signal list
            if sig_type == v4c.SIGNAL_TYPE_SCALAR:

                s_type, s_size = fmt_to_datatype_v4(signal.dtype, signal.shape)
                byte_size = s_size // 8 or 1

                fields.append((signal.tobytes(), byte_size))

                if invalidation_bytes_nr and invalidation_bits is not None:
                    inval_bits.append(invalidation_bits)

            elif sig_type == v4c.SIGNAL_TYPE_CANOPEN:
                names = signal.dtype.names

                if names == v4c.CANOPEN_TIME_FIELDS:

                    vals = signal.tobytes()

                    fields.append((vals, 6))

                else:
                    vals = []
                    for field in ("ms", "min", "hour", "day", "month", "year"):
                        vals.append(signal[field])
                    vals = fromarrays(vals).tobytes()

                    fields.append((vals, 7))

                if invalidation_bytes_nr and invalidation_bits is not None:
                    inval_bits.append(invalidation_bits)

            elif sig_type == v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:

                if invalidation_bytes_nr and invalidation_bits is not None:
                    inval_bits.append(invalidation_bits)

                fields.append((signal.tobytes(), signal.dtype.itemsize))

            elif sig_type == v4c.SIGNAL_TYPE_ARRAY:
                names = signal.dtype.names

                samples = signal[names[0]]

                shape = samples.shape[1:]
                s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape, True)
                size = s_size // 8
                for dim in shape:
                    size *= dim

                fields.append((samples.tobytes(), size))

                if invalidation_bytes_nr and invalidation_bits is not None:
                    inval_bits.append(invalidation_bits)

                for name in names[1:]:

                    samples = signal[name]
                    shape = samples.shape[1:]
                    s_type, s_size = fmt_to_datatype_v4(samples.dtype, ())
                    size = s_size // 8
                    for dim in shape:
                        size *= dim

                    fields.append((samples.tobytes(), size))

                    if invalidation_bytes_nr and invalidation_bits is not None:
                        inval_bits.append(invalidation_bits)

            else:
                if self.compact_vlsd:
                    cur_offset = sum(
                        blk.original_size for blk in gp.get_signal_data_blocks(i)
                    )

                    data = []
                    offsets = []
                    off = 0
                    if gp.channels[i].data_type == v4c.DATA_TYPE_STRING_UTF_16_LE:
                        for elem in signal:
                            offsets.append(off)
                            size = len(elem)
                            if size % 2:
                                size += 1
                                elem = elem + b"\0"
                            data.append(UINT32_p(size))
                            data.append(elem)
                            off += size + 4
                    else:
                        for elem in signal:
                            offsets.append(off)
                            size = len(elem)
                            data.append(UINT32_p(size))
                            data.append(elem)
                            off += size + 4

                    offsets = array(offsets, dtype=uint64)

                    stream.seek(0, 2)
                    addr = stream.tell()

                    data_size = off
                    if data_size:
                        info = SignalDataBlockInfo(
                            address=addr,
                            compressed_size=data_size,
                            original_size=data_size,
                            location=v4c.LOCATION_TEMPORARY_FILE,
                        )
                        gp.signal_data[i][0].append(info)
                        stream.write(b"".join(data))

                    offsets += cur_offset
                    fields.append((offsets.tobytes(), 8))

                else:
                    cur_offset = sum(
                        blk.original_size for blk in gp.get_signal_data_blocks(i)
                    )

                    offsets = arange(len(signal), dtype=uint64) * (signal.itemsize + 4)

                    values = [full(len(signal), signal.itemsize, dtype=uint32), signal]

                    types_ = [("", uint32), ("", signal.dtype)]

                    values = fromarrays(values, dtype=types_)

                    stream.seek(0, 2)
                    addr = stream.tell()
                    block_size = len(values) * values.itemsize
                    if block_size:
                        info = SignalDataBlockInfo(
                            address=addr,
                            compressed_size=block_size,
                            original_size=block_size,
                            location=v4c.LOCATION_TEMPORARY_FILE,
                        )
                        gp.signal_data[i][0].append(info)
                        values.tofile(stream)

                    offsets += cur_offset
                    fields.append((offsets.tobytes(), 8))

                if invalidation_bytes_nr and invalidation_bits is not None:
                    inval_bits.append(invalidation_bits)

        if invalidation_bytes_nr:
            invalidation_bytes_nr = len(inval_bits)
            cycles_nr = len(inval_bits[0])

            for _ in range(8 - invalidation_bytes_nr % 8):
                inval_bits.append(zeros(cycles_nr, dtype=bool))

            inval_bits.reverse()

            invalidation_bytes_nr = len(inval_bits) // 8

            gp.channel_group.invalidation_bytes_nr = invalidation_bytes_nr

            inval_bits = fliplr(
                packbits(array(inval_bits).T).reshape(
                    (cycles_nr, invalidation_bytes_nr)
                )
            )

            if self.version < "4.20":
                fields.append((inval_bits.tobytes(), invalidation_bytes_nr))

        samples = data_block_from_arrays(fields, added_cycles)
        size = len(samples)

        del fields

        stream.seek(0, 2)
        addr = stream.tell()

        if size:

            if self.version < "4.20":
                data = samples
                raw_size = size
                data = lz_compress(data)
                size = len(data)
                stream.write(data)
                gp.data_blocks.append(
                    DataBlockInfo(
                        address=addr,
                        block_type=v4c.DZ_BLOCK_LZ,
                        original_size=raw_size,
                        compressed_size=size,
                        param=0,
                    )
                )

                gp.channel_group.cycles_nr += added_cycles
                self.virtual_groups[index].cycles_nr += added_cycles

            else:
                data = samples
                raw_size = size
                data = lz_compress(data)
                size = len(data)
                stream.write(data)

                gp.data_blocks.append(
                    DataBlockInfo(
                        address=addr,
                        block_type=v4c.DT_BLOCK_LZ,
                        original_size=raw_size,
                        compressed_size=size,
                        param=0,
                    )
                )

                gp.channel_group.cycles_nr += added_cycles
                self.virtual_groups[index].cycles_nr += added_cycles

                if invalidation_bytes_nr:
                    addr = stream.tell()

                    data = inval_bits.tobytes()
                    raw_size = len(data)
                    data = lz_compress(data)
                    size = len(data)
                    stream.write(data)

                    gp.data_blocks[-1].invalidation_block(
                        InvalidationBlockInfo(
                            address=addr,
                            block_type=v4c.DT_BLOCK_LZ,
                            original_size=raw_size,
                            compressed_size=size,
                            param=None,
                        )
                    )

    def _extend_column_oriented(
        self, index: int, signals: list[tuple[NDArray[Any], NDArray[Any] | None]]
    ) -> None:
        """
        Extend a group with new samples. *signals* contains (values, invalidation_bits)
        pairs for each extended signal. The first pair is the master channel's pair, and the
        next pairs must respect the same order in which the signals were appended. The samples must have raw
        or physical values according to the *Signals* used for the initial append.

        Parameters
        ----------
        index : int
            group index
        signals : list
            list on (numpy.ndarray, numpy.ndarray) objects

        Examples
        --------
        >>> # case 1 conversion type None
        >>> s1 = np.array([1, 2, 3, 4, 5])
        >>> s2 = np.array([-1, -2, -3, -4, -5])
        >>> s3 = np.array([0.1, 0.04, 0.09, 0.16, 0.25])
        >>> t = np.array([0.001, 0.002, 0.003, 0.004, 0.005])
        >>> names = ['Positive', 'Negative', 'Float']
        >>> units = ['+', '-', '.f']
        >>> s1 = Signal(samples=s1, timestamps=t, unit='+', name='Positive')
        >>> s2 = Signal(samples=s2, timestamps=t, unit='-', name='Negative')
        >>> s3 = Signal(samples=s3, timestamps=t, unit='flts', name='Floats')
        >>> mdf = MDF4('new.mdf')
        >>> mdf.append([s1, s2, s3], comment='created by asammdf v1.1.0')
        >>> t = np.array([0.006, 0.007, 0.008, 0.009, 0.010])
        >>> # extend without invalidation bits
        >>> mdf2.extend(0, [(t, None), (s1, None), (s2, None), (s3, None)])
        >>> # some invaldiation btis
        >>> s1_inv = np.array([0,0,0,1,1], dtype=np.bool)
        >>> mdf2.extend(0, [(t, None), (s1.samples, None), (s2.samples, None), (s3.samples, None)])

        """
        gp = self.groups[index]
        if not signals:
            message = '"append" requires a non-empty list of Signal objects'
            raise MdfException(message)

        stream = self._tempfile
        stream.seek(0, 2)
        write = stream.write
        tell = stream.tell

        added_cycles = len(signals[0][0])

        self.virtual_groups[index].cycles_nr += added_cycles

        for i, (signal, invalidation_bits) in enumerate(signals):
            gp = self.groups[index + i]
            sig_type = gp.signal_types[0]

            # first add the signals in the simple signal list
            if sig_type == v4c.SIGNAL_TYPE_SCALAR:
                samples = signal

            elif sig_type == v4c.SIGNAL_TYPE_CANOPEN:
                names = signal.dtype.names

                if names == v4c.CANOPEN_TIME_FIELDS:
                    samples = signal

                else:
                    vals = []
                    for field in ("ms", "min", "hour", "day", "month", "year"):
                        vals.append(signal[field])
                    samples = fromarrays(vals)

            elif sig_type == v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:

                samples = signal

            elif sig_type == v4c.SIGNAL_TYPE_ARRAY:
                samples = signal

            else:
                cur_offset = sum(
                    blk.original_size for blk in gp.get_signal_data_blocks(0)
                )

                offsets = arange(len(signal), dtype=uint64) * (signal.itemsize + 4)

                values = [full(len(signal), signal.itemsize, dtype=uint32), signal]

                types_ = [("", uint32), ("", signal.dtype)]

                values = fromarrays(values, dtype=types_)

                addr = tell()
                block_size = len(values) * values.itemsize
                if block_size:
                    info = SignalDataBlockInfo(
                        address=addr,
                        compressed_size=block_size,
                        original_size=block_size,
                        location=v4c.LOCATION_TEMPORARY_FILE,
                    )
                    gp.signal_data[i][0].append(info)
                    write(values.tobytes())

                offsets += cur_offset

                samples = offsets

            addr = tell()

            if added_cycles:
                data = samples.tobytes()
                raw_size = len(data)
                data = lz_compress(data)

                size = len(data)
                write(data)

                gp.data_blocks.append(
                    DataBlockInfo(
                        address=addr,
                        block_type=v4c.DZ_BLOCK_LZ,
                        original_size=raw_size,
                        compressed_size=size,
                        param=0,
                    )
                )

                gp.channel_group.cycles_nr += added_cycles

                if invalidation_bits is not None:
                    addr = tell()
                    data = invalidation_bits.tobytes()
                    raw_size = len(data)
                    data = lz_compress(data)
                    size = len(data)
                    write(data)

                    gp.data_blocks[-1].invalidation_block(
                        InvalidationBlockInfo(
                            address=addr,
                            block_type=v4c.DZ_BLOCK_LZ,
                            original_size=raw_size,
                            compressed_size=size,
                            param=None,
                        )
                    )

    def attach(
        self,
        data: bytes,
        file_name: str | None = None,
        hash_sum: bytes | None = None,
        comment: str = "",
        compression: bool = True,
        mime: str = r"application/octet-stream",
        embedded: bool = True,
        password: str | bytes | None = None,
    ) -> int:
        """attach embedded attachment as application/octet-stream.

        Parameters
        ----------
        data : bytes
            data to be attached
        file_name : str
            string file name
        hash_sum : bytes
            md5 of the data
        comment : str
            attachment comment
        compression : bool
            use compression for embedded attachment data
        mime : str
            mime type string
        embedded : bool
            attachment is embedded in the file
        password : str | bytes | None , default None
            password used to encrypt the data using AES256 encryption

            .. versionadded:: 7.0.0

        Returns
        -------
        index : int
            new attachment index

        """
        if self._force_attachment_encryption:
            password = password or self._password

        if password and not CRYPTOGRAPHY_AVAILABLE:
            raise MdfException(
                "cryptography must be installed for attachment encryption"
            )

        if hash_sum is None:
            worker = md5()
            worker.update(data)
            hash_sum = worker.hexdigest()

        if hash_sum in self._attachments_cache:
            return self._attachments_cache[hash_sum]

        if password:
            if isinstance(password, str):
                password = password.encode("utf-8")

            size = len(password)
            if size < 32:
                password = password + bytes(32 - size)
            else:
                password = password[:32]

            iv = os.urandom(16)
            cipher = Cipher(algorithms.AES(password), modes.CBC(iv))
            encryptor = cipher.encryptor()

            original_size = len(data)

            rem = original_size % 16
            if rem:
                data += os.urandom(16 - rem)

            data = iv + encryptor.update(data) + encryptor.finalize()
            worker = md5()
            worker.update(data)
            hash_sum_encrypted = worker.hexdigest()

            comment = f"""<ATcomment>
    <TX>{comment}</TX>
    <extensions>
		<extension>
			<encrypted>true</encrypted>
			<algorithm>AES256</algorithm>
			<original_md5_sum>{hash_sum_encrypted}</original_md5_sum>
			<original_size>{original_size}</original_size>
		</extension>
	</extensions>
</ATcomment>       
"""
        else:
            hash_sum_encrypted = hash_sum
            comment = comment

        if hash_sum_encrypted in self._attachments_cache:
            return self._attachments_cache[hash_sum]

        creator_index = len(self.file_history)
        fh = FileHistory()
        fh.comment = """<FHcomment>
<TX>Added new embedded attachment from {}</TX>
<tool_id>asammdf</tool_id>
<tool_vendor>asammdf</tool_vendor>
<tool_version>{}</tool_version>
</FHcomment>""".format(
            file_name if file_name else "bin.bin", __version__
        )

        self.file_history.append(fh)

        file_name = file_name or "bin.bin"

        at_block = AttachmentBlock(
            data=data,
            compression=compression,
            embedded=embedded,
            file_name=file_name,
            comment=comment,
        )
        at_block.comment = comment
        at_block["creator_index"] = creator_index

        self.attachments.append(at_block)

        suffix = Path(file_name).suffix.lower().strip(".")
        if suffix == "a2l":
            mime = "application/A2L"
        else:
            mime = f"application/x-{suffix}"

        at_block.mime = mime

        index = len(self.attachments) - 1
        self._attachments_cache[hash_sum] = index
        self._attachments_cache[hash_sum_encrypted] = index

        return index

    def close(self) -> None:
        """if the MDF was created with memory=False and new
        channels have been appended, then this must be called just before the
        object is not used anymore to clean-up the temporary file"""

        self._parent = None
        if self._tempfile is not None:
            self._tempfile.close()
        if not self._from_filelike and self._file is not None:
            self._file.close()

        if self._mapped_file is not None:
            self._mapped_file.close()

        if self._delete_on_close:
            try:
                Path(self.name).unlink()
            except:
                pass

        if self.original_name is not None:
            if Path(self.original_name).suffix.lower() in (
                ".bz2",
                ".gzip",
                ".mf4z",
                ".zip",
            ):
                try:
                    Path(self.name).unlink()
                except:
                    pass

        for gp in self.groups:
            gp.clear()
        self.groups.clear()
        self.header = None
        self.identification = None
        self.file_history.clear()
        self.channels_db.clear()
        self.masters_db.clear()
        self.attachments.clear()
        self._attachments_cache.clear()
        self.file_comment = None
        self.events.clear()

        self._ch_map.clear()
        self._master_channel_metadata.clear()
        self._invalidation_cache.clear()
        self._external_dbc_cache.clear()
        self._si_map.clear()
        self._file_si_map.clear()
        self._cc_map.clear()
        self._file_cc_map.clear()
        self._cg_map.clear()
        self._cn_data_map.clear()
        self._dbc_cache.clear()
        self.virtual_groups.clear()

    @lru_cache(maxsize=128)
    def extract_attachment(
        self,
        index: int | None = None,
        password: str | bytes | None = None,
    ) -> tuple[bytes, Path, bytes]:
        """extract attachment data by index. If it is an embedded attachment,
        then this method creates the new file according to the attachment file
        name information

        Parameters
        ----------
        index : int
            attachment index; default *None*

        password : str | bytes | None, default None
            password used to encrypt the data using AES256 encryption

            .. versionadded:: 7.0.0

        Returns
        -------
        data : (bytes, pathlib.Path)
            tuple of attachment data and path

        """
        password = password or self._password
        if index is None:
            return b"", Path(""), md5().digest()

        attachment = self.attachments[index]

        current_path = Path.cwd()
        file_path = Path(attachment.file_name or "embedded")

        try:
            os.chdir(self.name.resolve().parent)

            flags = attachment.flags

            # for embedded attachments extract data and create new files
            if flags & v4c.FLAG_AT_EMBEDDED:
                data = attachment.extract()
                md5_worker = md5()
                md5_worker.update(data)
                md5_sum = md5_worker.digest()

                encryption_info = extract_encryption_information(attachment.comment)
                if encryption_info.get("encrypted", False):

                    if not password:
                        raise MdfException(
                            "the password must be provided for encrypted attachments"
                        )

                    if isinstance(password, str):
                        password = password.encode("utf-8")

                    size = len(password)
                    if size < 32:
                        password = password + bytes(32 - size)
                    else:
                        password = password[:32]

                    if encryption_info["algorithm"] == "aes256":

                        md5_worker = md5()
                        md5_worker.update(data)
                        md5_sum = md5_worker.hexdigest().lower()

                        if md5_sum != encryption_info["original_md5_sum"]:
                            raise MdfException(
                                f"MD5 sum mismatch for encrypted attachment: original={encryption_info['original_md5_sum']} and computed={md5_sum}"
                            )

                        iv, data = data[:16], data[16:]
                        cipher = Cipher(algorithms.AES(password), modes.CBC(iv))
                        decryptor = cipher.decryptor()
                        data = decryptor.update(data) + decryptor.finalize()

                        data = data[: encryption_info["original_size"]]

                    else:
                        raise MdfException(
                            f"not implemented attachment encryption algorithm <{encryption_info['algorithm']}>"
                        )

            else:

                # for external attachments read the file and return the content
                if flags & v4c.FLAG_AT_MD5_VALID:
                    data = open(file_path, "rb").read()

                    md5_worker = md5()
                    md5_worker.update(data)
                    md5_sum = md5_worker.digest()
                    if attachment["md5_sum"] == md5_sum:
                        if attachment.mime.startswith("text"):
                            with open(file_path, "r") as f:
                                data = f.read()
                    else:
                        message = (
                            f'ATBLOCK md5sum="{attachment["md5_sum"]}" '
                            f"and external attachment data ({file_path}) "
                            f'md5sum="{md5_sum}"'
                        )
                        logger.warning(message)
                else:
                    if attachment.mime.startswith("text"):
                        mode = "r"
                    else:
                        mode = "rb"
                    with open(file_path, mode) as f:
                        data = f.read()
                        md5_worker = md5()
                        md5_worker.update(data)
                        md5_sum = md5_worker.digest()

        except Exception as err:
            os.chdir(current_path)
            message = f'Exception during attachment "{attachment.file_name}" extraction: {err!r}'
            logger.warning(message)
            data = b""
            md5_sum = md5().digest()
        finally:
            os.chdir(current_path)

        return data, file_path, md5_sum

    @overload
    def get(
        self,
        name: str | None = ...,
        group: int | None = ...,
        index: int | None = ...,
        raster: RasterType | None = ...,
        samples_only: Literal[False] = ...,
        data: bytes | None = ...,
        raw: bool = ...,
        ignore_invalidation_bits: bool = ...,
        record_offset: int = ...,
        record_count: int | None = ...,
        skip_channel_validation: bool = ...,
    ) -> Signal:
        ...

    @overload
    def get(
        self,
        name: str | None = ...,
        group: int | None = ...,
        index: int | None = ...,
        raster: RasterType | None = ...,
        samples_only: Literal[True] = ...,
        data: bytes | None = ...,
        raw: bool = ...,
        ignore_invalidation_bits: bool = ...,
        record_offset: int = ...,
        record_count: int | None = ...,
        skip_channel_validation: bool = ...,
    ) -> tuple[NDArray[Any], NDArray[Any]]:
        ...

    def get(
        self,
        name: str | None = None,
        group: int | None = None,
        index: int | None = None,
        raster: RasterType | None = None,
        samples_only: bool = False,
        data: bytes | None = None,
        raw: bool = False,
        ignore_invalidation_bits: bool = False,
        record_offset: int = 0,
        record_count: int | None = None,
        skip_channel_validation: bool = False,
    ) -> Signal | tuple[NDArray[Any], NDArray[Any]]:
        """Gets channel samples. The raw data group samples are not loaded to
        memory so it is advised to use ``filter`` or ``select`` instead of
        performing several ``get`` calls.

        Channel can be specified in two ways:

        * using the first positional argument *name*

            * if there are multiple occurrences for this channel then the
              *group* and *index* arguments can be used to select a specific
              group.
            * if there are multiple occurrences for this channel and either the
              *group* or *index* arguments is None then a warning is issued

        * using the group number (keyword argument *group*) and the channel
          number (keyword argument *index*). Use *info* method for group and
          channel numbers

        If the *raster* keyword argument is not *None* the output is
        interpolated accordingly

        Parameters
        ----------
        name : string
            name of channel
        group : int
            0-based group index
        index : int
            0-based channel index
        raster : float
            time raster in seconds
        samples_only : bool
            if *True* return only the channel samples as numpy array; if
                *False* return a *Signal* object
        data : bytes
            prevent redundant data read by providing the raw data group samples
        raw : bool
            return channel samples without applying the conversion rule; default
            `False`
        ignore_invalidation_bits : bool
            option to ignore invalidation bits
        record_offset : int
            if *data=None* use this to select the record offset from which the
            group data should be loaded
        record_count : int
            number of records to read; default *None* and in this case all
            available records are used
        skip_channel_validation (False) : bool
            skip validation of channel name, group index and channel index; defualt
            *False*. If *True*, the caller has to make sure that the *group* and *index*
            arguments are provided and are correct.

            ..versionadded:: 7.0.0


        Returns
        -------
        res : (numpy.array, numpy.array) | Signal
            returns *Signal* if *samples_only*=*False* (default option),
            otherwise returns a (numpy.array, numpy.array) tuple of samples and
            invalidation bits. If invalidation bits are not used or if
            *ignore_invalidation_bits* if False, then the second item will be
            None.

            The *Signal* samples are:

                * numpy recarray for channels that have composition/channel
                  array address or for channel of type
                  CANOPENDATE, CANOPENTIME
                * numpy array for all the rest

        Raises
        ------
        MdfException :

        * if the channel name is not found
        * if the group index is out of range
        * if the channel index is out of range

        Examples
        --------
        >>> from asammdf import MDF, Signal
        >>> import numpy as np
        >>> t = np.arange(5)
        >>> s = np.ones(5)
        >>> mdf = MDF(version='4.10')
        >>> for i in range(4):
        ...     sigs = [Signal(s*(i*10+j), t, name='Sig') for j in range(1, 4)]
        ...     mdf.append(sigs)
        ...
        >>> # first group and channel index of the specified channel name
        ...
        >>> mdf.get('Sig')
        UserWarning: Multiple occurrences for channel "Sig". Using first occurrence from data group 4. Provide both "group" and "index" arguments to select another data group
        <Signal Sig:
                samples=[ 1.  1.  1.  1.  1.]
                timestamps=[0 1 2 3 4]
                unit=""
                info=None
                comment="">
        >>> # first channel index in the specified group
        ...
        >>> mdf.get('Sig', 1)
        <Signal Sig:
                samples=[ 11.  11.  11.  11.  11.]
                timestamps=[0 1 2 3 4]
                unit=""
                info=None
                comment="">
        >>> # channel named Sig from group 1 channel index 2
        ...
        >>> mdf.get('Sig', 1, 2)
        <Signal Sig:
                samples=[ 12.  12.  12.  12.  12.]
                timestamps=[0 1 2 3 4]
                unit=""
                info=None
                comment="">
        >>> # channel index 1 or group 2
        ...
        >>> mdf.get(None, 2, 1)
        <Signal Sig:
                samples=[ 21.  21.  21.  21.  21.]
                timestamps=[0 1 2 3 4]
                unit=""
                info=None
                comment="">
        >>> mdf.get(group=2, index=1)
        <Signal Sig:
                samples=[ 21.  21.  21.  21.  21.]
                timestamps=[0 1 2 3 4]
                unit=""
                info=None
                comment="">

        """

        if skip_channel_validation:
            gp_nr, ch_nr = group, index
        else:
            gp_nr, ch_nr = self._validate_channel_selection(name, group, index)

        grp = self.groups[gp_nr]

        # get the channel object
        channel = grp.channels[ch_nr]
        dependency_list = grp.channel_dependencies[ch_nr]

        master_is_required = not samples_only or raster

        if dependency_list:
            if not isinstance(dependency_list[0], ChannelArrayBlock):
                vals, timestamps, invalidation_bits, encoding = self._get_structure(
                    channel=channel,
                    group=grp,
                    group_index=gp_nr,
                    channel_index=ch_nr,
                    dependency_list=dependency_list,
                    raster=raster,
                    data=data,
                    ignore_invalidation_bits=ignore_invalidation_bits,
                    record_offset=record_offset,
                    record_count=record_count,
                    master_is_required=master_is_required,
                )
            else:
                vals, timestamps, invalidation_bits, encoding = self._get_array(
                    channel=channel,
                    group=grp,
                    group_index=gp_nr,
                    channel_index=ch_nr,
                    dependency_list=dependency_list,
                    raster=raster,
                    data=data,
                    ignore_invalidation_bits=ignore_invalidation_bits,
                    record_offset=record_offset,
                    record_count=record_count,
                    master_is_required=master_is_required,
                )

        else:
            vals, timestamps, invalidation_bits, encoding = self._get_scalar(
                channel=channel,
                group=grp,
                group_index=gp_nr,
                channel_index=ch_nr,
                dependency_list=dependency_list,
                raster=raster,
                data=data,
                ignore_invalidation_bits=ignore_invalidation_bits,
                record_offset=record_offset,
                record_count=record_count,
                master_is_required=master_is_required,
            )

        if samples_only:
            if not raw:
                conversion = channel.conversion
                if conversion:
                    vals = conversion.convert(vals)

            res = vals, invalidation_bits
        else:
            conversion = channel.conversion
            if not raw:
                if conversion:
                    vals = conversion.convert(vals)
                    conversion = None

                if vals.dtype.kind == "S":
                    encoding = "utf-8"

            channel_type = channel.channel_type

            if name is None:
                name = channel.name

            unit = conversion and conversion.unit or channel.unit

            comment = channel.comment

            source = channel.source

            if source:
                source = Source.from_source(source)
            else:
                cg_source = grp.channel_group.acq_source
                if cg_source:
                    source = Source.from_source(cg_source)
                else:
                    source = None

            if channel.attachment is not None:
                attachment = self.extract_attachment(
                    channel.attachment,
                )
            else:
                attachment = None

            master_metadata = self._master_channel_metadata.get(gp_nr, None)

            stream_sync = channel_type == v4c.CHANNEL_TYPE_SYNC

            try:
                res = Signal(
                    samples=vals,
                    timestamps=timestamps,
                    unit=unit,
                    name=name,
                    comment=comment,
                    conversion=conversion,
                    raw=raw,
                    master_metadata=master_metadata,
                    attachment=attachment,
                    source=source,
                    display_names=channel.display_names,
                    bit_count=channel.bit_count,
                    stream_sync=stream_sync,
                    invalidation_bits=invalidation_bits,
                    encoding=encoding,
                    group_index=gp_nr,
                    channel_index=ch_nr,
                )
            except:
                debug_channel(self, grp, channel, dependency_list)
                raise

        return res

    def _get_structure(
        self,
        channel: Channel,
        group: Group,
        group_index: int,
        channel_index: int,
        dependency_list: list[tuple[int, int]],
        raster: RasterType | None,
        data: bytes | None,
        ignore_invalidation_bits: bool,
        record_offset: int,
        record_count: int | None,
        master_is_required: bool,
    ) -> tuple[NDArray[Any], NDArray[Any] | None, NDArray[Any] | None, None]:
        grp = group
        gp_nr = group_index
        # get data group record
        self._prepare_record(grp)

        # get group data
        if data is None:
            data = self._load_data(
                grp, record_offset=record_offset, record_count=record_count
            )
        else:
            data = (data,)

        channel_invalidation_present = channel.flags & (
            v4c.FLAG_CN_ALL_INVALID | v4c.FLAG_CN_INVALIDATION_PRESENT
        )

        _dtype = dtype(channel.dtype_fmt)
        if _dtype.itemsize == channel.bit_count // 8:
            fast_path = True
            channel_values = []
            timestamps = []
            invalidation_bits = []

            byte_offset = channel.byte_offset
            record_size = (
                grp.channel_group.samples_byte_nr
                + grp.channel_group.invalidation_bytes_nr
            )

            count = 0
            for fragment in data:

                bts = fragment[0]

                buffer = get_channel_raw_bytes(
                    bts, record_size, byte_offset, _dtype.itemsize
                )

                channel_values.append(frombuffer(buffer, dtype=_dtype))

                if master_is_required:
                    timestamps.append(self.get_master(gp_nr, fragment, one_piece=True))
                if channel_invalidation_present:
                    invalidation_bits.append(
                        self.get_invalidation_bits(gp_nr, channel, fragment)
                    )

                count += 1
        else:
            unique_names = UniqueDB()
            fast_path = False
            names = [
                unique_names.get_unique_name(grp.channels[ch_nr].name)
                for _, ch_nr in dependency_list
            ]

            channel_values = [[] for _ in dependency_list]
            timestamps = []
            invalidation_bits = []

            count = 0
            for fragment in data:
                for i, (dg_nr, ch_nr) in enumerate(dependency_list):
                    vals = self.get(
                        group=dg_nr,
                        index=ch_nr,
                        samples_only=True,
                        data=fragment,
                        ignore_invalidation_bits=ignore_invalidation_bits,
                        record_offset=record_offset,
                        record_count=record_count,
                        raw=True,
                    )[0]
                    channel_values[i].append(vals)
                if master_is_required:
                    timestamps.append(self.get_master(gp_nr, fragment, one_piece=True))
                if channel_invalidation_present:
                    invalidation_bits.append(
                        self.get_invalidation_bits(gp_nr, channel, fragment)
                    )

                count += 1

        if fast_path:
            total_size = sum(len(_) for _ in channel_values)
            shape = (total_size,) + channel_values[0].shape[1:]

            if count > 1:
                out = empty(shape, dtype=channel_values[0].dtype)
                vals = concatenate(channel_values, out=out)
            else:
                vals = channel_values[0]
        else:
            total_size = sum(len(_) for _ in channel_values[0])

            if count > 1:
                arrays = [
                    concatenate(
                        lst,
                        out=empty((total_size,) + lst[0].shape[1:], dtype=lst[0].dtype),
                    )
                    for lst in channel_values
                ]
            else:
                arrays = [lst[0] for lst in channel_values]
            types = [
                (name_, arr.dtype, arr.shape[1:]) for name_, arr in zip(names, arrays)
            ]
            types = dtype(types)

            vals = fromarrays(arrays, dtype=types)

        if master_is_required:
            if count > 1:
                out = empty(total_size, dtype=timestamps[0].dtype)
                timestamps = concatenate(timestamps, out=out)
            else:
                timestamps = timestamps[0]
        else:
            timestamps = None

        if channel_invalidation_present:
            if count > 1:
                out = empty(total_size, dtype=invalidation_bits[0].dtype)
                invalidation_bits = concatenate(invalidation_bits, out=out)
            else:
                invalidation_bits = invalidation_bits[0]
            if not ignore_invalidation_bits:
                vals = vals[nonzero(~invalidation_bits)[0]]
                if master_is_required:
                    timestamps = timestamps[nonzero(~invalidation_bits)[0]]
                invalidation_bits = None
        else:
            invalidation_bits = None

        if raster and len(timestamps) > 1:
            t = arange(timestamps[0], timestamps[-1], raster)

            vals = Signal(
                vals, timestamps, name="_", invalidation_bits=invalidation_bits
            ).interp(
                t,
                integer_interpolation_mode=self._integer_interpolation,
                float_interpolation_mode=self._float_interpolation,
            )

            vals, timestamps, invalidation_bits = (
                vals.samples,
                vals.timestamps,
                vals.invalidation_bits,
            )

        return vals, timestamps, invalidation_bits, None

    def _get_array(
        self,
        channel: Channel,
        group: Group,
        group_index: int,
        channel_index: int,
        dependency_list: list[tuple[int, int]],
        raster: RasterType | None,
        data: bytes | None,
        ignore_invalidation_bits: bool,
        record_offset: int,
        record_count: int | None,
        master_is_required: bool,
    ) -> tuple[NDArray[Any], NDArray[Any] | None, NDArray[Any] | None, None]:

        grp = group
        gp_nr = group_index
        ch_nr = channel_index
        # get data group record
        self._prepare_record(grp)

        # get group data
        if data is None:
            data = self._load_data(
                grp, record_offset=record_offset, record_count=record_count
            )
        else:
            data = (data,)

        dep = ca_block = dependency_list[0]
        shape = tuple(ca_block[f"dim_size_{i}"] for i in range(ca_block.dims))
        shape = tuple(dim for dim in shape if dim > 1)
        shape = shape or (1,)

        dim = 1
        for d in shape:
            dim *= d

        item_size = channel.bit_count // 8
        size = item_size * dim

        if group.uses_ld:
            record_size = group.channel_group.samples_byte_nr
        else:
            record_size = (
                group.channel_group.samples_byte_nr
                + group.channel_group.invalidation_bytes_nr
            )

        channel_dtype = get_fmt_v4(
            channel.data_type,
            channel.bit_count,
            channel.channel_type,
        )

        byte_offset = channel.byte_offset

        types = [
            ("", f"a{byte_offset}"),
            ("vals", channel_dtype, shape),
            ("", f"a{record_size - size - byte_offset}"),
        ]

        dtype_fmt = dtype(types)

        channel_invalidation_present = channel.flags & (
            v4c.FLAG_CN_ALL_INVALID | v4c.FLAG_CN_INVALIDATION_PRESENT
        )

        channel_group = grp.channel_group
        samples_size = (
            channel_group.samples_byte_nr + channel_group.invalidation_bytes_nr
        )

        channel_values = []
        timestamps = []
        invalidation_bits = []
        count = 0

        for fragment in data:

            arrays = []
            types = []

            data_bytes, offset, _count, invalidation_bytes = fragment

            cycles = len(data_bytes) // samples_size

            vals = frombuffer(data_bytes, dtype=dtype_fmt)["vals"]

            if dep.flags & v4c.FLAG_CA_INVERSE_LAYOUT:
                shape = vals.shape
                shape = (shape[0],) + shape[1:][::-1]
                vals = vals.reshape(shape)

                axes = (0,) + tuple(range(len(shape) - 1, 0, -1))
                vals = transpose(vals, axes=axes)

            cycles_nr = len(vals)

            for ca_block in dependency_list[:1]:
                dims_nr = ca_block.dims

                if ca_block.ca_type == v4c.CA_TYPE_SCALE_AXIS:
                    shape = (ca_block.dim_size_0,)
                    arrays.append(vals)
                    dtype_pair = channel.name, vals.dtype, shape
                    types.append(dtype_pair)

                elif ca_block.ca_type == v4c.CA_TYPE_LOOKUP:
                    shape = vals.shape[1:]
                    arrays.append(vals)
                    dtype_pair = channel.name, vals.dtype, shape
                    types.append(dtype_pair)

                    if ca_block.flags & v4c.FLAG_CA_FIXED_AXIS:
                        for i in range(dims_nr):
                            shape = (ca_block[f"dim_size_{i}"],)
                            axis = []
                            for j in range(shape[0]):
                                key = f"axis_{i}_value_{j}"
                                axis.append(ca_block[key])
                            axis = array(axis)
                            axis = array([axis for _ in range(cycles_nr)])
                            arrays.append(axis)
                            dtype_pair = (f"axis_{i}", axis.dtype, shape)
                            types.append(dtype_pair)
                    else:
                        for i in range(dims_nr):

                            axis = ca_block.axis_channels[i]
                            shape = (ca_block[f"dim_size_{i}"],)

                            if axis is None:
                                axisname = f"axis_{i}"
                                axis_values = array(
                                    [arange(shape[0])] * cycles,
                                    dtype=f"({shape[0]},)f8",
                                )

                            else:
                                try:
                                    (ref_dg_nr, ref_ch_nr) = ca_block.axis_channels[i]
                                except:
                                    debug_channel(self, grp, channel, dependency_list)
                                    raise

                                axisname = (
                                    self.groups[ref_dg_nr].channels[ref_ch_nr].name
                                )

                                if ref_dg_nr == gp_nr:
                                    axis_values = self.get(
                                        group=ref_dg_nr,
                                        index=ref_ch_nr,
                                        samples_only=True,
                                        data=fragment,
                                        ignore_invalidation_bits=ignore_invalidation_bits,
                                        record_offset=record_offset,
                                        record_count=cycles,
                                        raw=True,
                                    )[0]
                                else:
                                    channel_group = grp.channel_group
                                    record_size = channel_group.samples_byte_nr
                                    record_size += channel_group.invalidation_bytes_nr
                                    start = offset // record_size
                                    end = start + len(data_bytes) // record_size + 1
                                    ref = self.get(
                                        group=ref_dg_nr,
                                        index=ref_ch_nr,
                                        samples_only=True,
                                        ignore_invalidation_bits=ignore_invalidation_bits,
                                        record_offset=record_offset,
                                        record_count=cycles,
                                        raw=True,
                                    )[0]
                                    axis_values = ref[start:end].copy()

                                axis_values = axis_values[axisname]
                                if len(axis_values) == 0 and cycles:
                                    axis_values = array([arange(shape[0])] * cycles)

                            arrays.append(axis_values)
                            dtype_pair = (axisname, axis_values.dtype, shape)
                            types.append(dtype_pair)

                elif ca_block.ca_type == v4c.CA_TYPE_ARRAY:
                    shape = vals.shape[1:]
                    arrays.append(vals)
                    dtype_pair = channel.name, vals.dtype, shape
                    types.append(dtype_pair)

            for ca_block in dependency_list[1:]:
                dims_nr = ca_block.dims

                if ca_block.flags & v4c.FLAG_CA_FIXED_AXIS:
                    for i in range(dims_nr):
                        shape = (ca_block[f"dim_size_{i}"],)
                        axis = []
                        for j in range(shape[0]):
                            key = f"axis_{i}_value_{j}"
                            axis.append(ca_block[key])

                        axis = array(
                            [axis for _ in range(cycles_nr)], dtype=f"{shape}f8"
                        )
                        arrays.append(axis)
                        types.append((f"axis_{i}", axis.dtype, shape))
                else:
                    for i in range(dims_nr):
                        axis = ca_block.axis_channels[i]
                        shape = (ca_block[f"dim_size_{i}"],)

                        if axis is None:
                            axisname = f"axis_{i}"
                            axis_values = array(
                                [arange(shape[0])] * cycles, dtype=f"({shape[0]},)f8"
                            )

                        else:
                            try:
                                ref_dg_nr, ref_ch_nr = ca_block.axis_channels[i]
                            except:
                                debug_channel(self, grp, channel, dependency_list)
                                raise

                            axisname = self.groups[ref_dg_nr].channels[ref_ch_nr].name

                            if ref_dg_nr == gp_nr:
                                axis_values = self.get(
                                    group=ref_dg_nr,
                                    index=ref_ch_nr,
                                    samples_only=True,
                                    data=fragment,
                                    ignore_invalidation_bits=ignore_invalidation_bits,
                                    record_offset=record_offset,
                                    record_count=cycles,
                                    raw=True,
                                )[0]
                            else:
                                channel_group = grp.channel_group
                                record_size = channel_group.samples_byte_nr
                                record_size += channel_group.invalidation_bytes_nr
                                start = offset // record_size
                                end = start + len(data_bytes) // record_size + 1
                                ref = self.get(
                                    group=ref_dg_nr,
                                    index=ref_ch_nr,
                                    samples_only=True,
                                    ignore_invalidation_bits=ignore_invalidation_bits,
                                    record_offset=record_offset,
                                    record_count=cycles,
                                    raw=True,
                                )[0]
                                axis_values = ref[start:end].copy()
                            axis_values = axis_values[axisname]
                            if len(axis_values) == 0 and cycles:
                                axis_values = array([arange(shape[0])] * cycles)

                        arrays.append(axis_values)
                        dtype_pair = (axisname, axis_values.dtype, shape)
                        types.append(dtype_pair)

            vals = fromarrays(arrays, dtype(types))

            if master_is_required:
                timestamps.append(self.get_master(gp_nr, fragment, one_piece=True))
            if channel_invalidation_present:
                invalidation_bits.append(
                    self.get_invalidation_bits(gp_nr, channel, fragment)
                )

            channel_values.append(vals)
            count += 1

        if count > 1:
            total_size = sum(len(_) for _ in channel_values)
            shape = (total_size,) + channel_values[0].shape[1:]

        if count > 1:
            out = empty(shape, dtype=channel_values[0].dtype)
            vals = concatenate(channel_values, out=out)
        elif count == 1:
            vals = channel_values[0]
        else:
            vals = []

        if master_is_required:
            if count > 1:
                out = empty(total_size, dtype=timestamps[0].dtype)
                timestamps = concatenate(timestamps, out=out)
            else:
                timestamps = timestamps[0]
        else:
            timestamps = None

        if channel_invalidation_present:
            if count > 1:
                out = empty(total_size, dtype=invalidation_bits[0].dtype)
                invalidation_bits = concatenate(invalidation_bits, out=out)
            else:
                invalidation_bits = invalidation_bits[0]
            if not ignore_invalidation_bits:
                vals = vals[nonzero(~invalidation_bits)[0]]
                if master_is_required:
                    timestamps = timestamps[nonzero(~invalidation_bits)[0]]
                invalidation_bits = None
        else:
            invalidation_bits = None

        if raster and len(timestamps) > 1:
            t = arange(timestamps[0], timestamps[-1], raster)

            vals = Signal(
                vals, timestamps, name="_", invalidation_bits=invalidation_bits
            ).interp(
                t,
                integer_interpolation_mode=self._integer_interpolation,
                float_interpolation_mode=self._float_interpolation,
            )

            vals, timestamps, invalidation_bits = (
                vals.samples,
                vals.timestamps,
                vals.invalidation_bits,
            )

        return vals, timestamps, invalidation_bits, None

    def _get_scalar(
        self,
        channel: Channel,
        group: Group,
        group_index: int,
        channel_index: int,
        dependency_list: list[tuple[int, int]],
        raster: RasterType | None,
        data: bytes | None,
        ignore_invalidation_bits: bool,
        record_offset: int,
        record_count: int | None,
        master_is_required: bool,
    ) -> tuple[NDArray[Any], NDArray[Any] | None, NDArray[Any] | None, str | None]:
        grp = group
        gp_nr = group_index
        ch_nr = channel_index

        # get group data
        if data is None:
            data = self._load_data(
                grp, record_offset=record_offset, record_count=record_count
            )
            one_piece = False
        else:
            one_piece = True

        channel_invalidation_present = channel.flags & (
            v4c.FLAG_CN_ALL_INVALID | v4c.FLAG_CN_INVALIDATION_PRESENT
        )

        data_type = channel.data_type
        channel_type = channel.channel_type
        bit_count = channel.bit_count

        encoding = None

        channel_dtype = channel.dtype_fmt

        # get channel values
        if channel_type in {
            v4c.CHANNEL_TYPE_VIRTUAL,
            v4c.CHANNEL_TYPE_VIRTUAL_MASTER,
        }:
            if not channel.dtype_fmt:
                channel.dtype_fmt = dtype(get_fmt_v4(data_type, 64))
            ch_dtype = channel.dtype_fmt

            channel_values = []
            timestamps = []
            invalidation_bits = []

            channel_group = grp.channel_group
            record_size = channel_group.samples_byte_nr
            record_size += channel_group.invalidation_bytes_nr

            count = 0

            if one_piece:
                data = (data,)

            for fragment in data:
                data_bytes, offset, _count, invalidation_bytes = fragment
                offset = offset // record_size

                vals = arange(len(data_bytes) // record_size, dtype=ch_dtype)
                vals += offset

                if master_is_required:
                    timestamps.append(
                        self.get_master(
                            gp_nr,
                            fragment,
                            record_offset=offset,
                            record_count=_count,
                            one_piece=True,
                        )
                    )
                if channel_invalidation_present:
                    invalidation_bits.append(
                        self.get_invalidation_bits(gp_nr, channel, fragment)
                    )

                channel_values.append(vals)
                count += 1

            if count > 1:
                total_size = sum(len(_) for _ in channel_values)
                shape = (total_size,) + channel_values[0].shape[1:]

            if count > 1:
                out = empty(shape, dtype=channel_values[0].dtype)
                vals = concatenate(channel_values, out=out)
            elif count == 1:
                vals = channel_values[0]
            else:
                vals = []

            if master_is_required:
                if count > 1:
                    out = empty(total_size, dtype=timestamps[0].dtype)
                    timestamps = concatenate(timestamps, out=out)
                else:
                    timestamps = timestamps[0]

            if channel_invalidation_present:
                if count > 1:
                    out = empty(total_size, dtype=invalidation_bits[0].dtype)
                    invalidation_bits = concatenate(invalidation_bits, out=out)
                else:
                    invalidation_bits = invalidation_bits[0]
                if not ignore_invalidation_bits:
                    vals = vals[nonzero(~invalidation_bits)[0]]
                    if master_is_required:
                        timestamps = timestamps[nonzero(~invalidation_bits)[0]]
                    invalidation_bits = None
            else:
                invalidation_bits = None

            if raster and len(timestamps) > 1:
                num = float(float32((timestamps[-1] - timestamps[0]) / raster))
                if num.is_integer():
                    t = linspace(timestamps[0], timestamps[-1], int(num))
                else:
                    t = arange(timestamps[0], timestamps[-1], raster)

                vals = Signal(
                    vals, timestamps, name="_", invalidation_bits=invalidation_bits
                ).interp(
                    t,
                    integer_interpolation_mode=self._integer_interpolation,
                    float_interpolation_mode=self._float_interpolation,
                )

                vals, timestamps, invalidation_bits = (
                    vals.samples,
                    vals.timestamps,
                    vals.invalidation_bits,
                )

            if channel.conversion:
                vals = channel.conversion.convert(vals)

        else:
            channel_group = grp.channel_group

            record_size = channel_group.samples_byte_nr

            if one_piece:

                fragment = data
                data_bytes = fragment[0]

                info = grp.record[ch_nr]

                if info is not None:
                    dtype_, byte_size, byte_offset, bit_offset = info
                    if (
                        ch_nr == 0
                        and len(grp.channels) == 1
                        and channel.dtype_fmt.itemsize == record_size
                    ):
                        buffer = bytearray(data_bytes)
                    else:
                        buffer = get_channel_raw_bytes(
                            data_bytes,
                            record_size + channel_group.invalidation_bytes_nr,
                            byte_offset,
                            byte_size,
                        )

                    vals = frombuffer(buffer, dtype=dtype_)

                    if not channel.standard_C_size:

                        size = byte_size

                        if channel_dtype.byteorder == "|" and data_type in (
                            v4c.DATA_TYPE_SIGNED_MOTOROLA,
                            v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
                        ):
                            view = f">u{vals.itemsize}"
                        else:
                            view = f"{channel_dtype.byteorder}u{vals.itemsize}"

                        if dtype(view) != vals.dtype:
                            vals = vals.view(view)

                        if bit_offset:
                            vals >>= bit_offset

                        if bit_count != size * 8:
                            if data_type in v4c.SIGNED_INT:
                                vals = as_non_byte_sized_signed_int(vals, bit_count)
                            else:
                                mask = (1 << bit_count) - 1
                                vals &= mask
                        elif data_type in v4c.SIGNED_INT:
                            view = f"{channel_dtype.byteorder}i{vals.itemsize}"
                            if dtype(view) != vals.dtype:
                                vals = vals.view(view)

                else:
                    vals = self._get_not_byte_aligned_data(data_bytes, grp, ch_nr)

                if bit_count == 1 and self._single_bit_uint_as_bool:
                    vals = array(vals, dtype=bool)

                if master_is_required:
                    timestamps = self.get_master(gp_nr, fragment, one_piece=True)
                else:
                    timestamps = None

                if channel_invalidation_present:
                    invalidation_bits = self.get_invalidation_bits(
                        gp_nr, channel, fragment
                    )

                    if not ignore_invalidation_bits:
                        vals = vals[nonzero(~invalidation_bits)[0]]
                        if master_is_required:
                            timestamps = timestamps[nonzero(~invalidation_bits)[0]]
                        invalidation_bits = None
                else:
                    invalidation_bits = None
            else:
                channel_values = []
                timestamps = []
                invalidation_bits = []

                info = grp.record[ch_nr]

                if info is None:
                    for count, fragment in enumerate(data, 1):
                        data_bytes, offset, _count, invalidation_bytes = fragment

                        vals = self._get_not_byte_aligned_data(data_bytes, grp, ch_nr)

                        if bit_count == 1 and self._single_bit_uint_as_bool:
                            vals = array(vals, dtype=bool)

                        if master_is_required:
                            timestamps.append(
                                self.get_master(gp_nr, fragment, one_piece=True)
                            )
                        if channel_invalidation_present:
                            invalidation_bits.append(
                                self.get_invalidation_bits(gp_nr, channel, fragment)
                            )

                        channel_values.append(vals)
                    vals = concatenate(channel_values)
                else:

                    dtype_, byte_size, byte_offset, bit_offset = info

                    buffer = []
                    count = 0

                    for count, fragment in enumerate(data, 1):
                        data_bytes = fragment[0]

                        if (
                            ch_nr == 0
                            and len(grp.channels) == 1
                            and channel.dtype_fmt.itemsize == record_size
                        ):
                            buffer.append(data_bytes)
                        else:
                            buffer.append(
                                get_channel_raw_bytes(
                                    data_bytes,
                                    record_size + channel_group.invalidation_bytes_nr,
                                    byte_offset,
                                    byte_size,
                                )
                            )

                        if master_is_required:
                            timestamps.append(
                                self.get_master(gp_nr, fragment, one_piece=True)
                            )
                        if channel_invalidation_present:
                            invalidation_bits.append(
                                self.get_invalidation_bits(gp_nr, channel, fragment)
                            )

                    if count > 1:
                        buffer = bytearray().join(buffer)
                    elif count == 1:
                        buffer = buffer[0]
                    else:
                        buffer = bytearray()

                    vals = frombuffer(buffer, dtype=dtype_)

                    if not channel.standard_C_size:

                        size = dtype_.itemsize

                        if channel_dtype.byteorder == "|" and data_type in (
                            v4c.DATA_TYPE_SIGNED_MOTOROLA,
                            v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
                        ):
                            view = f">u{vals.itemsize}"
                        else:
                            view = f"{channel_dtype.byteorder}u{vals.itemsize}"

                        if dtype(view) != dtype_:
                            vals = vals.view(view)

                        if bit_offset:
                            vals >>= bit_offset

                        if bit_count != size * 8:
                            if data_type in v4c.SIGNED_INT:
                                vals = as_non_byte_sized_signed_int(vals, bit_count)
                            else:
                                mask = (1 << bit_count) - 1
                                vals &= mask
                        elif data_type in v4c.SIGNED_INT:
                            view = f"{channel_dtype.byteorder}i{vals.itemsize}"
                            if dtype(view) != vals.dtype:
                                vals = vals.view(view)

                    if bit_count == 1 and self._single_bit_uint_as_bool:
                        vals = array(vals, dtype=bool)

                    total_size = len(vals)

                    if master_is_required:
                        if count > 1:
                            out = empty(total_size, dtype=timestamps[0].dtype)
                            timestamps = concatenate(timestamps, out=out)
                        elif count == 1:
                            timestamps = timestamps[0]
                        else:
                            timestamps = []

                    if channel_invalidation_present:
                        if count > 1:
                            out = empty(total_size, dtype=invalidation_bits[0].dtype)
                            invalidation_bits = concatenate(invalidation_bits, out=out)
                        elif count == 1:
                            invalidation_bits = invalidation_bits[0]
                        else:
                            invalidation_bits = []
                        if not ignore_invalidation_bits:
                            vals = vals[nonzero(~invalidation_bits)[0]]
                            if master_is_required:
                                timestamps = timestamps[nonzero(~invalidation_bits)[0]]
                            invalidation_bits = None
                    else:
                        invalidation_bits = None

            if raster and len(timestamps) > 1:

                num = float(float32((timestamps[-1] - timestamps[0]) / raster))
                if num.is_integer():
                    t = linspace(timestamps[0], timestamps[-1], int(num))
                else:
                    t = arange(timestamps[0], timestamps[-1], raster)

                vals = Signal(
                    vals, timestamps, name="_", invalidation_bits=invalidation_bits
                ).interp(
                    t,
                    integer_interpolation_mode=self._integer_interpolation,
                    float_interpolation_mode=self._float_interpolation,
                )

                vals, timestamps, invalidation_bits = (
                    vals.samples,
                    vals.timestamps,
                    vals.invalidation_bits,
                )

        if channel_type == v4c.CHANNEL_TYPE_VLSD:
            count_ = len(vals)

            if count_:
                signal_data = self._load_signal_data(
                    group=grp, index=ch_nr, start_offset=vals[0], end_offset=vals[-1]
                )
            else:
                signal_data = b""

            if signal_data:
                if data_type in (
                    v4c.DATA_TYPE_BYTEARRAY,
                    v4c.DATA_TYPE_UNSIGNED_INTEL,
                    v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
                ):
                    vals = extract(signal_data, 1, vals - vals[0])
                else:
                    vals = extract(signal_data, 0, vals - vals[0])

                if data_type not in (
                    v4c.DATA_TYPE_BYTEARRAY,
                    v4c.DATA_TYPE_UNSIGNED_INTEL,
                    v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
                ):

                    if data_type == v4c.DATA_TYPE_STRING_UTF_16_BE:
                        encoding = "utf-16-be"

                    elif data_type == v4c.DATA_TYPE_STRING_UTF_16_LE:
                        encoding = "utf-16-le"

                    elif data_type == v4c.DATA_TYPE_STRING_UTF_8:
                        encoding = "utf-8"

                    elif data_type == v4c.DATA_TYPE_STRING_LATIN_1:
                        encoding = "latin-1"

                    else:
                        raise MdfException(
                            f'wrong data type "{data_type}" for vlsd channel'
                        )
            else:
                if len(vals):
                    raise MdfException(
                        f'Wrong signal data block refence (0x{channel.data_block_addr:X}) for VLSD channel "{channel.name}"'
                    )
                # no VLSD signal data samples
                if data_type != v4c.DATA_TYPE_BYTEARRAY:
                    vals = array([], dtype="S")

                    if data_type == v4c.DATA_TYPE_STRING_UTF_16_BE:
                        encoding = "utf-16-be"

                    elif data_type == v4c.DATA_TYPE_STRING_UTF_16_LE:
                        encoding = "utf-16-le"

                    elif data_type == v4c.DATA_TYPE_STRING_UTF_8:
                        encoding = "utf-8"

                    elif data_type == v4c.DATA_TYPE_STRING_LATIN_1:
                        encoding = "latin-1"

                    else:
                        raise MdfException(
                            f'wrong data type "{data_type}" for vlsd channel'
                        )
                else:
                    vals = array(
                        [],
                        dtype=get_fmt_v4(data_type, bit_count, v4c.CHANNEL_TYPE_VALUE),
                    )

        elif (
            v4c.DATA_TYPE_STRING_LATIN_1 <= data_type <= v4c.DATA_TYPE_STRING_UTF_16_BE
        ):
            if channel_type in (v4c.CHANNEL_TYPE_VALUE, v4c.CHANNEL_TYPE_MLSD):

                if data_type == v4c.DATA_TYPE_STRING_UTF_16_BE:
                    encoding = "utf-16-be"

                elif data_type == v4c.DATA_TYPE_STRING_UTF_16_LE:
                    encoding = "utf-16-le"

                elif data_type == v4c.DATA_TYPE_STRING_UTF_8:
                    encoding = "utf-8"

                elif data_type == v4c.DATA_TYPE_STRING_LATIN_1:
                    encoding = "latin-1"

                else:
                    raise MdfException(
                        f'wrong data type "{data_type}" for string channel'
                    )

        elif data_type in (v4c.DATA_TYPE_CANOPEN_TIME, v4c.DATA_TYPE_CANOPEN_DATE):
            # CANopen date
            if data_type == v4c.DATA_TYPE_CANOPEN_DATE:

                types = dtype(
                    [
                        ("ms", "<u2"),
                        ("min", "<u1"),
                        ("hour", "<u1"),
                        ("day", "<u1"),
                        ("month", "<u1"),
                        ("year", "<u1"),
                    ]
                )
                vals = vals.view(types)

                arrays = []
                arrays.append(vals["ms"])
                # bit 6 and 7 of minutes are reserved
                arrays.append(vals["min"] & 0x3F)
                # only firt 4 bits of hour are used
                arrays.append(vals["hour"] & 0xF)
                # the first 4 bits are the day number
                arrays.append(vals["day"] & 0xF)
                # bit 6 and 7 of month are reserved
                arrays.append(vals["month"] & 0x3F)
                # bit 7 of year is reserved
                arrays.append(vals["year"] & 0x7F)
                # add summer or standard time information for hour
                arrays.append((vals["hour"] & 0x80) >> 7)
                # add day of week information
                arrays.append((vals["day"] & 0xF0) >> 4)

                names = [
                    "ms",
                    "min",
                    "hour",
                    "day",
                    "month",
                    "year",
                    "summer_time",
                    "day_of_week",
                ]
                vals = fromarrays(arrays, names=names)

            # CANopen time
            elif data_type == v4c.DATA_TYPE_CANOPEN_TIME:

                types = dtype([("ms", "<u4"), ("days", "<u2")])
                vals = vals.view(types)

        return vals, timestamps, invalidation_bits, encoding

    def _get_not_byte_aligned_data(
        self, data: bytes, group: Group, ch_nr: int
    ) -> NDArray[Any]:
        big_endian_types = (
            v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
            v4c.DATA_TYPE_REAL_MOTOROLA,
            v4c.DATA_TYPE_SIGNED_MOTOROLA,
        )

        if group.uses_ld:
            record_size = group.channel_group.samples_byte_nr
        else:
            record_size = (
                group.channel_group.samples_byte_nr
                + group.channel_group.invalidation_bytes_nr
            )

        channel = group.channels[ch_nr]

        bit_offset = channel.bit_offset
        byte_offset = channel.byte_offset
        bit_count = channel.bit_count

        if ch_nr >= 0:
            dependencies = group.channel_dependencies[ch_nr]
            if dependencies and isinstance(dependencies[0], ChannelArrayBlock):
                ca_block = dependencies[0]

                size = bit_count // 8

                shape = tuple(ca_block[f"dim_size_{i}"] for i in range(ca_block.dims))
                if ca_block.byte_offset_base // size > 1 and len(shape) == 1:
                    shape += (ca_block.byte_offset_base // size,)
                dim = 1
                for d in shape:
                    dim *= d
                size *= dim
                bit_count = size * 8

        byte_size = bit_offset + bit_count
        if byte_size % 8:
            byte_size = (byte_size // 8) + 1
        else:
            byte_size //= 8

        types = [
            ("", f"a{byte_offset}"),
            ("vals", f"({byte_size},)u1"),
            ("", f"a{record_size - byte_size - byte_offset}"),
        ]

        vals = fromstring(data, dtype=dtype(types))

        vals = vals["vals"]

        if byte_size in {1, 2, 4, 8}:
            extra_bytes = 0
        elif byte_size < 8:
            extra_bytes = 4 - (byte_size % 4)
        else:
            extra_bytes = 0

        std_size = byte_size + extra_bytes

        big_endian = channel.data_type in big_endian_types

        # prepend or append extra bytes columns
        # to get a standard size number of bytes

        if extra_bytes:
            if big_endian:

                vals = column_stack(
                    [vals, zeros(len(vals), dtype=f"<({extra_bytes},)u1")]
                )
                try:
                    vals = vals.view(f">u{std_size}").ravel()
                except:
                    vals = frombuffer(vals.tobytes(), dtype=f">u{std_size}")

                vals = vals >> (extra_bytes * 8 + bit_offset)
                vals &= (1 << bit_count) - 1

            else:
                vals = column_stack(
                    [vals, zeros(len(vals), dtype=f"<({extra_bytes},)u1")]
                )
                try:
                    vals = vals.view(f"<u{std_size}").ravel()
                except:
                    vals = frombuffer(vals.tobytes(), dtype=f"<u{std_size}")

                vals = vals >> bit_offset
                vals &= (1 << bit_count) - 1

        else:
            if big_endian:
                try:
                    vals = vals.view(f">u{std_size}").ravel()
                except:
                    vals = frombuffer(vals.tobytes(), dtype=f">u{std_size}")

                vals = vals >> bit_offset
                vals &= (1 << bit_count) - 1

            else:
                try:
                    vals = vals.view(f"<u{std_size}").ravel()
                except:
                    vals = frombuffer(vals.tobytes(), dtype=f"<u{std_size}")

                vals = vals >> bit_offset
                vals &= (1 << bit_count) - 1

        data_type = channel.data_type

        if data_type in v4c.SIGNED_INT:
            return as_non_byte_sized_signed_int(vals, bit_count)
        elif data_type in v4c.FLOATS:
            return vals.view(get_fmt_v4(data_type, bit_count))
        else:
            return vals

    def included_channels(
        self,
        index: int | None = None,
        channels: ChannelsType | None = None,
        skip_master: bool = True,
        minimal: bool = True,
    ) -> dict[int, dict[int, Sequence[int]]]:

        if channels is None:
            virtual_channel_group = self.virtual_groups[index]
            groups = virtual_channel_group.groups

            gps = {}

            for gp_index in groups:

                group = self.groups[gp_index]

                included_channels = set(range(len(group.channels)))
                master_index = self.masters_db.get(gp_index, None)
                if master_index is not None:
                    included_channels.remove(master_index)

                channels = group.channels

                for dependencies in group.channel_dependencies:
                    if dependencies is None:
                        continue

                    if all(
                        not isinstance(dep, ChannelArrayBlock) for dep in dependencies
                    ):

                        for _, ch_nr in dependencies:
                            try:
                                included_channels.remove(ch_nr)
                            except KeyError:
                                pass
                    else:
                        for dep in dependencies:
                            for referenced_channels in (
                                dep.axis_channels,
                                dep.dynamic_size_channels,
                                dep.input_quantity_channels,
                            ):
                                for gp_nr, ch_nr in referenced_channels:
                                    if gp_nr == gp_index:
                                        try:
                                            included_channels.remove(ch_nr)
                                        except KeyError:
                                            pass

                            if dep.output_quantity_channel:
                                gp_nr, ch_nr = dep.output_quantity_channel
                                if gp_nr == gp_index:
                                    try:
                                        included_channels.remove(ch_nr)
                                    except KeyError:
                                        pass

                            if dep.comparison_quantity_channel:
                                gp_nr, ch_nr = dep.comparison_quantity_channel
                                if gp_nr == gp_index:
                                    try:
                                        included_channels.remove(ch_nr)
                                    except KeyError:
                                        pass

                gps[gp_index] = sorted(included_channels)

            result = {index: gps}
        else:
            gps = {}
            for item in channels:
                if isinstance(item, (list, tuple)):
                    if len(item) not in (2, 3):
                        raise MdfException(
                            "The items used for filtering must be strings, "
                            "or they must match the first 3 arguments of the get "
                            "method"
                        )
                    else:
                        group, idx = self._validate_channel_selection(*item)
                        gps_idx = gps.setdefault(group, set())
                        gps_idx.add(idx)
                else:
                    name = item
                    group, idx = self._validate_channel_selection(name)
                    gps_idx = gps.setdefault(group, set())
                    gps_idx.add(idx)

            result = {}
            for gp_index, channels in gps.items():
                master = self.virtual_groups_map[gp_index]
                group = self.groups[gp_index]

                if minimal:

                    channel_dependencies = [
                        group.channel_dependencies[ch_nr] for ch_nr in channels
                    ]

                    for dependencies in channel_dependencies:
                        if dependencies is None:
                            continue

                        if all(
                            not isinstance(dep, ChannelArrayBlock)
                            for dep in dependencies
                        ):

                            for _, ch_nr in dependencies:
                                try:
                                    channels.remove(ch_nr)
                                except KeyError:
                                    pass
                        else:
                            for dep in dependencies:
                                for referenced_channels in (
                                    dep.axis_channels,
                                    dep.dynamic_size_channels,
                                    dep.input_quantity_channels,
                                ):
                                    for gp_nr, ch_nr in referenced_channels:
                                        if gp_nr == gp_index:
                                            try:
                                                channels.remove(ch_nr)
                                            except KeyError:
                                                pass

                                if dep.output_quantity_channel:
                                    gp_nr, ch_nr = dep.output_quantity_channel
                                    if gp_nr == gp_index:
                                        try:
                                            channels.remove(ch_nr)
                                        except KeyError:
                                            pass

                                if dep.comparison_quantity_channel:
                                    gp_nr, ch_nr = dep.comparison_quantity_channel
                                    if gp_nr == gp_index:
                                        try:
                                            channels.remove(ch_nr)
                                        except KeyError:
                                            pass

                    gp_master = self.masters_db.get(gp_index, None)
                    if gp_master is not None and gp_master in channels:
                        channels.remove(gp_master)

                if master not in result:
                    result[master] = {}
                    result[master][master] = [self.masters_db.get(master, None)]

                result[master][gp_index] = sorted(channels)

        return result

    def _yield_selected_signals(
        self,
        index: int,
        groups: dict[int, Sequence[int]] | None = None,
        record_offset: int = 0,
        record_count: int | None = None,
        skip_master: bool = True,
        version: str | None = None,
    ) -> Iterator[Signal | tuple[NDArray[Any], NDArray[Any]]]:
        version = version or self.version
        virtual_channel_group = self.virtual_groups[index]
        record_size = virtual_channel_group.record_size

        if groups is None:
            groups = self.included_channels(index, skip_master=skip_master)[index]

        record_size = 0
        for group_index in groups:
            grp = self.groups[group_index]
            record_size += (
                grp.channel_group.samples_byte_nr
                + grp.channel_group.invalidation_bytes_nr
            )

        record_size = record_size or 1

        if self._read_fragment_size:
            count = self._read_fragment_size // record_size or 1
        else:
            if version < "4.20":
                count = 16 * 1024 * 1024 // record_size or 1
            else:
                count = 128 * 1024 * 1024 // record_size or 1

        data_streams = []
        for idx, group_index in enumerate(groups):
            grp = self.groups[group_index]
            grp.read_split_count = count
            data_streams.append(
                self._load_data(
                    grp, record_offset=record_offset, record_count=record_count
                )
            )
            if group_index == index:
                master_index = idx

        encodings = {group_index: [None] for groups_index in groups}

        self._set_temporary_master(None)
        idx = 0

        while True:
            try:
                fragments = [next(stream) for stream in data_streams]
            except:
                break

            _master = self.get_master(index, data=fragments[master_index])
            self._set_temporary_master(_master)

            if idx == 0:
                signals = []
            else:
                signals = [(_master, None)]

            for fragment, (group_index, channels) in zip(fragments, groups.items()):
                grp = self.groups[group_index]
                if not grp.single_channel_dtype:
                    self._prepare_record(grp)

                if idx == 0:
                    for channel_index in channels:
                        signals.append(
                            self.get(
                                group=group_index,
                                index=channel_index,
                                data=fragment,
                                raw=True,
                                ignore_invalidation_bits=True,
                                samples_only=False,
                            )
                        )
                    pass
                else:
                    for channel_index in channels:
                        signals.append(
                            self.get(
                                group=group_index,
                                index=channel_index,
                                data=fragment,
                                raw=True,
                                ignore_invalidation_bits=True,
                                samples_only=True,
                            )
                        )

                if version < "4.00":
                    if idx == 0:
                        for sig, channel_index in zip(signals, channels):
                            if sig.samples.dtype.kind == "S":

                                strsig = self.get(
                                    group=group_index,
                                    index=channel_index,
                                    samples_only=True,
                                    ignore_invalidation_bits=True,
                                )[0]

                                _dtype = strsig.dtype
                                sig.samples = sig.samples.astype(_dtype)
                                encodings[group_index].append((sig.encoding, _dtype))
                                del strsig
                                if sig.encoding != "latin-1":

                                    if sig.encoding == "utf-16-le":
                                        sig.samples = (
                                            sig.samples.view(uint16)
                                            .byteswap()
                                            .view(sig.samples.dtype)
                                        )
                                        sig.samples = encode(
                                            decode(sig.samples, "utf-16-be"), "latin-1"
                                        )
                                    else:
                                        sig.samples = encode(
                                            decode(sig.samples, sig.encoding),
                                            "latin-1",
                                        )
                                sig.samples = sig.samples.astype(_dtype)
                            else:
                                encodings[group_index].append(None)
                    else:
                        for i, (sig, encoding_tuple) in enumerate(
                            zip(signals, encodings[group_index])
                        ):

                            if encoding_tuple:
                                encoding, _dtype = encoding_tuple
                                samples = sig[0]
                                if encoding != "latin-1":

                                    if encoding == "utf-16-le":
                                        samples = (
                                            samples.view(uint16)
                                            .byteswap()
                                            .view(samples.dtype)
                                        )
                                        samples = encode(
                                            decode(samples, "utf-16-be"), "latin-1"
                                        )
                                    else:
                                        samples = encode(
                                            decode(samples, encoding), "latin-1"
                                        )
                                samples = samples.astype(_dtype)
                                signals[i] = (samples, sig[1])

            self._set_temporary_master(None)
            idx += 1
            yield signals

    def get_master(
        self,
        index: int,
        data: bytes | None = None,
        raster: RasterType | None = None,
        record_offset: int = 0,
        record_count: int | None = None,
        one_piece: bool = False,
    ) -> NDArray[Any]:
        """returns master channel samples for given group

        Parameters
        ----------
        index : int
            group index
        data : (bytes, int, int, bytes|None)
            (data block raw bytes, fragment offset, count, invalidation bytes); default None
        raster : float
            raster to be used for interpolation; default None

            .. deprecated:: 5.13.0

        record_offset : int
            if *data=None* use this to select the record offset from which the
            group data should be loaded
        record_count : int
            number of records to read; default *None* and in this case all
            available records are used

        Returns
        -------
        t : numpy.array
            master channel samples

        """

        if raster is not None:
            PendingDeprecationWarning(
                "the argument raster is deprecated since version 5.13.0 "
                "and will be removed in a future release"
            )
        if self._master is not None:
            return self._master

        group = self.groups[index]
        if group.channel_group.flags & v4c.FLAG_CG_REMOTE_MASTER:
            if data is not None:
                record_offset = data[1]
                record_count = data[2]
            return self.get_master(
                group.channel_group.cg_master_index,
                record_offset=record_offset,
                record_count=record_count,
            )

        time_ch_nr = self.masters_db.get(index, None)
        channel_group = group.channel_group
        record_size = channel_group.samples_byte_nr
        record_size += channel_group.invalidation_bytes_nr
        if record_count is not None:
            cycles_nr = record_count
        else:
            cycles_nr = group.channel_group.cycles_nr

        fragment = data
        if fragment:
            data_bytes, offset, _count, invalidation_bytes = fragment
            cycles_nr = len(data_bytes) // record_size if record_size else 0
        else:
            offset = 0
            _count = record_count

        if time_ch_nr is None:
            if record_size:
                t = arange(cycles_nr, dtype=float64)
                t += offset
            else:
                t = array([], dtype=float64)
            metadata = ("timestamps", v4c.SYNC_TYPE_TIME)
        else:

            time_ch = group.channels[time_ch_nr]
            time_conv = time_ch.conversion
            time_name = time_ch.name

            metadata = (time_name, time_ch.sync_type)

            if time_ch.channel_type == v4c.CHANNEL_TYPE_VIRTUAL_MASTER:
                time_a = time_conv["a"]
                time_b = time_conv["b"]
                t = arange(cycles_nr, dtype=float64)
                t += offset
                t *= time_a
                t += time_b

                if record_count is None:
                    t = t[record_offset:]
                else:
                    t = t[record_offset : record_offset + record_count]

            else:
                # check if the channel group contains just the master channel
                # and that there are no padding bytes
                if (
                    len(group.channels) == 1
                    and time_ch.dtype_fmt.itemsize == record_size
                ):
                    if one_piece:

                        data_bytes, offset, _count, _ = data

                        t = frombuffer(data_bytes, dtype=time_ch.dtype_fmt)
                    else:

                        # get data
                        if fragment is None:
                            data = self._load_data(
                                group,
                                record_offset=record_offset,
                                record_count=record_count,
                            )
                        else:
                            data = (fragment,)

                        buffer = bytearray().join([fragment[0] for fragment in data])

                        t = frombuffer(buffer, dtype=time_ch.dtype_fmt)

                else:
                    dtype_, byte_size, byte_offset, bti_count = group.record[time_ch_nr]

                    if one_piece:
                        data_bytes = data[0]

                        buffer = get_channel_raw_bytes(
                            data_bytes,
                            record_size,
                            byte_offset,
                            byte_size,
                        )

                        t = frombuffer(buffer, dtype=dtype_)

                    else:

                        # get data
                        if fragment is None:
                            data = self._load_data(
                                group,
                                record_offset=record_offset,
                                record_count=record_count,
                            )
                        else:
                            data = (fragment,)

                        buffer = bytearray().join(
                            [
                                get_channel_raw_bytes(
                                    fragment[0],
                                    record_size,
                                    byte_offset,
                                    byte_size,
                                )
                                for fragment in data
                            ]
                        )

                        t = frombuffer(buffer, dtype=dtype_)

                # get timestamps
                if time_conv:
                    t = time_conv.convert(t)

        self._master_channel_metadata[index] = metadata

        if not t.dtype == float64:
            t = t.astype(float64)

        if raster and t.size:
            timestamps = t
            if len(t) > 1:
                num = float(float32((timestamps[-1] - timestamps[0]) / raster))
                if int(num) == num:
                    timestamps = linspace(t[0], t[-1], int(num))
                else:
                    timestamps = arange(t[0], t[-1], raster)
        else:
            timestamps = t
        return timestamps

    def get_bus_signal(
        self,
        bus: BusType,
        name: str,
        database: CanMatrix | StrPathType | None = None,
        ignore_invalidation_bits: bool = False,
        data: bytes | None = None,
        raw: bool = False,
        ignore_value2text_conversion: bool = True,
    ) -> Signal:
        """get a signal decoded from a raw bus logging. The currently supported buses are
        CAN and LIN (LDF databases are not supported, they need to be converted to DBC and
        feed to this function)

        .. versionadded:: 6.0.0


        Parameters
        ----------
        bus : str
            "CAN" or "LIN"
        name : str
            signal name
        database : str
            path of external CAN/LIN database file (.dbc or .arxml) or canmatrix.CanMatrix; default *None*

            .. versionchanged:: 6.0.0
                `db` and `database` arguments were merged into this single argument

        ignore_invalidation_bits : bool
            option to ignore invalidation bits
        raw : bool
            return channel samples without applying the conversion rule; default
            `False`
        ignore_value2text_conversion : bool
            return channel samples without values that have a description in .dbc or .arxml file
            `True`

        Returns
        -------
        sig : Signal
            Signal object with the physical values

        """

        if bus == "CAN":
            return self.get_can_signal(
                name,
                database=database,
                ignore_invalidation_bits=ignore_invalidation_bits,
                data=data,
                raw=raw,
                ignore_value2text_conversion=ignore_value2text_conversion,
            )
        elif bus == "LIN":
            return self.get_lin_signal(
                name,
                database=database,
                ignore_invalidation_bits=ignore_invalidation_bits,
                data=data,
                raw=raw,
                ignore_value2text_conversion=ignore_value2text_conversion,
            )

    def get_can_signal(
        self,
        name: str,
        database: CanMatrix | StrPathType | None = None,
        ignore_invalidation_bits: bool = False,
        data: bytes | None = None,
        raw: bool = False,
        ignore_value2text_conversion: bool = True,
    ) -> Signal:
        """get CAN message signal. You can specify an external CAN database (
        *database* argument) or canmatrix database object that has already been
        loaded from a file (*db* argument).

        The signal name can be specified in the following ways

        * ``CAN<ID>.<MESSAGE_NAME>.<SIGNAL_NAME>`` - the `ID` value starts from 1
          and must match the ID found in the measurement (the source CAN bus ID)
          Example: CAN1.Wheels.FL_WheelSpeed

        * ``CAN<ID>.CAN_DataFrame_<MESSAGE_ID>.<SIGNAL_NAME>`` - the `ID` value
          starts from 1 and the `MESSAGE_ID` is the decimal message ID as found
          in the database. Example: CAN1.CAN_DataFrame_218.FL_WheelSpeed

        * ``<MESSAGE_NAME>.<SIGNAL_NAME>`` - in this case the first occurrence of
          the message name and signal are returned (the same message could be
          found on multiple CAN buses; for example on CAN1 and CAN3)
          Example: Wheels.FL_WheelSpeed

        * ``CAN_DataFrame_<MESSAGE_ID>.<SIGNAL_NAME>`` - in this case the first
          occurrence of the message name and signal are returned (the same
          message could be found on multiple CAN buses; for example on CAN1 and
          CAN3). Example: CAN_DataFrame_218.FL_WheelSpeed

        * ``<SIGNAL_NAME>`` - in this case the first occurrence of the signal
          name is returned (the same signal name could be found in multiple
          messages and on multiple CAN buses). Example: FL_WheelSpeed


        Parameters
        ----------
        name : str
            signal name
        database : str
            path of external CAN database file (.dbc or .arxml) or canmatrix.CanMatrix; default *None*

            .. versionchanged:: 6.0.0
                `db` and `database` arguments were merged into this single argument

        ignore_invalidation_bits : bool
            option to ignore invalidation bits
        raw : bool
            return channel samples without applying the conversion rule; default
            `False`
        ignore_value2text_conversion : bool
            return channel samples without values that have a description in .dbc or .arxml file
            `True`

        Returns
        -------
        sig : Signal
            Signal object with the physical values

        """

        if database is None:
            return self.get(name)

        if isinstance(database, (str, Path)):
            database_path = Path(database)
            if database_path.suffix.lower() not in (".arxml", ".dbc"):
                message = f'Expected .dbc or .arxml file as CAN channel attachment but got "{database_path}"'
                logger.exception(message)
                raise MdfException(message)
            else:
                db_string = database_path.read_bytes()
                md5_sum = md5(db_string).digest()

                if md5_sum in self._external_dbc_cache:
                    db = self._external_dbc_cache[md5_sum]
                else:
                    db = load_can_database(database_path, contents=db_string)
                    if db is None:
                        raise MdfException("failed to load database")
        else:
            db = database

        is_j1939 = db.contains_j1939

        name_ = name.split(".")

        if len(name_) == 3:
            can_id_str, message_id_str, signal = name_

            can_id = v4c.CAN_ID_PATTERN.search(can_id_str)
            if can_id is None:
                raise MdfException(
                    f'CAN id "{can_id_str}" of signal name "{name}" is not recognised by this library'
                )
            else:
                can_id = int(can_id.group("id"))

            message_id = v4c.CAN_DATA_FRAME_PATTERN.search(message_id_str)
            if message_id is None:
                message_id = message_id_str
            else:
                message_id = int(message_id)

            if isinstance(message_id, str):
                message = db.frame_by_name(message_id)
            else:
                message = db.frame_by_id(message_id)

        elif len(name_) == 2:
            message_id_str, signal = name_

            can_id = None

            message_id = v4c.CAN_DATA_FRAME_PATTERN.search(message_id_str)
            if message_id is None:
                message_id = message_id_str
            else:
                message_id = int(message_id.group("id"))

            if isinstance(message_id, str):
                message = db.frame_by_name(message_id)
            else:
                message = db.frame_by_id(message_id)

        else:
            message = None
            for msg in db:
                for signal in msg:
                    if signal.name == name:
                        message = msg

            can_id = None
            signal = name

        if message is None:
            raise MdfException(f"Could not find signal {name} in {database}")

        for sig in message.signals:
            if sig.name == signal:
                signal = sig
                break
        else:
            raise MdfException(
                f'Signal "{signal}" not found in message "{message.name}" of "{database}"'
            )

        if can_id is None:
            index = None
            for _can_id, messages in self.bus_logging_map["CAN"].items():

                if is_j1939:
                    test_ids = [
                        canmatrix.ArbitrationId(id_, extended=True).pgn
                        for id_ in self.bus_logging_map["CAN"][_can_id]
                    ]

                    id_ = message.arbitration_id.pgn

                else:
                    id_ = message.arbitration_id.id
                    test_ids = self.bus_logging_map["CAN"][_can_id]

                if id_ in test_ids:
                    if is_j1939:
                        for id__, idx in self.bus_logging_map["CAN"][_can_id].items():
                            if canmatrix.ArbitrationId(id__, extended=True).pgn == id_:
                                index = idx
                                break
                    else:
                        index = self.bus_logging_map["CAN"][_can_id][
                            message.arbitration_id.id
                        ]

                if index is not None:
                    break
            else:
                raise MdfException(
                    f'Message "{message.name}" (ID={hex(message.arbitration_id.id)}) not found in the measurement'
                )
        else:
            if can_id in self.bus_logging_map["CAN"]:
                if is_j1939:
                    test_ids = [
                        canmatrix.ArbitrationId(id_, extended=True).pgn
                        for id_ in self.bus_logging_map["CAN"][can_id]
                    ]
                    id_ = message.arbitration_id.pgn

                else:
                    id_ = message.arbitration_id.id
                    test_ids = self.bus_logging_map["CAN"][can_id]

                if id_ in test_ids:
                    if is_j1939:
                        for id__, idx in self.bus_logging_map["CAN"][can_id].items():
                            if canmatrix.ArbitrationId(id__, extended=True).pgn == id_:
                                index = idx
                                break
                    else:
                        index = self.bus_logging_map["CAN"][can_id][
                            message.arbitration_id.id
                        ]
                else:
                    raise MdfException(
                        f'Message "{message.name}" (ID={hex(message.arbitration_id.id)}) not found in the measurement'
                    )
            else:
                raise MdfException(
                    f'No logging from "{can_id}" was found in the measurement'
                )

        can_ids = self.get(
            "CAN_DataFrame.ID",
            group=index,
            ignore_invalidation_bits=ignore_invalidation_bits,
            data=data,
        )
        can_ids.samples = can_ids.samples.astype("<u4") & 0x1FFFFFFF

        payload = self.get(
            "CAN_DataFrame.DataBytes",
            group=index,
            samples_only=True,
            ignore_invalidation_bits=ignore_invalidation_bits,
            data=data,
        )[0]

        if is_j1939:
            tmp_pgn = can_ids.samples >> 8
            ps = tmp_pgn & 0xFF
            pf = (can_ids.samples >> 16) & 0xFF
            _pgn = tmp_pgn & 0x3FF00
            can_ids.samples = where(pf >= 240, _pgn + ps, _pgn)

            idx = argwhere(can_ids.samples == message.arbitration_id.pgn).ravel()
        else:
            idx = argwhere(can_ids.samples == message.arbitration_id.id).ravel()

        payload = payload[idx]
        t = can_ids.timestamps[idx].copy()

        if can_ids.invalidation_bits is not None:
            invalidation_bits = can_ids.invalidation_bits[idx]
        else:
            invalidation_bits = None

        if not ignore_invalidation_bits and invalidation_bits is not None:
            payload = payload[nonzero(~invalidation_bits)[0]]
            t = t[nonzero(~invalidation_bits)[0]]

        extracted_signals = extract_mux(
            payload,
            message,
            None,
            None,
            t,
            original_message_id=None,
            ignore_value2text_conversion=ignore_value2text_conversion,
            raw=raw,
        )

        comment = signal.comment or ""

        for entry, signals in extracted_signals.items():
            for name_, sig in signals.items():
                if name_ == signal.name:
                    sig = Signal(
                        samples=sig["samples"],
                        timestamps=sig["t"],
                        name=name,
                        unit=signal.unit or "",
                        comment=comment,
                    )
                    if len(sig):
                        return sig
                    else:
                        raise MdfException(
                            f'No logging from "{signal}" was found in the measurement'
                        )

        raise MdfException(f'No logging from "{signal}" was found in the measurement')

    def get_lin_signal(
        self,
        name: str,
        database: CanMatrix | StrPathType | None = None,
        ignore_invalidation_bits: bool = False,
        data: bytes | None = None,
        raw: bool = False,
        ignore_value2text_conversion: bool = True,
    ) -> Signal:
        """get LIN message signal. You can specify an external LIN database (
        *database* argument) or canmatrix database object that has already been
        loaded from a file (*db* argument).

        The signal name can be specified in the following ways

        * ``LIN_Frame_<MESSAGE_ID>.<SIGNAL_NAME>`` - Example: LIN_Frame_218.FL_WheelSpeed

        * ``<MESSAGE_NAME>.<SIGNAL_NAME>`` - Example: Wheels.FL_WheelSpeed

        * ``<SIGNAL_NAME>`` - Example: FL_WheelSpeed

        .. versionadded:: 6.0.0


        Parameters
        ----------
        name : str
            signal name
        database : str
            path of external LIN database file (.dbc, .arxml or .ldf) or canmatrix.CanMatrix;
            default *None*

        ignore_invalidation_bits : bool
            option to ignore invalidation bits
        raw : bool
            return channel samples without applying the conversion rule; default
            `False`
        ignore_value2text_conversion : bool
            return channel samples without values that have a description in .dbc, .arxml or .ldf file
            `True`

        Returns
        -------
        sig : Signal
            Signal object with the physical values

        """

        if database is None:
            return self.get(name)

        if isinstance(database, (str, Path)):
            database_path = Path(database)
            if database_path.suffix.lower() not in (".arxml", ".dbc", ".ldf"):
                message = f'Expected .dbc, .arxml or .ldf file as LIN channel attachment but got "{database_path}"'
                logger.exception(message)
                raise MdfException(message)
            else:
                db_string = database_path.read_bytes()
                md5_sum = md5(db_string).digest()

                if md5_sum in self._external_dbc_cache:
                    db = self._external_dbc_cache[md5_sum]
                else:
                    contents = (
                        None if database_path.suffix.lower() == ".ldf" else db_string
                    )
                    db = load_can_database(database_path, contents=contents)
                    if db is None:
                        raise MdfException("failed to load database")
        else:
            db = database

        name_ = name.split(".")

        if len(name_) == 2:
            message_id_str, signal = name_

            message_id = v4c.LIN_DATA_FRAME_PATTERN.search(message_id_str)
            if message_id is None:
                message_id = message_id_str
            else:
                message_id = int(message_id.group("id"))

            if isinstance(message_id, str):
                message = db.frame_by_name(message_id)
            else:
                message = db.frame_by_id(message_id)

        else:
            message = None
            for msg in db:
                for signal in msg:
                    if signal.name == name:
                        message = msg

            signal = name

        if message is None:
            raise MdfException(f"Could not find signal {name} in {database}")

        for sig in message.signals:
            if sig.name == signal:
                signal = sig
                break
        else:
            raise MdfException(
                f'Signal "{signal}" not found in message "{message.name}" of "{database}"'
            )

        id_ = message.arbitration_id.id

        if id_ in self.bus_logging_map["LIN"]:
            index = self.bus_logging_map["LIN"][id_]
        else:
            raise MdfException(
                f'Message "{message.name}" (ID={hex(message.arbitration_id.id)}) not found in the measurement'
            )

        can_ids = self.get(
            "LIN_Frame.ID",
            group=index,
            ignore_invalidation_bits=ignore_invalidation_bits,
            data=data,
        )
        can_ids.samples = can_ids.samples.astype("<u4") & 0x1FFFFFFF
        payload = self.get(
            "LIN_Frame.DataBytes",
            group=index,
            samples_only=True,
            ignore_invalidation_bits=ignore_invalidation_bits,
            data=data,
        )[0]

        idx = argwhere(can_ids.samples == message.arbitration_id.id).ravel()

        payload = payload[idx]
        t = can_ids.timestamps[idx].copy()

        if can_ids.invalidation_bits is not None:
            invalidation_bits = can_ids.invalidation_bits[idx]
        else:
            invalidation_bits = None

        if not ignore_invalidation_bits and invalidation_bits is not None:
            payload = payload[nonzero(~invalidation_bits)[0]]
            t = t[nonzero(~invalidation_bits)[0]]

        extracted_signals = extract_mux(
            payload,
            message,
            None,
            None,
            t,
            original_message_id=None,
            ignore_value2text_conversion=ignore_value2text_conversion,
            raw=raw,
        )

        comment = signal.comment or ""

        for entry, signals in extracted_signals.items():
            for name_, sig in signals.items():
                if name_ == signal.name:
                    sig = Signal(
                        samples=sig["samples"],
                        timestamps=sig["t"],
                        name=name,
                        unit=signal.unit or "",
                        comment=comment,
                    )
                    if len(sig):
                        return sig
                    else:
                        raise MdfException(
                            f'No logging from "{signal}" was found in the measurement'
                        )

        raise MdfException(f'No logging from "{signal}" was found in the measurement')

    def info(self) -> dict[str, Any]:
        """get MDF information as a dict

        Examples
        --------
        >>> mdf = MDF4('test.mdf')
        >>> mdf.info()


        """
        info = {
            "version": self.version,
            "program": self.identification.program_identification.decode("utf-8").strip(
                " \0\n\r\t"
            ),
            "comment": self.header.comment,
        }
        info["groups"] = len(self.groups)
        for i, gp in enumerate(self.groups):
            inf = {}
            info[f"group {i}"] = inf
            inf["cycles"] = gp.channel_group.cycles_nr
            inf["comment"] = gp.channel_group.comment
            inf["channels count"] = len(gp.channels)
            for j, channel in enumerate(gp.channels):
                name = channel.name

                ch_type = v4c.CHANNEL_TYPE_TO_DESCRIPTION[channel.channel_type]
                inf[f"channel {j}"] = f'name="{name}" type={ch_type}'

        return info

    def save(
        self,
        dst: WritableBufferType | StrPathType,
        overwrite: bool = False,
        compression: CompressionType = 0,
    ) -> Path:
        """Save MDF to *dst*. If overwrite is *True* then the destination file
        is overwritten, otherwise the file name is appended with '.<cntr>', were
        '<cntr>' is the first counter that produces a new file name
        (that does not already exist in the filesystem)

        Parameters
        ----------
        dst : str
            destination file name, Default ''
        overwrite : bool
            overwrite flag, default *False*
        compression : int
            use compressed data blocks, default 0; valid since version 4.10

            * 0 - no compression
            * 1 - deflate (slower, but produces smaller files)
            * 2 - transposition + deflate (slowest, but produces
              the smallest files)

        Returns
        -------
        output_file : pathlib.Path
            path to saved file

        """

        if is_file_like(dst):
            dst_ = dst
            file_like = True
            if hasattr(dst, "name"):
                dst = Path(dst.name)
            else:
                dst = Path("__file_like.mf4")
            dst_.seek(0)
            suffix = ".mf4"
        else:
            file_like = False
            suffix = Path(dst).suffix.lower()

            dst = Path(dst).with_suffix(".mf4")

            destination_dir = dst.parent
            destination_dir.mkdir(parents=True, exist_ok=True)

            if overwrite is False:
                if dst.is_file():
                    cntr = 0
                    while True:
                        name = dst.with_suffix(f".{cntr}.mf4")
                        if not name.exists():
                            break
                        else:
                            cntr += 1
                    message = (
                        f'Destination file "{dst}" already exists '
                        f'and "overwrite" is False. Saving MDF file as "{name}"'
                    )
                    logger.warning(message)
                    dst = name

            if dst == self.name:
                destination = dst.with_suffix(".savetemp")
            else:
                destination = dst

            dst_ = open(destination, "wb+")

        if not self.file_history:
            comment = "created"
        else:
            comment = "updated"

        fh = FileHistory()
        fh.comment = f"""<FHcomment>
<TX>{comment}</TX>
<tool_id>asammdf</tool_id>
<tool_vendor>asammdf</tool_vendor>
<tool_version>{__version__}</tool_version>
</FHcomment>"""

        self.file_history.append(fh)

        cg_map = {}

        try:
            defined_texts = {"": 0, b"": 0}
            cc_map = {}
            si_map = {}

            groups_nr = len(self.groups)

            write = dst_.write
            tell = dst_.tell
            seek = dst_.seek

            blocks = []

            write(bytes(self.identification))

            self.header.to_blocks(dst_.tell(), blocks)
            for block in blocks:
                write(bytes(block))

            original_data_addresses = []

            if compression == 1:
                zip_type = v4c.FLAG_DZ_DEFLATE
            else:
                zip_type = v4c.FLAG_DZ_TRANPOSED_DEFLATE

            # write DataBlocks first
            for gp_nr, gp in enumerate(self.groups):
                original_data_addresses.append(gp.data_group.data_block_addr)

                if gp.channel_group.flags & v4c.FLAG_CG_VLSD:
                    continue

                address = tell()

                total_size = (
                    gp.channel_group.samples_byte_nr
                    + gp.channel_group.invalidation_bytes_nr
                ) * gp.channel_group.cycles_nr

                if total_size:

                    if self._write_fragment_size:

                        samples_size = (
                            gp.channel_group.samples_byte_nr
                            + gp.channel_group.invalidation_bytes_nr
                        )
                        if samples_size:
                            split_size = self._write_fragment_size // samples_size
                            split_size *= samples_size
                            if split_size == 0:
                                split_size = samples_size
                            chunks = float(total_size) / split_size
                            chunks = int(ceil(chunks))

                            self._read_fragment_size = split_size
                        else:
                            chunks = 1
                    else:
                        chunks = 1

                    data = self._load_data(gp)

                    if chunks == 1:
                        data_, _1, _2, inval_ = next(data)
                        if self.version >= "4.20" and gp.uses_ld:
                            if compression:
                                if gp.channel_group.samples_byte_nr > 1:
                                    current_zip_type = zip_type
                                    if compression == 1:
                                        param = 0
                                    else:
                                        param = gp.channel_group.samples_byte_nr
                                else:
                                    current_zip_type = v4c.FLAG_DZ_DEFLATE
                                    param = 0

                                kwargs = {
                                    "data": data_,
                                    "zip_type": current_zip_type,
                                    "param": param,
                                    "original_type": b"DV",
                                }
                                data_block = DataZippedBlock(**kwargs)
                            else:
                                data_block = DataBlock(data=data_, type="DV")
                            write(bytes(data_block))
                            data_address = address

                            align = data_block.block_len % 8
                            if align:
                                write(b"\0" * (8 - align))

                            if inval_ is not None:
                                inval_address = address = tell()
                                if compression:
                                    if compression == 1:
                                        param = 0
                                    else:
                                        param = gp.channel_group.invalidation_bytes_nr
                                    kwargs = {
                                        "data": inval_,
                                        "zip_type": zip_type,
                                        "param": param,
                                        "original_type": b"DI",
                                    }
                                    inval_block = DataZippedBlock(**kwargs)
                                else:
                                    inval_block = DataBlock(data=inval_, type="DI")
                                write(bytes(inval_block))

                                align = inval_block.block_len % 8
                                if align:
                                    write(b"\0" * (8 - align))

                            address = tell()

                            kwargs = {
                                "flags": v4c.FLAG_LD_EQUAL_LENGHT,
                                "data_block_nr": 1,
                                "data_block_len": gp.channel_group.cycles_nr,
                                "data_block_addr_0": data_address,
                            }
                            if inval_:
                                kwargs["flags"] |= v4c.FLAG_LD_INVALIDATION_PRESENT
                                kwargs["invalidation_bits_addr_0"] = inval_address
                            ld_block = ListData(**kwargs)
                            write(bytes(ld_block))

                            align = ld_block.block_len % 8
                            if align:
                                write(b"\0" * (8 - align))

                            if gp.channel_group.cycles_nr:
                                gp.data_group.data_block_addr = address
                            else:
                                gp.data_group.data_block_addr = 0

                        else:
                            if compression and self.version >= "4.10":
                                if compression == 1:
                                    param = 0
                                else:
                                    param = (
                                        gp.channel_group.samples_byte_nr
                                        + gp.channel_group.invalidation_bytes_nr
                                    )
                                kwargs = {
                                    "data": data_,
                                    "zip_type": zip_type,
                                    "param": param,
                                }
                                data_block = DataZippedBlock(**kwargs)
                            else:
                                data_block = DataBlock(data=data_)
                            write(bytes(data_block))

                            align = data_block.block_len % 8
                            if align:
                                write(b"\0" * (8 - align))

                            if gp.channel_group.cycles_nr:
                                gp.data_group.data_block_addr = address
                            else:
                                gp.data_group.data_block_addr = 0
                    else:
                        if self.version >= "4.20" and gp.uses_ld:
                            dv_addr = []
                            di_addr = []
                            block_size = 0
                            for i, (data_, _1, _2, inval_) in enumerate(data):
                                if i == 0:
                                    block_size = len(data_)
                                if compression:
                                    if compression == 1:
                                        param = 0
                                    else:
                                        param = gp.channel_group.samples_byte_nr
                                    kwargs = {
                                        "data": data_,
                                        "zip_type": zip_type,
                                        "param": param,
                                        "original_type": b"DV",
                                    }
                                    data_block = DataZippedBlock(**kwargs)
                                else:
                                    data_block = DataBlock(data=data_, type="DV")
                                dv_addr.append(tell())
                                write(bytes(data_block))

                                align = data_block.block_len % 8
                                if align:
                                    write(b"\0" * (8 - align))

                                if inval_ is not None:

                                    if compression:
                                        if compression == 1:
                                            param = 0
                                        else:
                                            param = (
                                                gp.channel_group.invalidation_bytes_nr
                                            )
                                        kwargs = {
                                            "data": inval_,
                                            "zip_type": zip_type,
                                            "param": param,
                                            "original_type": b"DI",
                                        }
                                        inval_block = DataZippedBlock(**kwargs)
                                    else:
                                        inval_block = DataBlock(data=inval_, type="DI")
                                    di_addr.append(tell())
                                    write(bytes(inval_block))

                                    align = inval_block.block_len % 8
                                    if align:
                                        write(b"\0" * (8 - align))

                            address = tell()

                            kwargs = {
                                "flags": v4c.FLAG_LD_EQUAL_LENGHT,
                                "data_block_nr": len(dv_addr),
                                "data_block_len": block_size
                                // gp.channel_group.samples_byte_nr,
                            }
                            for i, addr in enumerate(dv_addr):
                                kwargs[f"data_block_addr_{i}"] = addr

                            if di_addr:
                                kwargs["flags"] |= v4c.FLAG_LD_INVALIDATION_PRESENT
                                for i, addr in enumerate(di_addr):
                                    kwargs[f"invalidation_bits_addr_{i}"] = addr

                            ld_block = ListData(**kwargs)
                            write(bytes(ld_block))

                            align = ld_block.block_len % 8
                            if align:
                                write(b"\0" * (8 - align))

                            if gp.channel_group.cycles_nr:
                                gp.data_group.data_block_addr = address
                            else:
                                gp.data_group.data_block_addr = 0

                        else:
                            kwargs = {
                                "flags": v4c.FLAG_DL_EQUAL_LENGHT,
                                "zip_type": zip_type,
                            }
                            hl_block = HeaderList(**kwargs)

                            kwargs = {
                                "flags": v4c.FLAG_DL_EQUAL_LENGHT,
                                "links_nr": chunks + 1,
                                "data_block_nr": chunks,
                                "data_block_len": split_size,
                            }
                            dl_block = DataList(**kwargs)

                            for i, data__ in enumerate(data):
                                data_ = data__[0]

                                if compression and self.version >= "4.10":
                                    if compression == 1:
                                        zip_type = v4c.FLAG_DZ_DEFLATE
                                    else:
                                        zip_type = v4c.FLAG_DZ_TRANPOSED_DEFLATE
                                    if compression == 1:
                                        param = 0
                                    else:
                                        param = (
                                            gp.channel_group.samples_byte_nr
                                            + gp.channel_group.invalidation_bytes_nr
                                        )
                                    kwargs = {
                                        "data": data_,
                                        "zip_type": zip_type,
                                        "param": param,
                                    }
                                    block = DataZippedBlock(**kwargs)
                                else:
                                    block = DataBlock(data=data_)
                                address = tell()
                                block.address = address

                                write(bytes(block))

                                align = block.block_len % 8
                                if align:
                                    write(b"\0" * (8 - align))
                                dl_block[f"data_block_addr{i}"] = address

                            address = tell()
                            dl_block.address = address
                            write(bytes(dl_block))

                            if compression and self.version != "4.00":
                                hl_block.first_dl_addr = address
                                address = tell()
                                hl_block.address = address
                                write(bytes(hl_block))

                            gp.data_group.data_block_addr = address
                else:
                    gp.data_group.data_block_addr = 0

                if self._callback:
                    self._callback(int(50 * (gp_nr + 1) / groups_nr), 100)
                if self._terminate:
                    dst_.close()
                    self.close()
                    return

            address = tell()

            blocks = []

            # file history blocks
            for fh in self.file_history:
                address = fh.to_blocks(address, blocks, defined_texts)

            for i, fh in enumerate(self.file_history[:-1]):
                fh.next_fh_addr = self.file_history[i + 1].address
            self.file_history[-1].next_fh_addr = 0

            # data groups
            gp_rec_ids = []
            valid_data_groups = []
            for gp in self.groups:
                if gp.channel_group.flags & v4c.FLAG_CG_VLSD:
                    continue

                valid_data_groups.append(gp.data_group)
                gp_rec_ids.append(gp.data_group.record_id_len)

                address = gp.data_group.to_blocks(address, blocks, defined_texts)

            if valid_data_groups:
                for i, dg in enumerate(valid_data_groups[:-1]):
                    addr_ = valid_data_groups[i + 1].address
                    dg.next_dg_addr = addr_
                valid_data_groups[-1].next_dg_addr = 0

            # go through each data group and append the rest of the blocks
            for i, gp in enumerate(self.groups):

                channels = gp.channels

                for j, channel in enumerate(channels):
                    if channel.attachment is not None:
                        channel.attachment_addr = self.attachments[
                            channel.attachment
                        ].address
                    elif channel.attachment_nr:
                        channel.attachment_addr = 0

                    address = channel.to_blocks(
                        address, blocks, defined_texts, cc_map, si_map
                    )

                    if channel.channel_type == v4c.CHANNEL_TYPE_SYNC:
                        if channel.attachment is not None:
                            channel.data_block_addr = self.attachments[
                                channel.attachment
                            ].address
                    else:

                        sdata = self._load_signal_data(group=gp, index=j)
                        if sdata:
                            split_size = self._write_fragment_size
                            if self._write_fragment_size:
                                chunks = float(len(sdata)) / split_size
                                chunks = int(ceil(chunks))
                            else:
                                chunks = 1

                            if chunks == 1:
                                if compression and self.version > "4.00":
                                    signal_data = DataZippedBlock(
                                        data=sdata,
                                        zip_type=v4c.FLAG_DZ_DEFLATE,
                                        original_type=b"SD",
                                    )
                                    signal_data.address = address
                                    address += signal_data.block_len
                                    blocks.append(signal_data)
                                    align = signal_data.block_len % 8
                                    if align:
                                        blocks.append(b"\0" * (8 - align))
                                        address += 8 - align
                                else:
                                    signal_data = DataBlock(data=sdata, type="SD")
                                    signal_data.address = address
                                    address += signal_data.block_len
                                    blocks.append(signal_data)
                                    align = signal_data.block_len % 8
                                    if align:
                                        blocks.append(b"\0" * (8 - align))
                                        address += 8 - align

                                channel.data_block_addr = signal_data.address
                            else:
                                kwargs = {
                                    "flags": v4c.FLAG_DL_EQUAL_LENGHT,
                                    "links_nr": chunks + 1,
                                    "data_block_nr": chunks,
                                    "data_block_len": self._write_fragment_size,
                                }
                                dl_block = DataList(**kwargs)

                                for k in range(chunks):

                                    data_ = sdata[k * split_size : (k + 1) * split_size]
                                    if compression and self.version > "4.00":
                                        zip_type = v4c.FLAG_DZ_DEFLATE
                                        param = 0

                                        kwargs = {
                                            "data": data_,
                                            "zip_type": zip_type,
                                            "param": param,
                                            "original_type": b"SD",
                                        }
                                        block = DataZippedBlock(**kwargs)
                                    else:
                                        block = DataBlock(data=data_, type="SD")
                                    blocks.append(block)
                                    block.address = address
                                    address += block.block_len

                                    align = block.block_len % 8
                                    if align:
                                        blocks.append(b"\0" * (8 - align))
                                        address += 8 - align
                                    dl_block[f"data_block_addr{k}"] = block.address

                                dl_block.address = address
                                blocks.append(dl_block)

                                address += dl_block.block_len

                                if compression and self.version > "4.00":
                                    kwargs = {
                                        "flags": v4c.FLAG_DL_EQUAL_LENGHT,
                                        "zip_type": v4c.FLAG_DZ_DEFLATE,
                                        "first_dl_addr": dl_block.address,
                                    }
                                    hl_block = HeaderList(**kwargs)
                                    hl_block.address = address
                                    address += hl_block.block_len

                                    blocks.append(hl_block)

                                    channel.data_block_addr = hl_block.address
                                else:
                                    channel.data_block_addr = dl_block.address

                        else:
                            channel.data_block_addr = 0

                    dep_list = gp.channel_dependencies[j]
                    if dep_list:
                        if all(isinstance(dep, ChannelArrayBlock) for dep in dep_list):
                            for dep in dep_list:
                                dep.address = address
                                address += dep.block_len
                                blocks.append(dep)
                            for k, dep in enumerate(dep_list[:-1]):
                                dep.composition_addr = dep_list[k + 1].address
                            dep_list[-1].composition_addr = 0

                            channel.component_addr = dep_list[0].address

                        else:
                            index = dep_list[0][1]
                            addr_ = gp.channels[index].address

                group_channels = gp.channels
                if group_channels:
                    for j, channel in enumerate(group_channels[:-1]):
                        channel.next_ch_addr = group_channels[j + 1].address
                    group_channels[-1].next_ch_addr = 0

                # channel dependecies
                j = len(channels) - 1
                while j >= 0:
                    dep_list = gp.channel_dependencies[j]
                    if dep_list and all(isinstance(dep, tuple) for dep in dep_list):
                        index = dep_list[0][1]
                        channels[j].component_addr = channels[index].address
                        index = dep_list[-1][1]
                        channels[j].next_ch_addr = channels[index].next_ch_addr
                        channels[index].next_ch_addr = 0

                        for _, ch_nr in dep_list:
                            channels[ch_nr].source_addr = 0
                    j -= 1

                # channel group
                if gp.channel_group.flags & v4c.FLAG_CG_VLSD:
                    continue

                gp.channel_group.first_sample_reduction_addr = 0

                if channels:
                    gp.channel_group.first_ch_addr = gp.channels[0].address
                else:
                    gp.channel_group.first_ch_addr = 0
                gp.channel_group.next_cg_addr = 0

                address = gp.channel_group.to_blocks(
                    address, blocks, defined_texts, si_map
                )
                gp.data_group.first_cg_addr = gp.channel_group.address

                cg_map[i] = gp.channel_group.address

                if self._callback:
                    self._callback(int(50 * (i + 1) / groups_nr) + 25, 100)
                if self._terminate:
                    dst_.close()
                    self.close()
                    return

            for gp in self.groups:
                for dep_list in gp.channel_dependencies:
                    if dep_list:
                        if all(isinstance(dep, ChannelArrayBlock) for dep in dep_list):
                            for dep in dep_list:

                                for i, (gp_nr, ch_nr) in enumerate(
                                    dep.dynamic_size_channels
                                ):
                                    grp = self.groups[gp_nr]
                                    ch = grp.channels[ch_nr]
                                    dep[
                                        f"dynamic_size_{i}_dg_addr"
                                    ] = grp.data_group.address
                                    dep[
                                        f"dynamic_size_{i}_cg_addr"
                                    ] = grp.channel_group.address
                                    dep[f"dynamic_size_{i}_ch_addr"] = ch.address

                                for i, (gp_nr, ch_nr) in enumerate(
                                    dep.input_quantity_channels
                                ):
                                    grp = self.groups[gp_nr]
                                    ch = grp.channels[ch_nr]
                                    dep[
                                        f"input_quantity_{i}_dg_addr"
                                    ] = grp.data_group.address
                                    dep[
                                        f"input_quantity_{i}_cg_addr"
                                    ] = grp.channel_group.address
                                    dep[f"input_quantity_{i}_ch_addr"] = ch.address

                                for i, conversion in enumerate(dep.axis_conversions):
                                    if conversion:
                                        address = conversion.to_blocks(
                                            address, blocks, defined_texts, cc_map
                                        )
                                        dep[f"axis_conversion_{i}"] = conversion.address
                                    else:
                                        dep[f"axis_conversion_{i}"] = 0

                                if dep.output_quantity_channel:
                                    gp_nr, ch_nr = dep.output_quantity_channel
                                    grp = self.groups[gp_nr]
                                    ch = grp.channels[ch_nr]
                                    dep[
                                        f"output_quantity_dg_addr"
                                    ] = grp.data_group.address
                                    dep[
                                        f"output_quantity_cg_addr"
                                    ] = grp.channel_group.address
                                    dep[f"output_quantity_ch_addr"] = ch.address

                                if dep.comparison_quantity_channel:
                                    gp_nr, ch_nr = dep.comparison_quantity_channel
                                    grp = self.groups[gp_nr]
                                    ch = grp.channels[ch_nr]
                                    dep[
                                        f"comparison_quantity_dg_addr"
                                    ] = grp.data_group.address
                                    dep[
                                        f"comparison_quantity_cg_addr"
                                    ] = grp.channel_group.address
                                    dep[f"comparison_quantity_ch_addr"] = ch.address

                                for i, (gp_nr, ch_nr) in enumerate(dep.axis_channels):
                                    grp = self.groups[gp_nr]
                                    ch = grp.channels[ch_nr]
                                    dep[
                                        f"scale_axis_{i}_dg_addr"
                                    ] = grp.data_group.address
                                    dep[
                                        f"scale_axis_{i}_cg_addr"
                                    ] = grp.channel_group.address
                                    dep[f"scale_axis_{i}_ch_addr"] = ch.address

            position = tell()

            for gp in self.groups:
                gp.data_group.record_id_len = 0

                cg_master_index = gp.channel_group.cg_master_index
                if cg_master_index is not None:
                    gp.channel_group.cg_master_addr = cg_map[cg_master_index]
                    seek(gp.channel_group.address)
                    write(bytes(gp.channel_group))

            seek(position)

            ev_map = []

            if self.events:
                for event in self.events:
                    for i, ref in enumerate(event.scopes):
                        try:
                            dg_cntr, ch_cntr = ref
                            event[f"scope_{i}_addr"] = (
                                self.groups[dg_cntr].channels[ch_cntr].address
                            )
                        except TypeError:
                            dg_cntr = ref
                            event[f"scope_{i}_addr"] = self.groups[
                                dg_cntr
                            ].channel_group.address

                    blocks.append(event)
                    ev_map.append(address)
                    event.address = address
                    address += event.block_len

                    if event.name:
                        tx_block = TextBlock(text=event.name)
                        tx_block.address = address
                        blocks.append(tx_block)
                        address += tx_block.block_len
                        event.name_addr = tx_block.address
                    else:
                        event.name_addr = 0

                    if event.comment:
                        meta = event.comment.startswith("<EVcomment")
                        tx_block = TextBlock(text=event.comment, meta=meta)
                        tx_block.address = address
                        blocks.append(tx_block)
                        address += tx_block.block_len
                        event.comment_addr = tx_block.address
                    else:
                        event.comment_addr = 0

                    if event.parent is not None:
                        event.parent_ev_addr = ev_map[event.parent]
                    if event.range_start is not None:
                        event.range_start_ev_addr = ev_map[event.range_start]

                for i in range(len(self.events) - 1):
                    self.events[i].next_ev_addr = self.events[i + 1].address
                self.events[-1].next_ev_addr = 0

                self.header.first_event_addr = self.events[0].address

            if self._terminate:
                dst_.close()
                self.close()
                return

            # attachments
            at_map = {}
            if self.attachments:

                # put the attachment texts before the attachments
                for at_block in self.attachments:
                    for text in (at_block.file_name, at_block.mime, at_block.comment):
                        if text not in defined_texts:
                            tx_block = TextBlock(text=str(text))
                            defined_texts[text] = address
                            tx_block.address = address
                            address += tx_block.block_len
                            blocks.append(tx_block)

                for at_block in self.attachments:
                    address = at_block.to_blocks(address, blocks, defined_texts)

                for i in range(len(self.attachments) - 1):
                    at_block = self.attachments[i]
                    at_block.next_at_addr = self.attachments[i + 1].address
                self.attachments[-1].next_at_addr = 0

                if self.events:
                    for event in self.events:
                        for i in range(event.attachment_nr):
                            key = f"attachment_{i}_addr"
                            addr = event[key]
                            event[key] = at_map[addr]

                for i, gp in enumerate(self.groups):

                    for j, channel in enumerate(gp.channels):
                        if channel.attachment is not None:
                            channel.attachment_addr = self.attachments[
                                channel.attachment
                            ].address
                        elif channel.attachment_nr:
                            channel.attachment_addr = 0

                        if (
                            channel.channel_type == v4c.CHANNEL_TYPE_SYNC
                            and channel.attachment is not None
                        ):
                            channel.data_block_addr = self.attachments[
                                channel.attachment
                            ].address

            if self._callback:
                blocks_nr = len(blocks)
                threshold = blocks_nr / 25
                count = 1
                for i, block in enumerate(blocks):
                    write(bytes(block))
                    if i >= threshold:
                        self._callback(75 + count, 100)
                        count += 1
                        threshold += blocks_nr / 25
            else:
                for block in blocks:
                    write(bytes(block))

            for gp, rec_id in zip(self.groups, gp_rec_ids):
                gp.data_group.record_id_len = rec_id

            if valid_data_groups:
                addr_ = valid_data_groups[0].address
                self.header.first_dg_addr = addr_
            else:
                self.header.first_dg_addr = 0
            self.header.file_history_addr = self.file_history[0].address
            if self.attachments:
                first_attachment = self.attachments[0]
                addr_ = first_attachment.address
                self.header.first_attachment_addr = addr_
            else:
                self.header.first_attachment_addr = 0

            seek(v4c.IDENTIFICATION_BLOCK_SIZE)
            write(bytes(self.header))

            for orig_addr, gp in zip(original_data_addresses, self.groups):
                gp.data_group.data_block_addr = orig_addr

            at_map = {value: key for key, value in at_map.items()}

            for event in self.events:
                for i in range(event.attachment_nr):
                    key = f"attachment_{i}_addr"
                    addr = event[key]
                    event[key] = at_map[addr]

        except:
            if not file_like:
                dst_.close()
            raise
        else:
            if not file_like:
                dst_.close()

        if suffix in (".zip", ".mf4z"):
            output_fname = dst.with_suffix(suffix)
            try:
                zipped_mf4 = ZipFile(output_fname, "w", compression=ZIP_DEFLATED)
                zipped_mf4.write(
                    str(dst),
                    dst.name,
                    compresslevel=1,
                )
                zipped_mf4.close()
                os.remove(destination)
                dst = output_fname
            except:
                pass

        if dst == self.name:
            self.close()
            try:
                Path.unlink(self.name)
                Path.rename(destination, self.name)
            except:
                pass

            self.groups.clear()
            self.header = None
            self.identification = None
            self.file_history.clear()
            self.channels_db.clear()
            self.masters_db.clear()
            self.attachments.clear()
            self.file_comment = None

            self._ch_map.clear()

            self._tempfile = TemporaryFile(dir=self.temporary_folder)
            self._file = open(self.name, "rb")
            self._read()

        if self._callback:
            self._callback(100, 100)

        if self.name == Path("__new__.mf4"):
            self.name = dst

        return dst

    def get_channel_name(self, group: int, index: int) -> str:
        """Gets channel name.

        Parameters
        ----------
        group : int
            0-based group index
        index : int
            0-based channel index

        Returns
        -------
        name : str
            found channel name

        """
        gp_nr, ch_nr = self._validate_channel_selection(None, group, index)

        return self.groups[gp_nr].channels[ch_nr].name

    def get_channel_metadata(
        self,
        name: str | None = None,
        group: int | None = None,
        index: int | None = None,
    ) -> Channel:
        gp_nr, ch_nr = self._validate_channel_selection(name, group, index)

        grp = self.groups[gp_nr]

        channel = grp.channels[ch_nr]

        return channel

    def get_channel_unit(
        self,
        name: str | None = None,
        group: int | None = None,
        index: int | None = None,
    ) -> str:
        """Gets channel unit.

        Channel can be specified in two ways:

        * using the first positional argument *name*

            * if there are multiple occurrences for this channel then the
              *group* and *index* arguments can be used to select a specific
              group.
            * if there are multiple occurrences for this channel and either the
              *group* or *index* arguments is None then a warning is issued

        * using the group number (keyword argument *group*) and the channel
          number (keyword argument *index*). Use *info* method for group and
          channel numbers


        If the *raster* keyword argument is not *None* the output is
        interpolated accordingly.

        Parameters
        ----------
        name : string
            name of channel
        group : int
            0-based group index
        index : int
            0-based channel index

        Returns
        -------
        unit : str
            found channel unit

        """
        gp_nr, ch_nr = self._validate_channel_selection(name, group, index)

        grp = self.groups[gp_nr]

        channel = grp.channels[ch_nr]

        conversion = channel.conversion

        unit = conversion and conversion.unit or channel.unit or ""

        return unit

    def get_channel_comment(
        self,
        name: str | None = None,
        group: int | None = None,
        index: int | None = None,
    ) -> str:
        """Gets channel comment.

        Channel can be specified in two ways:

        * using the first positional argument *name*

            * if there are multiple occurrences for this channel then the
              *group* and *index* arguments can be used to select a specific
              group.
            * if there are multiple occurrences for this channel and either the
              *group* or *index* arguments is None then a warning is issued

        * using the group number (keyword argument *group*) and the channel
          number (keyword argument *index*). Use *info* method for group and
          channel numbers


        If the *raster* keyword argument is not *None* the output is
        interpolated accordingly.

        Parameters
        ----------
        name : string
            name of channel
        group : int
            0-based group index
        index : int
            0-based channel index

        Returns
        -------
        comment : str
            found channel comment

        """
        gp_nr, ch_nr = self._validate_channel_selection(name, group, index)

        grp = self.groups[gp_nr]

        channel = grp.channels[ch_nr]

        return extract_cncomment_xml(channel.comment)

    def _finalize(self) -> None:
        """
        Attempt finalization of the file.
        :return:    None
        """

        flags = self.identification.unfinalized_standard_flags

        stream = self._file
        blocks, block_groups, addresses = all_blocks_addresses(stream)

        stream.seek(0, 2)
        limit = stream.tell()
        mapped = self._mapped

        if flags & v4c.FLAG_UNFIN_UPDATE_LAST_DL:
            for dg_addr in block_groups[b"##DG\x00\x00"]:
                group = DataGroup(address=dg_addr, stream=stream, mapped=mapped)
                data_addr = group.data_block_addr
                if not data_addr:
                    continue

                stream.seek(data_addr)
                blk_id = stream.read(4)
                if blk_id == b"##DT":
                    continue
                elif blk_id in (b"##DL", b"##HL"):

                    if blk_id == b"##HL":
                        hl = HeaderList(address=data_addr, stream=stream, mapped=mapped)
                        data_addr = hl.first_dl_addr

                    while True:
                        dl = DataList(address=data_addr, stream=stream, mapped=mapped)
                        if not dl.next_dl_addr:
                            break

                    kwargs = {}

                    count = dl.links_nr - 1
                    valid_count = 0
                    for i in range(count):
                        dt_addr = dl[f"data_block_addr{i}"]
                        if dt_addr:
                            valid_count += 1
                            kwargs[f"data_block_addr{i}"] = dt_addr
                        else:
                            break

                    starting_address = dl.address
                    next_block_position = bisect.bisect_right(
                        addresses, starting_address
                    )
                    # search for data blocks after the DLBLOCK
                    for j in range(i, count):

                        if next_block_position >= len(addresses):
                            break

                        next_block_address = addresses[next_block_position]
                        next_block_type = blocks[next_block_address]

                        if next_block_type not in {b"##DZ", b"##DT", b"##DV", b"##DI"}:
                            break
                        else:

                            stream.seek(next_block_address + v4c.DZ_INFO_COMMON_OFFSET)

                            if next_block_type == b"##DZ":
                                (
                                    zip_type,
                                    param,
                                    original_size,
                                    zip_size,
                                ) = v4c.DZ_COMMON_INFO_uf(
                                    stream.read(v4c.DZ_COMMON_INFO_SIZE)
                                )

                                exceeded = (
                                    limit
                                    - (
                                        next_block_address
                                        + v4c.DZ_COMMON_SIZE
                                        + zip_size
                                    )
                                    < 0
                                )

                            else:
                                id_string, block_len = COMMON_SHORT_uf(
                                    stream.read(v4c.COMMON_SIZE)
                                )
                                original_size = block_len - 24

                                exceeded = limit - (next_block_address + block_len) < 0

                            # update the data block size in case all links were NULL before
                            if i == 0 and (dl.flags & v4c.FLAG_DL_EQUAL_LENGHT):
                                kwargs["data_block_len"] = original_size

                            # check if the file limit is exceeded
                            if exceeded:
                                break
                            else:
                                next_block_position += 1
                                valid_count += 1
                                kwargs[f"data_block_addr{j}"] = next_block_address

                    kwargs["links_nr"] = valid_count + 1
                    kwargs["flags"] = dl.flags
                    if dl.flags & v4c.FLAG_DL_EQUAL_LENGHT:
                        kwargs["data_block_len"] = dl.data_block_len
                    else:
                        for i in enumerate(valid_count):
                            kwargs[f"offset_{i}"] = dl[f"offset_{i}"]

                    stream.seek(data_addr)
                    stream.write(bytes(DataList(**kwargs)))

            self.identification[
                "unfinalized_standard_flags"
            ] -= v4c.FLAG_UNFIN_UPDATE_LAST_DL

        if flags & v4c.FLAG_UNFIN_UPDATE_LAST_DT_LENGTH:
            try:
                for dg_addr in block_groups[b"##DG\x00\x00"]:
                    group = DataGroup(address=dg_addr, stream=stream, mapped=mapped)
                    data_addr = group.data_block_addr
                    if not data_addr:
                        continue

                    stream.seek(data_addr)
                    blk_id = stream.read(4)
                    if blk_id == b"##DT":
                        blk = DataBlock(address=data_addr, stream=stream, mapped=mapped)
                    elif blk_id == b"##DL":
                        while True:
                            dl = DataList(
                                address=data_addr, stream=stream, mapped=mapped
                            )
                            if not dl.next_dl_addr:
                                break

                        data_addr = dl[f"data_block_addr{dl.links_nr - 2}"]
                        blk = DataBlock(address=data_addr, stream=stream, mapped=mapped)

                    elif blk_id == b"##HL":

                        hl = HeaderList(address=data_addr, stream=stream, mapped=mapped)

                        data_addr = hl.first_dl_addr
                        while True:
                            dl = DataList(
                                address=data_addr, stream=stream, mapped=mapped
                            )
                            if not dl.next_dl_addr:
                                break

                        data_addr = dl[f"data_block_addr{dl.links_nr - 2}"]
                        blk = DataBlock(address=data_addr, stream=stream, mapped=mapped)

                    next_block = bisect.bisect_right(addresses, data_addr)
                    if next_block == len(addresses):
                        block_len = limit - data_addr
                    else:
                        block_len = addresses[next_block] - data_addr

                    blk.block_len = block_len
                    stream.seek(data_addr)
                    stream.write(bytes(blk))
            except:
                print(format_exc())
                raise

            self.identification.unfinalized_standard_flags -= (
                v4c.FLAG_UNFIN_UPDATE_LAST_DT_LENGTH
            )
        self.identification.file_identification = b"MDF     "

    def _sort(self) -> None:
        if self._file is None:
            return

        flags = self.identification["unfinalized_standard_flags"]

        common = defaultdict(list)
        for i, group in enumerate(self.groups):
            if group.sorted:
                continue

            try:
                data_block = next(group.get_data_blocks())
                common[data_block.address].append((i, group.channel_group.record_id))
            except:
                continue

        read = self._file.read
        seek = self._file.seek

        self._tempfile.seek(0, 2)

        tell = self._tempfile.tell
        write = self._tempfile.write

        for address, groups in common.items():

            cg_map = {
                rec_id: self.groups[index_].channel_group for index_, rec_id in groups
            }

            final_records = {id_: [] for (_, id_) in groups}

            for rec_id, channel_group in cg_map.items():
                if channel_group.address in self._cn_data_map:
                    dg_cntr, ch_cntr = self._cn_data_map[channel_group.address]
                    self.groups[dg_cntr].signal_data[ch_cntr] = ([], iter(EMPTY_TUPLE))

            group = self.groups[groups[0][0]]

            record_id_nr = group.data_group.record_id_len
            cg_size = group.record_size

            if record_id_nr == 1:
                _unpack_stuct = UINT8_uf
            elif record_id_nr == 2:
                _unpack_stuct = UINT16_uf
            elif record_id_nr == 4:
                _unpack_stuct = UINT32_uf
            elif record_id_nr == 8:
                _unpack_stuct = UINT64_uf
            else:
                message = f"invalid record id size {record_id_nr}"
                raise MdfException(message)

            rem = b""
            for info in group.get_data_blocks():
                dtblock_address, dtblock_raw_size, dtblock_size, block_type, param = (
                    info.address,
                    info.original_size,
                    info.compressed_size,
                    info.block_type,
                    info.param,
                )

                seek(dtblock_address)

                if block_type != v4c.DT_BLOCK:
                    partial_records = {id_: [] for _, id_ in groups}
                    new_data = read(dtblock_size)

                    if block_type == v4c.DZ_BLOCK_DEFLATE:
                        new_data = decompress(new_data, bufsize=dtblock_raw_size)
                    elif block_type == v4c.DZ_BLOCK_TRANSPOSED:
                        new_data = decompress(new_data, bufsize=dtblock_raw_size)
                        cols = param
                        lines = dtblock_raw_size // cols

                        nd = fromstring(new_data[: lines * cols], dtype=uint8)
                        nd = nd.reshape((cols, lines))
                        new_data = nd.T.ravel().tobytes() + new_data[lines * cols :]

                    new_data = rem + new_data

                    try:
                        rem = sort_data_block(
                            new_data,
                            partial_records,
                            cg_size,
                            record_id_nr,
                            _unpack_stuct,
                        )
                    except:
                        print(format_exc())
                        raise

                    for rec_id, new_data in partial_records.items():

                        channel_group = cg_map[rec_id]

                        if channel_group.address in self._cn_data_map:
                            dg_cntr, ch_cntr = self._cn_data_map[channel_group.address]
                        else:
                            dg_cntr, ch_cntr = None, None

                        if new_data:

                            tempfile_address = tell()

                            new_data = b"".join(new_data)
                            original_size = len(new_data)
                            if original_size:
                                new_data = lz_compress(new_data)
                                compressed_size = len(new_data)

                                write(new_data)

                                if dg_cntr is not None:

                                    info = SignalDataBlockInfo(
                                        address=tempfile_address,
                                        compressed_size=compressed_size,
                                        original_size=original_size,
                                        block_type=v4c.DZ_BLOCK_LZ,
                                        location=v4c.LOCATION_TEMPORARY_FILE,
                                    )
                                    self.groups[dg_cntr].signal_data[ch_cntr][0].append(
                                        info
                                    )

                                else:

                                    block_info = DataBlockInfo(
                                        address=tempfile_address,
                                        block_type=v4c.DZ_BLOCK_LZ,
                                        compressed_size=compressed_size,
                                        original_size=original_size,
                                        param=0,
                                    )
                                    final_records[rec_id].append(block_info)

                else:  # DTBLOCK

                    seek(dtblock_address)
                    limit = 32 * 1024 * 1024  # 32MB
                    while dtblock_size:

                        if dtblock_size > limit:
                            dtblock_size -= limit
                            new_data = rem + read(limit)
                        else:
                            new_data = rem + read(dtblock_size)
                            dtblock_size = 0
                        partial_records = {id_: [] for _, id_ in groups}

                        rem = sort_data_block(
                            new_data,
                            partial_records,
                            cg_size,
                            record_id_nr,
                            _unpack_stuct,
                        )

                        for rec_id, new_data in partial_records.items():

                            channel_group = cg_map[rec_id]

                            if channel_group.address in self._cn_data_map:
                                dg_cntr, ch_cntr = self._cn_data_map[
                                    channel_group.address
                                ]
                            else:
                                dg_cntr, ch_cntr = None, None

                            if new_data:

                                tempfile_address = tell()
                                new_data = b"".join(new_data)

                                original_size = len(new_data)
                                if original_size:
                                    new_data = lz_compress(new_data)
                                    compressed_size = len(new_data)

                                    write(new_data)

                                    if dg_cntr is not None:

                                        info = SignalDataBlockInfo(
                                            address=tempfile_address,
                                            compressed_size=compressed_size,
                                            original_size=original_size,
                                            block_type=v4c.DZ_BLOCK_LZ,
                                            location=v4c.LOCATION_TEMPORARY_FILE,
                                        )
                                        self.groups[dg_cntr].signal_data[ch_cntr][
                                            0
                                        ].append(info)

                                    else:
                                        block_info = DataBlockInfo(
                                            address=tempfile_address,
                                            block_type=v4c.DZ_BLOCK_LZ,
                                            compressed_size=compressed_size,
                                            original_size=original_size,
                                            param=None,
                                        )

                                        final_records[rec_id].append(block_info)

            # after we read all DTBLOCKs in the original file,
            # we assign freshly created blocks from temporary file to
            # corresponding groups.
            for idx, rec_id in groups:
                group = self.groups[idx]
                group.data_location = v4c.LOCATION_TEMPORARY_FILE
                group.set_blocks_info(final_records[rec_id])
                group.sorted = True

        for i, group in enumerate(self.groups):
            if flags & v4c.FLAG_UNFIN_UPDATE_CG_COUNTER:

                channel_group = group.channel_group

                if channel_group.flags & v4c.FLAG_CG_VLSD:
                    continue

                if (
                    self.version >= "4.20"
                    and channel_group.flags & v4c.FLAG_CG_REMOTE_MASTER
                ):
                    index = channel_group.cg_master_index
                else:
                    index = i

                if group.uses_ld:
                    samples_size = channel_group.samples_byte_nr
                else:
                    samples_size = (
                        channel_group.samples_byte_nr
                        + channel_group.invalidation_bytes_nr
                    )

                total_size = sum(blk.original_size for blk in group.get_data_blocks())

                cycles_nr = total_size // samples_size
                virtual_channel_group = self.virtual_groups[index]
                virtual_channel_group.cycles_nr = cycles_nr
                channel_group.cycles_nr = cycles_nr

        if (
            self.identification["unfinalized_standard_flags"]
            & v4c.FLAG_UNFIN_UPDATE_CG_COUNTER
        ):
            self.identification[
                "unfinalized_standard_flags"
            ] -= v4c.FLAG_UNFIN_UPDATE_CG_COUNTER
        if (
            self.identification["unfinalized_standard_flags"]
            & v4c.FLAG_UNFIN_UPDATE_VLSD_BYTES
        ):
            self.identification[
                "unfinalized_standard_flags"
            ] -= v4c.FLAG_UNFIN_UPDATE_VLSD_BYTES

    def _process_bus_logging(self) -> None:
        groups_count = len(self.groups)
        for index in range(groups_count):
            group = self.groups[index]
            if group.channel_group.flags & v4c.FLAG_CG_BUS_EVENT:
                source = group.channel_group.acq_source

                if (
                    source
                    and source.bus_type in (v4c.BUS_TYPE_CAN, v4c.BUS_TYPE_OTHER)
                    and "CAN_DataFrame" in [ch.name for ch in group.channels]
                ):
                    try:
                        self._process_can_logging(index, group)
                    except Exception as e:
                        message = f"Error during CAN logging processing: {format_exc()}"
                        logger.error(message)

                if (
                    source
                    and source.bus_type in (v4c.BUS_TYPE_LIN, v4c.BUS_TYPE_OTHER)
                    and "LIN_Frame" in [ch.name for ch in group.channels]
                ):
                    try:
                        self._process_lin_logging(index, group)
                    except Exception as e:
                        message = f"Error during LIN logging processing: {e}"
                        logger.error(message)

    def _process_can_logging(self, group_index: int, grp: Group) -> None:

        channels = grp.channels
        group = grp

        dbc = None

        for i, channel in enumerate(channels):
            if channel.name == "CAN_DataFrame":
                attachment_addr = channel.attachment

                if attachment_addr is not None:
                    if attachment_addr not in self._dbc_cache:

                        attachment, at_name, md5_sum = self.extract_attachment(
                            index=attachment_addr,
                        )
                        if at_name.suffix.lower() not in (".arxml", ".dbc"):
                            message = f'Expected .dbc or .arxml file as CAN channel attachment but got "{at_name}"'
                            logger.warning(message)
                        elif not attachment:
                            message = f'Attachment "{at_name}" not found'
                            logger.warning(message)
                        else:
                            dbc = load_can_database(at_name, contents=attachment)
                            if dbc:
                                self._dbc_cache[attachment_addr] = dbc
                    else:
                        dbc = self._dbc_cache[attachment_addr]
                break

        if not group.channel_group.flags & v4c.FLAG_CG_PLAIN_BUS_EVENT:
            self._prepare_record(group)
            data = self._load_data(group, record_offset=0, record_count=1)

            for fragment_index, fragment in enumerate(data):

                self._set_temporary_master(None)
                self._set_temporary_master(self.get_master(group_index, data=fragment))

                bus_ids = self.get(
                    "CAN_DataFrame.BusChannel",
                    group=group_index,
                    data=fragment,
                    samples_only=True,
                )[0].astype("<u1")

                msg_ids = (
                    self.get(
                        "CAN_DataFrame.ID",
                        group=group_index,
                        data=fragment,
                        samples_only=True,
                    )[0].astype("<u4")
                    & 0x1FFFFFFF
                )

                if len(bus_ids) == 0:
                    continue

                bus = bus_ids[0]
                msg_id = msg_ids[0]

                bus_map = self.bus_logging_map["CAN"].setdefault(bus, {})
                bus_map[int(msg_id)] = group_index

        elif dbc is None:
            self._prepare_record(group)
            data = self._load_data(group, optimize_read=False)

            for fragment_index, fragment in enumerate(data):

                self._set_temporary_master(None)
                self._set_temporary_master(self.get_master(group_index, data=fragment))

                bus_ids = self.get(
                    "CAN_DataFrame.BusChannel",
                    group=group_index,
                    data=fragment,
                    samples_only=True,
                )[0].astype("<u1")

                msg_ids = (
                    self.get(
                        "CAN_DataFrame.ID",
                        group=group_index,
                        data=fragment,
                        samples_only=True,
                    )[0].astype("<u4")
                    & 0x1FFFFFFF
                )

                if len(bus_ids) == 0:
                    continue

                buses = unique(bus_ids)

                for bus in buses:
                    bus_msg_ids = msg_ids[bus_ids == bus]
                    unique_ids = unique(bus_msg_ids)
                    unique_ids.sort()
                    unique_ids = unique_ids.tolist()

                    bus_map = self.bus_logging_map["CAN"].setdefault(bus, {})

                    for msg_id in unique_ids:
                        bus_map[int(msg_id)] = group_index

            self._set_temporary_master(None)

        else:

            is_j1939 = dbc.contains_j1939

            if is_j1939:
                messages = {message.arbitration_id.pgn: message for message in dbc}
            else:
                messages = {message.arbitration_id.id: message for message in dbc}

            msg_map = {}

            self._prepare_record(group)
            data = self._load_data(group, optimize_read=False)

            for fragment_index, fragment in enumerate(data):

                self._set_temporary_master(None)
                self._set_temporary_master(self.get_master(group_index, data=fragment))

                data_bytes = self.get(
                    "CAN_DataFrame.DataBytes",
                    group=group_index,
                    data=fragment,
                    samples_only=True,
                )[0]

                bus_ids = self.get(
                    "CAN_DataFrame.BusChannel",
                    group=group_index,
                    data=fragment,
                    samples_only=True,
                )[0].astype("<u1")

                msg_ids = (
                    self.get(
                        "CAN_DataFrame.ID", group=group_index, data=fragment
                    ).astype("<u4")
                    & 0x1FFFFFFF
                )

                if is_j1939:

                    tmp_pgn = msg_ids.samples >> 8
                    ps = tmp_pgn & 0xFF
                    pf = (msg_ids.samples >> 16) & 0xFF
                    _pgn = tmp_pgn & 0x3FF00
                    msg_ids.samples = where(pf >= 240, _pgn + ps, _pgn)

                buses = unique(bus_ids)
                if len(bus_ids) == 0:
                    continue

                for bus in buses:
                    idx_ = bus_ids == bus
                    bus_msg_ids = msg_ids.samples[idx_]

                    bus_t = msg_ids.timestamps[idx_]

                    bus_data_bytes = data_bytes[idx_]

                    unique_ids = sorted(unique(bus_msg_ids).astype("<u8"))

                    bus_map = self.bus_logging_map["CAN"].setdefault(bus, {})

                    for msg_id in unique_ids:
                        bus_map[int(msg_id)] = group_index

                    for msg_id in unique_ids:
                        message = messages.get(msg_id, None)
                        if message is None:
                            continue

                        idx = bus_msg_ids == msg_id
                        payload = bus_data_bytes[idx]
                        t = bus_t[idx]

                        extracted_signals = extract_mux(
                            payload, message, msg_id, bus, t
                        )

                        for entry, signals in extracted_signals.items():
                            if len(next(iter(signals.values()))["samples"]) == 0:
                                continue
                            if entry not in msg_map:
                                sigs = []

                                for name_, signal in signals.items():
                                    sig = Signal(
                                        samples=signal["samples"],
                                        timestamps=signal["t"],
                                        name=signal["name"],
                                        comment=signal["comment"],
                                        unit=signal["unit"],
                                        invalidation_bits=signal["invalidation_bits"],
                                        display_names={
                                            f"{message.name}.{signal['name']}": "message_name",
                                            f"CAN{bus}.{message.name}.{signal['name']}": "bus_name",
                                        },
                                    )

                                    sigs.append(sig)

                                cg_nr = self.append(
                                    sigs,
                                    acq_name=f"from CAN{bus} message ID=0x{msg_id:X}",
                                    comment=f"{message} 0x{msg_id:X}",
                                    common_timebase=True,
                                )

                                msg_map[entry] = cg_nr

                                for ch_index, ch in enumerate(
                                    self.groups[cg_nr].channels
                                ):
                                    if ch_index == 0:
                                        continue

                                    entry = cg_nr, ch_index

                                    name_ = f"{message}.{ch.name}"
                                    self.channels_db.add(name_, entry)

                                    name_ = f"CAN{bus}.{message}.{ch.name}"
                                    self.channels_db.add(name_, entry)

                                    name_ = f"CAN_DataFrame_{msg_id}.{ch.name}"
                                    self.channels_db.add(name_, entry)

                                    name_ = f"CAN{bus}.CAN_DataFrame_{msg_id}.{ch.name}"
                                    self.channels_db.add(name_, entry)

                            else:

                                index = msg_map[entry]

                                sigs = []

                                for name_, signal in signals.items():

                                    sigs.append(
                                        (signal["samples"], signal["invalidation_bits"])
                                    )

                                    t = signal["t"]

                                sigs.insert(0, (t, None))

                                self.extend(index, sigs)
                self._set_temporary_master(None)

    def _process_lin_logging(self, group_index: int, grp: Group) -> None:

        channels = grp.channels
        group = grp

        dbc = None

        for i, channel in enumerate(channels):
            if channel.name == "LIN_Frame":
                attachment_addr = channel.attachment
                if attachment_addr is not None:
                    if attachment_addr not in self._dbc_cache:

                        attachment, at_name, md5_sum = self.extract_attachment(
                            index=attachment_addr,
                        )
                        if at_name.suffix.lower() not in (".arxml", ".dbc", ".ldf"):
                            message = f'Expected .dbc, .arxml or .ldf file as LIN channel attachment but got "{at_name}"'
                            logger.warning(message)
                        elif not attachment:
                            message = f'Attachment "{at_name}" not found'
                            logger.warning(message)
                        else:
                            contents = (
                                None if at_name.suffix.lower() == ".ldf" else attachment
                            )
                            dbc = load_can_database(at_name, contents=contents)
                            if dbc:
                                self._dbc_cache[attachment_addr] = dbc
                    else:
                        dbc = self._dbc_cache[attachment_addr]
                break

        if dbc is None:
            self._prepare_record(group)
            data = self._load_data(group, optimize_read=False)

            for fragment_index, fragment in enumerate(data):

                self._set_temporary_master(None)
                self._set_temporary_master(self.get_master(group_index, data=fragment))

                msg_ids = (
                    self.get(
                        "LIN_Frame.ID",
                        group=group_index,
                        data=fragment,
                        samples_only=True,
                    )[0].astype("<u4")
                    & 0x1FFFFFFF
                )

                unique_ids = sorted(unique(msg_ids).astype("<u8"))

                lin_map = self.bus_logging_map["LIN"]

                for msg_id in unique_ids:
                    lin_map[int(msg_id)] = group_index

            self._set_temporary_master(None)

        else:

            messages = {message.arbitration_id.id: message for message in dbc}

            msg_map = {}

            self._prepare_record(group)
            data = self._load_data(group, optimize_read=False)

            for fragment_index, fragment in enumerate(data):

                self._set_temporary_master(None)
                self._set_temporary_master(self.get_master(group_index, data=fragment))

                msg_ids = (
                    self.get("LIN_Frame.ID", group=group_index, data=fragment).astype(
                        "<u4"
                    )
                    & 0x1FFFFFFF
                )

                data_bytes = self.get(
                    "LIN_Frame.DataBytes",
                    group=group_index,
                    data=fragment,
                    samples_only=True,
                )[0]

                bus_msg_ids = msg_ids.samples

                bus_t = msg_ids.timestamps
                bus_data_bytes = data_bytes

                unique_ids = sorted(unique(bus_msg_ids).astype("<u8"))

                lin_map = self.bus_logging_map["LIN"]

                for msg_id in unique_ids:
                    lin_map[int(msg_id)] = group_index

                for msg_id in unique_ids:
                    message = messages.get(msg_id, None)
                    if message is None:
                        continue

                    idx = bus_msg_ids == msg_id
                    payload = bus_data_bytes[idx]
                    t = bus_t[idx]

                    extracted_signals = extract_mux(payload, message, msg_id, 0, t)

                    for entry, signals in extracted_signals.items():
                        if len(next(iter(signals.values()))["samples"]) == 0:
                            continue
                        if entry not in msg_map:
                            sigs = []

                            for name_, signal in signals.items():
                                sig = Signal(
                                    samples=signal["samples"],
                                    timestamps=signal["t"],
                                    name=signal["name"],
                                    comment=signal["comment"],
                                    unit=signal["unit"],
                                    invalidation_bits=signal["invalidation_bits"],
                                    display_names={
                                        f"{message.name}.{signal['name']}": "message_name",
                                        f"LIN.{message.name}.{signal['name']}": "bus_name",
                                    },
                                )

                                sigs.append(sig)

                            cg_nr = self.append(
                                sigs,
                                acq_name=f"from LIN message ID=0x{msg_id:X}",
                                comment=f"{message} 0x{msg_id:X}",
                                common_timebase=True,
                            )

                            msg_map[entry] = cg_nr

                            for ch_index, ch in enumerate(self.groups[cg_nr].channels):
                                if ch_index == 0:
                                    continue

                                entry = cg_nr, ch_index

                                name_ = f"{message}.{ch.name}"
                                self.channels_db.add(name_, entry)

                                name_ = f"LIN.{message}.{ch.name}"
                                self.channels_db.add(name_, entry)

                                name_ = f"LIN_Frame_{msg_id}.{ch.name}"
                                self.channels_db.add(name_, entry)

                                name_ = f"LIN.LIN_Frame_{msg_id}.{ch.name}"
                                self.channels_db.add(name_, entry)

                        else:

                            index = msg_map[entry]

                            sigs = []

                            for name_, signal in signals.items():

                                sigs.append(
                                    (signal["samples"], signal["invalidation_bits"])
                                )

                                t = signal["t"]

                            sigs.insert(0, (t, None))

                            self.extend(index, sigs)
                self._set_temporary_master(None) class FloatInterpolation(IntEnum):
    REPEAT_PREVIOUS_SAMPLE = 0
    LINEAR_INTERPOLATION = 1 class IntegerInterpolation(IntEnum):
    REPEAT_PREVIOUS_SAMPLE = 0
    LINEAR_INTERPOLATION = 1
    HYBRID_INTERPOLATION = 2 class Source:

    __slots__ = "name", "path", "comment", "source_type", "bus_type"

    SOURCE_OTHER = v4c.SOURCE_OTHER
    SOURCE_ECU = v4c.SOURCE_ECU
    SOURCE_BUS = v4c.SOURCE_BUS
    SOURCE_IO = v4c.SOURCE_IO
    SOURCE_TOOL = v4c.SOURCE_TOOL
    SOURCE_USER = v4c.SOURCE_USER

    BUS_TYPE_NONE = v4c.BUS_TYPE_NONE
    BUS_TYPE_OTHER = v4c.BUS_TYPE_OTHER
    BUS_TYPE_CAN = v4c.BUS_TYPE_CAN
    BUS_TYPE_LIN = v4c.BUS_TYPE_LIN
    BUS_TYPE_MOST = v4c.BUS_TYPE_MOST
    BUS_TYPE_FLEXRAY = v4c.BUS_TYPE_FLEXRAY
    BUS_TYPE_K_LINE = v4c.BUS_TYPE_K_LINE
    BUS_TYPE_ETHERNET = v4c.BUS_TYPE_ETHERNET
    BUS_TYPE_USB = v4c.BUS_TYPE_USB

    def __init__(
        self, name: str, path: str, comment: str, source_type: int, bus_type: int
    ) -> None:
        """Commons reprezentation for source information

        Attributes
        ----------
        name : str
            source name
        path : str
            source path
        comment : str
            source comment
        source_type : int
            source type code
        bus_type : int
            source bus code

        """
        self.name, self.path, self.comment, self.source_type, self.bus_type = (
            name,
            path,
            comment,
            source_type,
            bus_type,
        )

    @classmethod
    @lru_cache(128)
    def from_source(cls, source: SourceType) -> Source:
        if isinstance(source, v3b.ChannelExtension):
            if source.type == v3c.SOURCE_ECU:
                return cls(
                    source.name,
                    source.path,
                    source.comment,
                    cls.SOURCE_OTHER,  # source type other
                    cls.BUS_TYPE_NONE,  # bus type none
                )
            else:
                return cls(
                    source.name,
                    source.path,
                    source.comment,
                    cls.SOURCE_BUS,  # source type bus
                    cls.BUS_TYPE_CAN,  # bus type CAN
                )

        elif isinstance(source, v4b.SourceInformation):
            return cls(
                source.name,
                source.path,
                source.comment,
                source.source_type,
                source.bus_type,
            )
        elif isinstance(source, Source):
            return cls(
                source.name,
                source.path,
                source.comment,
                source.source_type,
                source.bus_type,
            )

    def get_details(self) -> str:
        source_type = v4c.SOURCE_TYPE_TO_STRING[self.source_type]
        bus_type = v4c.BUS_TYPE_TO_STRING[self.bus_type]
        return f"     {source_type} source on bus {bus_type}: name=[{self.name}] path=[{self.path}]"         class DetectDict(TypedDict):
class MdfException(Exception):
class ChannelsDB(Dict[str, Tuple[Tuple[int, int], ...]]):
class UniqueDB(object):
class Group:
class VirtualChannelGroup:
class DataBlockInfo:
class InvalidationBlockInfo(DataBlockInfo):
class SignalDataBlockInfo:
        def detect(text: bytes) -> DetectDict:
def extract_cncomment_xml(comment: str) -> str:
def matlab_compatible(name: str) -> str:
def get_text_v3(
    address: int,
    stream: ReadableBufferType,
    mapped: bool = ...,
    decode: Literal[True] = ...,
) -> str:
def get_text_v3(
    address: int,
    stream: ReadableBufferType,
    mapped: bool = ...,
    decode: Literal[False] = ...,
) -> bytes:
def get_text_v3(
    address: int, stream: ReadableBufferType, mapped: bool = False, decode: bool = True
) -> str | bytes:
def get_text_v4(
    address: int,
    stream: ReadableBufferType,
    mapped: bool = ...,
    decode: Literal[True] = ...,
) -> str:
def get_text_v4(
    address: int,
    stream: ReadableBufferType,
    mapped: bool = ...,
    decode: Literal[False] = ...,
) -> bytes:
def get_text_v4(
    address: int, stream: ReadableBufferType, mapped: bool = False, decode: bool = True
) -> str | bytes:
def sanitize_xml(text: str) -> str:
def extract_display_names(comment: str) -> dict[str, str]:
def extract_encryption_information(comment: str) -> dict[str, str]:
def get_fmt_v3(
    data_type: int, size: int, byte_order: int = v3c.BYTE_ORDER_INTEL
) -> str:
def get_fmt_v4(
    data_type: int, size: int, channel_type: int = v4c.CHANNEL_TYPE_VALUE
) -> str:
def fmt_to_datatype_v3(
    fmt: dtype[Any], shape: tuple[int, ...], array: bool = False
) -> tuple[int, int]:
def info_to_datatype_v4(signed: bool, little_endian: bool) -> int:
def fmt_to_datatype_v4(
    fmt: dtype[Any], shape: tuple[int, ...], array: bool = False
) -> tuple[int, int]:
def as_non_byte_sized_signed_int(
    integer_array: NDArray[Any], bit_length: int
) -> NDArray[Any]:
def debug_channel(
    mdf: MDF_v2_v3_v4,
    group: Group,
    channel: ChannelType,
    dependency: list[tuple[int, int]],
    file: StringIO | None = None,
) -> None:
def count_channel_groups(
    stream: ReadableBufferType, include_channels: bool = False, mapped: bool = False
) -> tuple[int, int]:
def validate_version_argument(version: str, hint: int = 4) -> str:
    def __init__(self) -> None:
    def add(self, channel_name: str, entry: tuple[int, int]) -> None:
def randomized_string(size: int) -> bytes:
def is_file_like(obj: object) -> bool:
    def __init__(self) -> None:
    def get_unique_name(self, name: str) -> str:
def cut_video_stream(stream: bytes, start: float, end: float, fmt: str) -> bytes:
def get_video_stream_duration(stream: bytes) -> float | None:
    def __init__(self, data_group: DataGroupType) -> None:
    def __getitem__(self, item: str) -> Any:
    def __setitem__(self, item: str, value: Any) -> None:
    def set_blocks_info(self, info: list[DataBlockInfo]) -> None:
    def __contains__(self, item: str) -> bool:
    def clear(self) -> None:
    def get_data_blocks(self) -> Iterator[DataBlockInfo]:
    def get_signal_data_blocks(self, index: int) -> Iterator[SignalDataBlockInfo]:
    def __init__(self) -> None:
    def __repr__(self) -> None:
def block_fields(obj: object) -> list[str]:
def components(
    channel: NDArray[Any],
    channel_name: str,
    unique_names: UniqueDB,
    prefix: str = "",
    master: NDArray[Any] | None = None,
    only_basenames: bool = False,
) -> tuple[str, Series[Any]]:
    def __init__(
        self,
        address: int,
        block_type: int,
        original_size: int,
        compressed_size: int,
        param: int,
        invalidation_block=None,
        block_limit: int | None = None,
    ) -> None:
    def __repr__(self) -> str:
    def __init__(
        self,
        address: int,
        block_type: int,
        original_size: int,
        compressed_size: int,
        param: int,
        all_valid: bool = False,
        block_limit: int | None = None,
    ) -> None:
    def __repr__(self) -> str:
    def __init__(
        self,
        address: int,
        original_size: int,
        block_type: int = v4c.DT_BLOCK,
        param: int = 0,
        compressed_size: int | None = None,
        location: int = v4c.LOCATION_ORIGINAL_FILE,
    ) -> None:
    def __repr__(self) -> str:
def get_fields(obj: object) -> list[Any]:
def downcast(array: NDArray[Any]) -> NDArray[Any]:
def master_using_raster(
    mdf: MDF_v2_v3_v4, raster: RasterType, endpoint: bool = False
) -> NDArray[Any]:
def csv_int2bin(val) -> str:
def csv_int2hex(val) -> str:
    def csv_bytearray2hex(val, size: int | None = None) -> str:
    def csv_bytearray2hex(val, size: int | None = None) -> str:
def pandas_query_compatible(name: str) -> str:
def load_can_database(
    path: StrPathType, contents: bytes | str | None = None, **kwargs
) -> CanMatrix | None:
def all_blocks_addresses(obj: ReadableBufferType):
def plausible_timestamps(
    t: NDArray[Any],
    minimum: float,
    maximum: float,
    exp_min: int = -15,
    exp_max: int = 15,
) -> tuple[bool, NDArray[bool_]]:
def escape_xml_string(string: str) -> str:
EMPTY_TUPLE = tuple()
CHANNEL_COUNT = (1000, 2000, 10000, 20000)
CONVERT = (10 * 2**20, 20 * 2**20, 30 * 2**20, 40 * 2**20)
CONVERT = interp(_channel_count, CHANNEL_COUNT, CONVERT).astype("<u4")
MERGE = (10 * 2**20, 20 * 2**20, 35 * 2**20, 60 * 2**20)
MERGE = interp(_channel_count, CHANNEL_COUNT, MERGE).astype("<u4")
CHANNEL_COUNT = _channel_count
MDF2_VERSIONS = ("2.00", "2.10", "2.14")
MDF3_VERSIONS = ("3.00", "3.10", "3.20", "3.30")
MDF4_VERSIONS = ("4.00", "4.10", "4.11", "4.20")
SUPPORTED_VERSIONS = MDF2_VERSIONS + MDF3_VERSIONS + MDF4_VERSIONS
ALLOWED_MATLAB_CHARS = set(string.ascii_letters + string.digits + "_")
 class ChannelConversion(_ChannelConversionBase):
    """CCBLOCK class

    *ChannelConversion* has the following common fields

    * ``id`` - bytes : block ID; always b'CC'
    * ``block_len`` - int : block bytes size
    * ``range_flag`` - int : value range valid flag
    * ``min_phy_value`` - float : min raw value of all samples
    * ``max_phy_value`` - float : max raw value of all samples
    * ``unit`` - bytes : physical unit
    * ``conversion_type`` - int : integer code for conversion type
    * ``ref_param_nr`` - int : number of referenced parameters

    *ChannelConversion* has the following specific fields

    * linear conversion

        * ``a`` - float : factor
        * ``b`` - float : offset
        * ``CANapeHiddenExtra`` - bytes : sometimes CANape appends extra
          information; not compliant with MDF specs

    * algebraic conversion

        * ``formula`` - bytes : equation as string

    * polynomial or rational conversion

        * ``P1`` to ``P6`` - float : parameters

    * exponential or logarithmic conversion

        * ``P1`` to ``P7`` - float : parameters

    * tabular with or without interpolation (grouped by index)

        * ``raw_<N>`` - int : N-th raw value (X axis)
        * ``phys_<N>`` - float : N-th physical value (Y axis)

    * text table conversion

        * ``param_val_<N>`` - int : N-th raw value (X axis)
        * ``text_<N>`` - N-th text physical value (Y axis)

    * text range table conversion

        * ``default_lower`` - float : default lower raw value
        * ``default_upper`` - float : default upper raw value
        * ``default_addr`` - int : address of default text physical value
        * ``lower_<N>`` - float : N-th lower raw value
        * ``upper_<N>`` - float : N-th upper raw value
        * ``text_<N>`` - int : address of N-th text physical value

    Other attributes

    * ``address`` - int : block address inside mdf file
    * ``formula`` - str : formula string in case of algebraic conversion
    * ``referenced_blocks`` - list : list of CCBLOCK/TXBLOCK referenced by the conversion
    * ``unit`` - str : physical unit

    Parameters
    ----------
    address : int
        block address inside mdf file
    raw_bytes : bytes
        complete block read from disk
    stream : file handle
        mdf file handle
    for dynamically created objects :
        see the key-value pairs

    Examples
    --------
    >>> with open('test.mdf', 'rb') as mdf:
    ...     cc1 = ChannelConversion(stream=mdf, address=0xBA52)
    >>> cc2 = ChannelConversion(conversion_type=0)
    >>> cc1['b'], cc1['a']
    0, 100.0

    """

    def __init__(self, **kwargs) -> None:
        super().__init__()

        self.unit = self.formula = ""

        self.referenced_blocks = {}

        if "raw_bytes" in kwargs or "stream" in kwargs:
            mapped = kwargs.get("mapped", False)
            self.address = address = kwargs.get("address", 0)
            try:
                block = kwargs["raw_bytes"]
                (self.id, self.block_len) = COMMON_uf(block)
                size = self.block_len
                block_size = len(block)
                block = block[4:]
                stream = kwargs["stream"]

            except KeyError:
                stream = kwargs["stream"]
                if mapped:
                    (self.id, self.block_len) = COMMON_uf(stream, address)
                    block_size = size = self.block_len
                    block = stream[address + 4 : address + size]
                else:
                    stream.seek(address)
                    block = stream.read(4)
                    (self.id, self.block_len) = COMMON_u(block)

                    block_size = size = self.block_len
                    block = stream.read(size - 4)

            (
                self.range_flag,
                self.min_phy_value,
                self.max_phy_value,
                self.unit_field,
                self.conversion_type,
                self.ref_param_nr,
            ) = CONVERSION_COMMON_SHORT_uf(block)

            self.unit = self.unit_field.decode("latin-1").strip(" \t\r\n\0")

            conv_type = self.conversion_type

            if conv_type == v23c.CONVERSION_TYPE_LINEAR:
                (self.b, self.a) = unpack_from("<2d", block, v23c.CC_COMMON_SHORT_SIZE)
                if not size == v23c.CC_LIN_BLOCK_SIZE:
                    self.CANapeHiddenExtra = block[v23c.CC_LIN_BLOCK_SIZE - 4 :]
                    size = len(self.CANapeHiddenExtra)
                    nr = size // 40
                    values = unpack_from(
                        "<" + "d32s" * nr, block, v23c.CC_COMMON_SHORT_SIZE
                    )

                    for i in range(nr):
                        (self[f"param_val_{i}"], self[f"text_{i}"]) = (
                            values[i * 2],
                            values[2 * i + 1],
                        )

            elif conv_type == v23c.CONVERSION_TYPE_NONE:
                pass

            elif conv_type == v23c.CONVERSION_TYPE_FORMULA:
                self.formula_field = block[v23c.CC_COMMON_SHORT_SIZE :]
                self.formula = (
                    self.formula_field.decode("latin-1")
                    .strip(" \t\r\n\0")
                    .replace("x", "X")
                )
                if "X1" not in self.formula:
                    self.formula = self.formula.replace("X", "X1")

            elif conv_type in (v23c.CONVERSION_TYPE_TABI, v23c.CONVERSION_TYPE_TAB):

                nr = self.ref_param_nr

                size = v23c.CC_COMMON_BLOCK_SIZE + nr * 16

                if block_size == v23c.MAX_UINT16:
                    stream.seek(address)
                    raw_bytes = stream.read(size)
                    conversion = ChannelConversion(
                        raw_bytes=raw_bytes, stream=stream, address=address
                    )
                    conversion.block_len = size

                    self.update(conversion)
                    self.referenced_blocks = conversion.referenced_blocks

                else:
                    values = unpack_from(f"<{2*nr}d", block, v23c.CC_COMMON_SHORT_SIZE)
                    for i in range(nr):
                        (self[f"raw_{i}"], self[f"phys_{i}"]) = (
                            values[i * 2],
                            values[2 * i + 1],
                        )

            elif conv_type in (v23c.CONVERSION_TYPE_POLY, v23c.CONVERSION_TYPE_RAT):
                (self.P1, self.P2, self.P3, self.P4, self.P5, self.P6) = unpack_from(
                    "<6d", block, v23c.CC_COMMON_SHORT_SIZE
                )

            elif conv_type in (v23c.CONVERSION_TYPE_EXPO, v23c.CONVERSION_TYPE_LOGH):
                (
                    self.P1,
                    self.P2,
                    self.P3,
                    self.P4,
                    self.P5,
                    self.P6,
                    self.P7,
                ) = unpack_from("<7d", block, v23c.CC_COMMON_SHORT_SIZE)

            elif conv_type == v23c.CONVERSION_TYPE_TABX:
                nr = self.ref_param_nr

                size = v23c.CC_COMMON_BLOCK_SIZE + nr * 40

                if block_size == v23c.MAX_UINT16:
                    stream.seek(address)
                    raw_bytes = stream.read(size)
                    conversion = ChannelConversion(
                        raw_bytes=raw_bytes, stream=stream, address=address
                    )
                    conversion.block_len = size

                    for attr in get_fields(conversion):
                        setattr(self, attr, getattr(conversion, attr))

                    self.referenced_blocks = conversion.referenced_blocks

                else:

                    values = unpack_from(
                        "<" + "d32s" * nr, block, v23c.CC_COMMON_SHORT_SIZE
                    )

                    for i in range(nr):
                        (self[f"param_val_{i}"], self[f"text_{i}"]) = (
                            values[i * 2],
                            values[2 * i + 1],
                        )

            elif conv_type == v23c.CONVERSION_TYPE_RTABX:

                nr = self.ref_param_nr - 1

                size = v23c.CC_COMMON_BLOCK_SIZE + (nr + 1) * 20

                if block_size == v23c.MAX_UINT16:
                    stream.seek(address)
                    raw_bytes = stream.read(size)
                    conversion = ChannelConversion(
                        raw_bytes=raw_bytes, stream=stream, address=address
                    )
                    conversion.block_len = size

                    for attr in get_fields(conversion):
                        setattr(self, attr, getattr(conversion, attr))
                    self.referenced_blocks = conversion.referenced_blocks

                else:

                    (
                        self.default_lower,
                        self.default_upper,
                        self.default_addr,
                    ) = unpack_from("<2dI", block, v23c.CC_COMMON_SHORT_SIZE)

                    if self.default_addr:
                        self.referenced_blocks["default_addr"] = get_text_v3(
                            address=self.default_addr,
                            stream=stream,
                            mapped=mapped,
                            decode=False,
                        )
                    else:
                        self.referenced_blocks["default_addr"] = b""

                    values = unpack_from(
                        "<" + "2dI" * nr, block, v23c.CC_COMMON_SHORT_SIZE + 20
                    )

                    for i in range(nr):
                        (self[f"lower_{i}"], self[f"upper_{i}"], self[f"text_{i}"]) = (
                            values[i * 3],
                            values[3 * i + 1],
                            values[3 * i + 2],
                        )
                        if values[3 * i + 2]:
                            block = get_text_v3(
                                address=values[3 * i + 2],
                                stream=stream,
                                mapped=mapped,
                                decode=False,
                            )
                            self.referenced_blocks[f"text_{i}"] = block

                        else:
                            self.referenced_blocks[f"text_{i}"] = b""

            if self.id != b"CC":
                message = f'Expected "CC" block @{hex(address)} but found "{self.id}"'

                logger.exception(message)
                raise MdfException(message)

        else:

            self.address = 0
            self.id = b"CC"
            self.unit_field = kwargs.get("unit", b"")

            if kwargs["conversion_type"] == v23c.CONVERSION_TYPE_NONE:
                self.block_len = v23c.CC_COMMON_BLOCK_SIZE
                self.range_flag = kwargs.get("range_flag", 1)
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                self.unit_field = kwargs.get("unit", ("\0" * 20).encode("latin-1"))
                self.conversion_type = v23c.CONVERSION_TYPE_NONE
                self.ref_param_nr = 0

            elif kwargs["conversion_type"] == v23c.CONVERSION_TYPE_LINEAR:
                self.block_len = v23c.CC_LIN_BLOCK_SIZE
                self.range_flag = kwargs.get("range_flag", 1)
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                self.unit_field = kwargs.get("unit", ("\0" * 20).encode("latin-1"))
                self.conversion_type = v23c.CONVERSION_TYPE_LINEAR
                self.ref_param_nr = 2
                self.b = kwargs.get("b", 0)
                self.a = kwargs.get("a", 1)
                if not self.block_len == v23c.CC_LIN_BLOCK_SIZE:
                    self.CANapeHiddenExtra = kwargs["CANapeHiddenExtra"]

            elif kwargs["conversion_type"] in (
                v23c.CONVERSION_TYPE_POLY,
                v23c.CONVERSION_TYPE_RAT,
            ):
                self.block_len = v23c.CC_POLY_BLOCK_SIZE
                self.range_flag = kwargs.get("range_flag", 1)
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                self.unit_field = kwargs.get("unit", ("\0" * 20).encode("latin-1"))
                self.conversion_type = kwargs["conversion_type"]
                self.ref_param_nr = 6
                self.P1 = kwargs.get("P1", 0)
                self.P2 = kwargs.get("P2", 0)
                self.P3 = kwargs.get("P3", 0)
                self.P4 = kwargs.get("P4", 0)
                self.P5 = kwargs.get("P5", 0)
                self.P6 = kwargs.get("P6", 0)

            elif kwargs["conversion_type"] in (
                v23c.CONVERSION_TYPE_EXPO,
                v23c.CONVERSION_TYPE_LOGH,
            ):
                self.block_len = v23c.CC_EXPO_BLOCK_SIZE
                self.range_flag = kwargs.get("range_flag", 1)
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                self.unit_field = kwargs.get("unit", ("\0" * 20).encode("latin-1"))
                self.conversion_type = v23c.CONVERSION_TYPE_EXPO
                self.ref_param_nr = 7
                self.P1 = kwargs.get("P1", 0)
                self.P2 = kwargs.get("P2", 0)
                self.P3 = kwargs.get("P3", 0)
                self.P4 = kwargs.get("P4", 0)
                self.P5 = kwargs.get("P5", 0)
                self.P6 = kwargs.get("P6", 0)
                self.P7 = kwargs.get("P7", 0)

            elif kwargs["conversion_type"] == v23c.CONVERSION_TYPE_FORMULA:
                formula = kwargs["formula"]
                formula_len = len(formula)
                try:
                    self.formula = formula.decode("latin-1")
                    formula += b"\0"
                except AttributeError:
                    self.formula = formula
                    formula = formula.encode("latin-1") + b"\0"
                self.block_len = 46 + formula_len + 1
                self.range_flag = kwargs.get("range_flag", 1)
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                self.unit_field = kwargs.get("unit", ("\0" * 20).encode("latin-1"))
                self.conversion_type = v23c.CONVERSION_TYPE_FORMULA
                self.ref_param_nr = formula_len
                self.formula_field = formula

            elif kwargs["conversion_type"] in (
                v23c.CONVERSION_TYPE_TABI,
                v23c.CONVERSION_TYPE_TAB,
            ):
                nr = kwargs["ref_param_nr"]
                self.block_len = v23c.CC_COMMON_BLOCK_SIZE + nr * 2 * 8
                self.range_flag = kwargs.get("range_flag", 1)
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                self.unit_field = kwargs.get("unit", ("\0" * 20).encode("latin-1"))
                self.conversion_type = kwargs["conversion_type"]
                self.ref_param_nr = nr
                for i in range(nr):
                    self[f"raw_{i}"] = kwargs[f"raw_{i}"]
                    self[f"phys_{i}"] = kwargs[f"phys_{i}"]

            elif kwargs["conversion_type"] == v23c.CONVERSION_TYPE_TABX:
                nr = kwargs["ref_param_nr"]
                self.block_len = v23c.CC_COMMON_BLOCK_SIZE + 40 * nr
                self.range_flag = kwargs.get("range_flag", 0)
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                self.unit_field = kwargs.get("unit", ("\0" * 20).encode("latin-1"))
                self.conversion_type = v23c.CONVERSION_TYPE_TABX
                self.ref_param_nr = nr

                for i in range(nr):
                    self[f"param_val_{i}"] = kwargs[f"param_val_{i}"]
                    self[f"text_{i}"] = kwargs[f"text_{i}"]

            elif kwargs["conversion_type"] == v23c.CONVERSION_TYPE_RTABX:
                nr = kwargs["ref_param_nr"]
                self.block_len = v23c.CC_COMMON_BLOCK_SIZE + 20 * nr
                self.range_flag = kwargs.get("range_flag", 0)
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                self.unit_field = kwargs.get("unit", ("\0" * 20).encode("latin-1"))
                self.conversion_type = v23c.CONVERSION_TYPE_RTABX
                self.ref_param_nr = nr

                self.default_lower = 0
                self.default_upper = 0
                self.default_addr = 0
                key = "default_addr"
                if key in kwargs:
                    self.referenced_blocks[key] = kwargs[key]
                else:
                    self.referenced_blocks[key] = b""

                for i in range(nr - 1):
                    self[f"lower_{i}"] = kwargs[f"lower_{i}"]
                    self[f"upper_{i}"] = kwargs[f"upper_{i}"]
                    key = f"text_{i}"
                    self[key] = 0
                    self.referenced_blocks[key] = kwargs[key]
            else:
                message = (
                    f'Conversion type "{kwargs["conversion_type"]}" not implemented'
                )
                logger.exception(message)
                raise MdfException(message)

    def to_blocks(
        self,
        address: int,
        blocks: list[Any],
        defined_texts: dict[str, int],
        cc_map: dict[bytes, int],
    ) -> int:
        self.unit_field = self.unit.encode("latin-1", "ignore")[:19]

        if self.conversion_type == v23c.CONVERSION_TYPE_FORMULA:
            formula = self.formula
            if not formula.endswith("\0"):
                formula += "\0"
            self.formula_field = formula.encode("latin-1")
            self.block_len = v23c.CC_COMMON_BLOCK_SIZE + len(self.formula_field)

        for key, block in self.referenced_blocks.items():
            if block:
                if isinstance(block, ChannelConversion):
                    address = block.to_blocks(address, blocks, defined_texts, cc_map)
                    self[key] = block.address
                else:
                    text = block
                    if text in defined_texts:
                        self[key] = defined_texts[text]
                    else:
                        block = TextBlock(text=text)
                        defined_texts[text] = address
                        blocks.append(block)
                        self[key] = address
                        address += block.block_len
            else:
                self[key] = 0

        bts = bytes(self)
        if bts in cc_map:
            self.address = cc_map[bts]
        else:
            blocks.append(bts)
            self.address = address
            cc_map[bts] = address
            size = self.block_len
            address += len(bts)

        return address

    def metadata(self, indent: str = "") -> str:
        conv = self.conversion_type
        if conv == v23c.CONVERSION_TYPE_NONE:
            keys = v23c.KEYS_CONVERSION_NONE
        elif conv == v23c.CONVERSION_TYPE_FORMULA:
            keys = v23c.KEYS_CONVERSION_FORMULA
        elif conv == v23c.CONVERSION_TYPE_LINEAR:
            keys = v23c.KEYS_CONVERSION_LINEAR
            if not self.block_len == v23c.CC_LIN_BLOCK_SIZE:
                keys += ("CANapeHiddenExtra",)

                nr = self.ref_param_nr
                new_keys = []
                for i in range(nr):
                    new_keys.append(f"param_val_{i}")
                    new_keys.append(f"text_{i}")
                keys += tuple(new_keys)

        elif conv in (v23c.CONVERSION_TYPE_POLY, v23c.CONVERSION_TYPE_RAT):
            keys = v23c.KEYS_CONVERSION_POLY_RAT
        elif conv in (v23c.CONVERSION_TYPE_EXPO, v23c.CONVERSION_TYPE_LOGH):
            keys = v23c.KEYS_CONVERSION_EXPO_LOGH
        elif conv in (v23c.CONVERSION_TYPE_TABI, v23c.CONVERSION_TYPE_TAB):
            nr = self.ref_param_nr
            keys = list(v23c.KEYS_CONVERSION_NONE)
            for i in range(nr):
                keys.append(f"raw_{i}")
                keys.append(f"phys_{i}")
        elif conv == v23c.CONVERSION_TYPE_RTABX:
            nr = self.ref_param_nr
            keys = list(v23c.KEYS_CONVERSION_NONE)
            keys += ["default_lower", "default_upper", "default_addr"]
            for i in range(nr - 1):
                keys.append(f"lower_{i}")
                keys.append(f"upper_{i}")
                keys.append(f"text_{i}")
        elif conv == v23c.CONVERSION_TYPE_TABX:
            nr = self.ref_param_nr
            keys = list(v23c.KEYS_CONVERSION_NONE)
            for i in range(nr):
                keys.append(f"param_val_{i}")
                keys.append(f"text_{i}")

        max_len = max(len(key) for key in keys)
        template = f"{{: <{max_len}}}: {{}}"

        metadata = []
        lines = f"""
address: {hex(self.address)}

""".split(
            "\n"
        )

        for key in keys:
            val = getattr(self, key)
            if key.endswith("addr") or key.startswith("text_") and isinstance(val, int):
                lines.append(template.format(key, hex(val)))
            elif isinstance(val, float):
                lines.append(template.format(key, round(val, 6)))
            else:
                if isinstance(val, bytes):
                    lines.append(template.format(key, val.strip(b"\0")))
                else:
                    lines.append(template.format(key, val))

            if key == "conversion_type":
                lines[
                    -1
                ] += f" [{v23c.CONVERSION_TYPE_TO_STRING[self.conversion_type]}]"
            elif self.referenced_blocks and key in self.referenced_blocks:
                val = self.referenced_blocks[key]
                if isinstance(val, bytes):
                    lines[-1] += f" (= {str(val)[1:]})"
                else:
                    lines[-1] += f" (= CCBLOCK @ {hex(val.address)})"

        if self.referenced_blocks:
            max_len = max(len(key) for key in self.referenced_blocks)
            template = f"{{: <{max_len}}}: {{}}"

            lines.append("")
            lines.append("Referenced blocks:")
            for key, block in self.referenced_blocks.items():
                if isinstance(block, ChannelConversion):
                    lines.append(template.format(key, ""))
                    lines.extend(block.metadata(indent + "    ").split("\n"))
                else:
                    lines.append(template.format(key, str(block)[1:]))

        for line in lines:
            if not line:
                metadata.append(line)
            else:
                for wrapped_line in wrap(
                    line, initial_indent=indent, subsequent_indent=indent, width=120
                ):
                    metadata.append(wrapped_line)

        return "\n".join(metadata)

    def convert(self, values):
        conversion_type = self.conversion_type

        if conversion_type == v23c.CONVERSION_TYPE_NONE:
            pass

        elif conversion_type == v23c.CONVERSION_TYPE_LINEAR:
            a = self.a
            b = self.b
            if (a, b) != (1, 0):
                values = values * a
                if b:
                    values += b

        elif conversion_type in (v23c.CONVERSION_TYPE_TABI, v23c.CONVERSION_TYPE_TAB):
            nr = self.ref_param_nr

            raw_vals = [self[f"raw_{i}"] for i in range(nr)]
            raw_vals = np.array(raw_vals)
            phys = [self[f"phys_{i}"] for i in range(nr)]
            phys = np.array(phys)

            if conversion_type == v23c.CONVERSION_TYPE_TABI:
                values = np.interp(values, raw_vals, phys)
            else:
                dim = raw_vals.shape[0]

                inds = np.searchsorted(raw_vals, values)

                inds[inds >= dim] = dim - 1

                inds2 = inds - 1
                inds2[inds2 < 0] = 0

                cond = np.abs(values - raw_vals[inds]) >= np.abs(
                    values - raw_vals[inds2]
                )

                values = np.where(cond, phys[inds2], phys[inds])

        elif conversion_type == v23c.CONVERSION_TYPE_TABX:
            nr = self.ref_param_nr
            raw_vals = [self[f"param_val_{i}"] for i in range(nr)]
            raw_vals = np.array(raw_vals)
            phys = [self[f"text_{i}"] for i in range(nr)]
            phys = np.array(phys)

            x = sorted(zip(raw_vals, phys))
            raw_vals = np.array([e[0] for e in x], dtype="<i8")
            phys = np.array([e[1] for e in x])

            default = b""

            idx1 = np.searchsorted(raw_vals, values, side="right") - 1
            idx2 = np.searchsorted(raw_vals, values, side="left")

            idx = np.argwhere(idx1 != idx2).flatten()

            new_values = np.zeros(
                len(values), dtype=max(phys.dtype, np.array([default]).dtype)
            )

            new_values[idx] = default
            idx = np.argwhere(idx1 == idx2).flatten()
            if len(idx):
                new_values[idx] = phys[idx1[idx]]

            values = new_values

        elif conversion_type == v23c.CONVERSION_TYPE_RTABX:
            nr = self.ref_param_nr - 1

            phys = []
            for i in range(nr):
                value = self.referenced_blocks[f"text_{i}"]
                phys.append(value)

            phys = np.array(phys)

            default = self.referenced_blocks["default_addr"]

            if b"{X}" in default:
                default = default.decode("latin-1").replace("{X}", "X").split('"')[1]
                partial_conversion = True
            else:
                partial_conversion = False

            lower = np.array([self[f"lower_{i}"] for i in range(nr)])
            upper = np.array([self[f"upper_{i}"] for i in range(nr)])

            idx1 = np.searchsorted(lower, values, side="right") - 1
            idx2 = np.searchsorted(upper, values, side="left")

            idx = np.argwhere(idx1 != idx2).flatten()

            if partial_conversion and len(idx):
                X = values[idx]
                new_values = np.zeros(len(values), dtype=np.float64)
                new_values[idx] = evaluate(default)

                idx = np.argwhere(idx1 == idx2).flatten()
                if len(idx):
                    new_values[idx] = np.nan
                values = new_values

            else:
                if len(idx):
                    new_values = np.zeros(
                        len(values), dtype=max(phys.dtype, np.array([default]).dtype)
                    )
                    new_values[idx] = default

                    idx = np.argwhere(idx1 == idx2).flatten()
                    if len(idx):
                        new_values[idx] = phys[idx1[idx]]

                    values = new_values
                else:
                    values = phys[idx1]

        elif conversion_type in (v23c.CONVERSION_TYPE_EXPO, v23c.CONVERSION_TYPE_LOGH):
            # pylint: disable=C0103

            if conversion_type == v23c.CONVERSION_TYPE_EXPO:
                func = np.log
            else:
                func = np.exp
            P1 = self.P1
            P2 = self.P2
            P3 = self.P3
            P4 = self.P4
            P5 = self.P5
            P6 = self.P6
            P7 = self.P7
            if P4 == 0:
                values = func(((values - P7) * P6 - P3) / P1) / P2
            elif P1 == 0:
                values = func((P3 / (values - P7) - P6) / P4) / P5
            else:
                message = f"wrong conversion {conversion_type}"
                raise ValueError(message)

        elif conversion_type == v23c.CONVERSION_TYPE_RAT:
            # pylint: disable=unused-variable,C0103

            P1 = self.P1
            P2 = self.P2
            P3 = self.P3
            P4 = self.P4
            P5 = self.P5
            P6 = self.P6

            X = values
            if (P1, P4, P5, P6) == (0, 0, 0, 1):
                if (P2, P3) != (1, 0):
                    values = values * P2
                    if P3:
                        values += P3
            elif (P3, P4, P5, P6) == (0, 0, 1, 0):
                if (P1, P2) != (1, 0):
                    values = values * P1
                    if P2:
                        values += P2
            else:
                try:
                    values = evaluate(v23c.RAT_CONV_TEXT)
                except TypeError:
                    values = (P1 * X**2 + P2 * X + P3) / (P4 * X**2 + P5 * X + P6)

        elif conversion_type == v23c.CONVERSION_TYPE_POLY:
            # pylint: disable=unused-variable,C0103

            P1 = self.P1
            P2 = self.P2
            P3 = self.P3
            P4 = self.P4
            P5 = self.P5
            P6 = self.P6

            X = values

            coefs = (P2, P3, P5, P6)
            if coefs == (0, 0, 0, 0):
                if P1 != P4:
                    try:
                        values = evaluate(v23c.POLY_CONV_SHORT_TEXT)
                    except TypeError:
                        values = P4 * X / P1
            else:

                try:
                    values = evaluate(v23c.POLY_CONV_LONG_TEXT)
                except TypeError:
                    values = (P2 - (P4 * (X - P5 - P6))) / (P3 * (X - P5 - P6) - P1)

        elif conversion_type == v23c.CONVERSION_TYPE_FORMULA:
            # pylint: disable=unused-variable,C0103

            X1 = values
            try:
                values = evaluate(self.formula)
            except:
                values = evaluate3(self.formula)

        return values

    def __getitem__(self, item: str) -> Any:
        return self.__getattribute__(item)

    def __setitem__(self, item: str, value: Any) -> None:
        self.__setattr__(item, value)

    def __bytes__(self) -> bytes:
        conv = self.conversion_type

        # compute the fmt
        if conv == v23c.CONVERSION_TYPE_NONE:
            fmt = v23c.FMT_CONVERSION_COMMON
        elif conv == v23c.CONVERSION_TYPE_FORMULA:
            fmt = v23c.FMT_CONVERSION_FORMULA.format(
                self.block_len - v23c.CC_COMMON_BLOCK_SIZE
            )
        elif conv == v23c.CONVERSION_TYPE_LINEAR:
            fmt = v23c.FMT_CONVERSION_LINEAR
            if not self.block_len == v23c.CC_LIN_BLOCK_SIZE:
                fmt += f"{self.block_len - v23c.CC_LIN_BLOCK_SIZE}s"
        elif conv in (v23c.CONVERSION_TYPE_POLY, v23c.CONVERSION_TYPE_RAT):
            fmt = v23c.FMT_CONVERSION_POLY_RAT
        elif conv in (v23c.CONVERSION_TYPE_EXPO, v23c.CONVERSION_TYPE_LOGH):
            fmt = v23c.FMT_CONVERSION_EXPO_LOGH
        elif conv in (v23c.CONVERSION_TYPE_TABI, v23c.CONVERSION_TYPE_TAB):
            nr = self.ref_param_nr
            fmt = v23c.FMT_CONVERSION_COMMON + f"{2*nr}d"
        elif conv == v23c.CONVERSION_TYPE_RTABX:
            nr = self.ref_param_nr
            fmt = v23c.FMT_CONVERSION_COMMON + "2dI" * nr
        elif conv == v23c.CONVERSION_TYPE_TABX:
            nr = self.ref_param_nr
            fmt = v23c.FMT_CONVERSION_COMMON + "d32s" * nr

        if conv == v23c.CONVERSION_TYPE_NONE:
            keys = v23c.KEYS_CONVERSION_NONE
        elif conv == v23c.CONVERSION_TYPE_FORMULA:
            keys = v23c.KEYS_CONVERSION_FORMULA
        elif conv == v23c.CONVERSION_TYPE_LINEAR:
            keys = v23c.KEYS_CONVERSION_LINEAR
            if not self.block_len == v23c.CC_LIN_BLOCK_SIZE:
                keys += ("CANapeHiddenExtra",)
        elif conv in (v23c.CONVERSION_TYPE_POLY, v23c.CONVERSION_TYPE_RAT):
            keys = v23c.KEYS_CONVERSION_POLY_RAT
        elif conv in (v23c.CONVERSION_TYPE_EXPO, v23c.CONVERSION_TYPE_LOGH):
            keys = v23c.KEYS_CONVERSION_EXPO_LOGH
        elif conv in (v23c.CONVERSION_TYPE_TABI, v23c.CONVERSION_TYPE_TAB):
            nr = self.ref_param_nr
            keys = list(v23c.KEYS_CONVERSION_NONE)
            for i in range(nr):
                keys.append(f"raw_{i}")
                keys.append(f"phys_{i}")
        elif conv == v23c.CONVERSION_TYPE_RTABX:
            nr = self.ref_param_nr
            keys = list(v23c.KEYS_CONVERSION_NONE)
            keys += ["default_lower", "default_upper", "default_addr"]
            for i in range(nr - 1):
                keys.append(f"lower_{i}")
                keys.append(f"upper_{i}")
                keys.append(f"text_{i}")
        elif conv == v23c.CONVERSION_TYPE_TABX:
            nr = self.ref_param_nr
            keys = list(v23c.KEYS_CONVERSION_NONE)
            for i in range(nr):
                keys.append(f"param_val_{i}")
                keys.append(f"text_{i}")

        if self.block_len > v23c.MAX_UINT16:
            self.block_len = v23c.MAX_UINT16
        result = pack(fmt, *[self[key] for key in keys])
        return result

    def __str__(self) -> str:
        fields = []
        for attr in dir(self):
            if attr[:2] + attr[-2:] == "____":
                continue
            try:
                if callable(getattr(self, attr)):
                    continue
                fields.append(f"{attr}:{getattr(self, attr)}")
            except AttributeError:
                continue
        return f"ChannelConversion (referenced blocks: {self.referenced_blocks}, address: {hex(self.address)}, fields: {fields})" class ChannelExtension:
    """CEBLOCK class

    CEBLOCK has the following common fields

    * ``id`` - bytes : block ID; always b'CE'
    * ``block_len`` - int : block bytes size
    * ``type`` - int : extension type identifier

    CEBLOCK has the following specific fields

    * for DIM block

        * ``module_nr`` - int: module number
        * ``module_address`` - int : module address
        * ``description`` - bytes : module description
        * ``ECU_identification`` - bytes : identification of ECU
        * ``reserved0`` - bytes : reserved bytes

    * for Vector CAN block

        * ``CAN_id`` - int : CAN message ID
        * ``CAN_ch_index`` - int : index of CAN channel
        * ``message_name`` - bytes : message name
        * ``sender_name`` - btyes : sender name
        * ``reserved0`` - bytes : reserved bytes

    Other attributes

    * ``address`` - int : block address inside mdf file
    * ``comment`` - str : extension comment
    * ``name`` - str : extension name
    * ``path`` - str : extension path

    Parameters
    ----------
    stream : file handle
        mdf file handle
    address : int
        block address inside mdf file
    for dynamically created objects :
        see the key-value pairs

    """

    __slots__ = (
        "address",
        "name",
        "path",
        "comment",
        "id",
        "block_len",
        "type",
        "module_nr",
        "module_address",
        "description",
        "ECU_identification",
        "reserved0",
        "CAN_id",
        "CAN_ch_index",
        "message_name",
        "sender_name",
    )

    def __init__(self, **kwargs) -> None:
        super().__init__()

        self.name = self.path = self.comment = ""

        if "stream" in kwargs:
            stream = kwargs["stream"]
            try:

                (self.id, self.block_len, self.type) = SOURCE_COMMON_uf(
                    kwargs["raw_bytes"]
                )
                if self.type == v23c.SOURCE_ECU:
                    (
                        self.module_nr,
                        self.module_address,
                        self.description,
                        self.ECU_identification,
                        self.reserved0,
                    ) = SOURCE_EXTRA_ECU_uf(kwargs["raw_bytes"], 6)
                elif self.type == v23c.SOURCE_VECTOR:
                    (
                        self.CAN_id,
                        self.CAN_ch_index,
                        self.message_name,
                        self.sender_name,
                        self.reserved0,
                    ) = SOURCE_EXTRA_VECTOR_uf(kwargs["raw_bytes"], 6)

                self.address = kwargs.get("address", 0)
            except KeyError:
                if kwargs.get("mapped", False):
                    self.address = address = kwargs["address"]

                    (self.id, self.block_len, self.type) = SOURCE_COMMON_uf(
                        stream, address
                    )

                    if self.type == v23c.SOURCE_ECU:
                        (
                            self.module_nr,
                            self.module_address,
                            self.description,
                            self.ECU_identification,
                            self.reserved0,
                        ) = SOURCE_EXTRA_ECU_uf(stream, address + 6)
                    elif self.type == v23c.SOURCE_VECTOR:
                        (
                            self.CAN_id,
                            self.CAN_ch_index,
                            self.message_name,
                            self.sender_name,
                            self.reserved0,
                        ) = SOURCE_EXTRA_VECTOR_uf(stream, address + 6)
                else:
                    self.address = address = kwargs["address"]
                    stream.seek(address)
                    block = stream.read(v23c.CE_BLOCK_SIZE)
                    (self.id, self.block_len, self.type) = SOURCE_COMMON_uf(block)

                    if self.type == v23c.SOURCE_ECU:
                        (
                            self.module_nr,
                            self.module_address,
                            self.description,
                            self.ECU_identification,
                            self.reserved0,
                        ) = SOURCE_EXTRA_ECU_uf(block, 6)
                    elif self.type == v23c.SOURCE_VECTOR:
                        (
                            self.CAN_id,
                            self.CAN_ch_index,
                            self.message_name,
                            self.sender_name,
                            self.reserved0,
                        ) = SOURCE_EXTRA_VECTOR_uf(block, 6)

            if self.id != b"CE":
                message = f'Expected "CE" block @{hex(address)} but found "{self.id}"'

                logger.exception(message)
                raise MdfException(message)

        else:

            self.address = 0
            self.id = b"CE"
            self.block_len = kwargs.get("block_len", v23c.CE_BLOCK_SIZE)
            self.type = kwargs.get("type", 2)
            if self.type == v23c.SOURCE_ECU:
                self.module_nr = kwargs.get("module_nr", 0)
                self.module_address = kwargs.get("module_address", 0)
                self.description = kwargs.get("description", b"\0")
                self.ECU_identification = kwargs.get("ECU_identification", b"\0")
                self.reserved0 = kwargs.get("reserved0", b"\0")
            elif self.type == v23c.SOURCE_VECTOR:
                self.CAN_id = kwargs.get("CAN_id", 0)
                self.CAN_ch_index = kwargs.get("CAN_ch_index", 0)
                self.message_name = kwargs.get("message_name", b"\0")
                self.sender_name = kwargs.get("sender_name", b"\0")
                self.reserved0 = kwargs.get("reserved0", b"\0")

        if self.type == v23c.SOURCE_ECU:
            self.path = self.ECU_identification.decode("latin-1").strip(" \t\n\r\0")
            self.name = self.description.decode("latin-1").strip(" \t\n\r\0")
            self.comment = (
                f"Module number={self.module_nr} @ address={self.module_address}"
            )
        else:
            self.path = self.sender_name.decode("latin-1").strip(" \t\n\r\0")
            self.name = self.message_name.decode("latin-1").strip(" \t\n\r\0")
            self.comment = (
                f"Message ID={hex(self.CAN_id)} on CAN bus {self.CAN_ch_index}"
            )

    def to_blocks(
        self,
        address: int,
        blocks: list[Any],
        defined_texts: dict[str, int],
        cc_map: dict[bytes, int],
    ) -> int:

        if self.type == v23c.SOURCE_ECU:
            self.ECU_identification = self.path.encode("latin-1")[:31]
            self.description = self.name.encode("latin-1")[:79]
        else:
            self.sender_name = self.path.encode("latin-1")[:35]
            self.message_name = self.name.encode("latin-1")[:35]

        bts = bytes(self)
        if bts in cc_map:
            self.address = cc_map[bts]
        else:
            blocks.append(bts)
            self.address = address
            cc_map[bts] = address
            address += self.block_len

        return address

    def __getitem__(self, item: str) -> Any:
        return self.__getattribute__(item)

    def __setitem__(self, item: str, value: Any) -> None:
        self.__setattr__(item, value)

    def metadata(self) -> str:
        if self.type == v23c.SOURCE_ECU:
            keys = (
                "id",
                "block_len",
                "type",
                "module_nr",
                "module_address",
                "description",
                "ECU_identification",
                "reserved0",
            )
        else:
            keys = (
                "id",
                "block_len",
                "type",
                "CAN_id",
                "CAN_ch_index",
                "message_name",
                "sender_name",
                "reserved0",
            )
        max_len = max(len(key) for key in keys)
        template = f"{{: <{max_len}}}: {{}}"

        metadata = []
        lines = f"""
address: {hex(self.address)}

""".split(
            "\n"
        )

        for key in keys:
            val = getattr(self, key)
            if key.endswith("addr") or key.startswith("text_"):
                lines.append(template.format(key, hex(val)))
            elif isinstance(val, float):
                lines.append(template.format(key, round(val, 6)))
            else:
                if isinstance(val, bytes):
                    lines.append(template.format(key, val.strip(b"\0")))
                else:
                    lines.append(template.format(key, val))

            if key == "type":
                lines[-1] += f" = {v23c.SOURCE_TYPE_TO_STRING[self.type]}"
            elif key == "bus_type":
                lines[-1] += f" = {v23c.BUS_TYPE_TO_STRING[self.bus_type]}"

        for line in lines:
            if not line:
                metadata.append(line)
            else:
                for wrapped_line in wrap(line, width=120):
                    metadata.append(wrapped_line)

        return "\n".join(metadata)

    def __bytes__(self) -> bytes:
        typ = self.type
        if typ == v23c.SOURCE_ECU:
            return v23c.SOURCE_ECU_p(
                self.id,
                self.block_len,
                self.type,
                self.module_nr,
                self.module_address,
                self.description,
                self.ECU_identification,
                self.reserved0,
            )
        else:
            return v23c.SOURCE_VECTOR_p(
                self.id,
                self.block_len,
                self.type,
                self.CAN_id,
                self.CAN_ch_index,
                self.message_name,
                self.sender_name,
                self.reserved0,
            )

    def __str__(self) -> str:
        fields = []
        for attr in dir(self):
            if attr[:2] + attr[-2:] == "____":
                continue
            try:
                if callable(getattr(self, attr)):
                    continue
                fields.append(f"{attr}:{getattr(self, attr)}")
            except AttributeError:
                continue
        return f"ChannelExtension (name: {self.name}, path: {self.path}, comment: {self.comment}, address: {hex(self.address)}, fields: {fields})" class HeaderBlock:
    """HDBLOCK class

    HDBLOCK fields

    * ``id`` - bytes : block ID; always b'HD'
    * ``block_len`` - int : block bytes size
    * ``first_dg_addr`` - int : address of first data group block (DGBLOCK)
    * ``comment_addr`` - int : address of TXBLOCK that contains the
      measurement file comment
    * ``program_addr`` - int : address of program block (PRBLOCK)
    * ``dg_nr`` - int : number of data groups
    * ``date`` - bytes : date at which the recording was started in
      "DD:MM:YYYY" format
    * ``time`` - btyes : time at which the recording was started in
      "HH:MM:SS" format
    * ``author_field`` - bytes : author name
    * ``organization_field``` - bytes : organization name
    * ``project_field``` - bytes : project name
    * ``subject_field``` - bytes : subject

    Since version 3.2 the following extra keys were added

    * ``abs_time`` - int : time stamp at which recording was started in
      nanoseconds.
    * ``tz_offset`` - int : UTC time offset in hours (= GMT time zone)
    * ``time_quality`` - int : time quality class
    * ``timer_identification`` - bytes : timer identification (time source)

    Other attributes

    * ``address`` - int : block address inside mdf file; should always be 64
    * ``comment`` - int : file comment
    * ``program`` - ProgramBlock : program block
    * ``author`` - str : measurement author
    * ``department`` - str : author department
    * ``project`` - str : working project
    * ``subject`` - str : measurement subject

    Parameters
    ----------
    stream : file handle
        mdf file handle
    version : int
        mdf version in case of new file (dynamically created)

    """

    __slots__ = (
        "address",
        "program",
        "comment",
        "id",
        "block_len",
        "first_dg_addr",
        "comment_addr",
        "program_addr",
        "dg_nr",
        "date",
        "time",
        "author",
        "department",
        "project",
        "subject",
        "abs_time",
        "tz_offset",
        "time_quality",
        "timer_identification",
        "author_field",
        "department_field",
        "project_field",
        "subject_field",
    )

    def __init__(self, **kwargs) -> None:
        super().__init__()

        self.address = 64
        self.program = None
        self.comment = ""
        try:

            stream = kwargs["stream"]
            stream.seek(64)

            (
                self.id,
                self.block_len,
                self.first_dg_addr,
                self.comment_addr,
                self.program_addr,
                self.dg_nr,
                self.date,
                self.time,
                self.author_field,
                self.department_field,
                self.project_field,
                self.subject_field,
            ) = unpack(v23c.HEADER_COMMON_FMT, stream.read(v23c.HEADER_COMMON_SIZE))

            if self.block_len > v23c.HEADER_COMMON_SIZE:
                (
                    self.abs_time,
                    self.tz_offset,
                    self.time_quality,
                    self.timer_identification,
                ) = unpack(
                    v23c.HEADER_POST_320_EXTRA_FMT,
                    stream.read(v23c.HEADER_POST_320_EXTRA_SIZE),
                )

            if self.id != b"HD":
                message = (
                    f'Expected "HD" block @{hex(self.address)} but found "{self.id}"'
                )
                message = message.format(hex(64), self.id)
                logger.exception(message)
                raise MdfException(message)

            if self.program_addr:
                self.program = ProgramBlock(address=self.program_addr, stream=stream)
            if self.comment_addr:
                self.comment = get_text_v3(address=self.comment_addr, stream=stream)

        except KeyError:
            version = kwargs.get("version", "3.20")
            self.id = b"HD"
            self.block_len = 208 if version >= "3.20" else 164
            self.first_dg_addr = 0
            self.comment_addr = 0
            self.program_addr = 0
            self.dg_nr = 0
            try:
                user = getuser()
            except ModuleNotFoundError:
                user = ""
            self.author_field = "{:\0<32}".format(user).encode("latin-1")
            self.department_field = "{:\0<32}".format("").encode("latin-1")
            self.project_field = "{:\0<32}".format("").encode("latin-1")
            self.subject_field = "{:\0<32}".format("").encode("latin-1")

            if self.block_len > v23c.HEADER_COMMON_SIZE:
                self.time_quality = 0
                self.timer_identification = "{:\0<32}".format(
                    "Local PC Reference Time"
                ).encode("latin-1")

            self.start_time = datetime(1980, 1, 1)

        self.author = self.author_field.strip(b" \r\n\t\0").decode("latin-1")
        self.department = self.department_field.strip(b" \r\n\t\0").decode("latin-1")
        self.project = self.project_field.strip(b" \r\n\t\0").decode("latin-1")
        self.subject = self.subject_field.strip(b" \r\n\t\0").decode("latin-1")

    def to_blocks(
        self,
        address: int,
        blocks: list[Any],
        defined_texts: dict[str, int],
        si_map: dict[bytes, int],
    ) -> int:
        blocks.append(self)
        self.address = address
        address += self.block_len

        key = "comment_addr"
        text = self.comment
        if text:
            if text in defined_texts:
                self[key] = defined_texts[text]
            else:
                tx_block = TextBlock(text=text)
                self[key] = address
                defined_texts[text] = address
                tx_block.address = address
                address += tx_block.block_len
                blocks.append(tx_block)
        else:
            self[key] = 0

        key = "program_addr"
        if self.program:
            self[key] = address
            address += self.program.block_len
            blocks.append(self.program)

        else:
            self[key] = 0

        self.author_field = self.author.encode("latin-1")
        self.department_field = self.department.encode("latin-1")
        self.project_field = self.project.encode("latin-1")
        self.subject_field = self.subject.encode("latin-1")

        return address

    @property
    def start_time(self) -> datetime:
        """getter and setter the measurement start timestamp

        Returns
        -------
        timestamp : datetime.datetime
            start timestamp

        """

        if self.block_len > v23c.HEADER_COMMON_SIZE:
            if self.abs_time:
                timestamp = self.abs_time / 10**9
                try:
                    timestamp = datetime.fromtimestamp(timestamp)
                except OSError:
                    timestamp = datetime.now()
            else:
                timestamp = "{} {}".format(
                    self.date.decode("ascii"), self.time.decode("ascii")
                )

                timestamp = datetime.strptime(timestamp, "%d:%m:%Y %H:%M:%S")

        else:
            timestamp = "{} {}".format(
                self.date.decode("ascii"), self.time.decode("ascii")
            )

            timestamp = datetime.strptime(timestamp, "%d:%m:%Y %H:%M:%S")

        return timestamp

    @start_time.setter
    def start_time(self, timestamp: datetime) -> None:
        self.date = timestamp.strftime("%d:%m:%Y").encode("ascii")
        self.time = timestamp.strftime("%H:%M:%S").encode("ascii")
        if self.block_len > v23c.HEADER_COMMON_SIZE:
            timestamp = int(timestamp.timestamp() * 10**9)
            self.abs_time = timestamp
            self.tz_offset = 0

    def __getitem__(self, item: str) -> Any:
        return self.__getattribute__(item)

    def __setitem__(self, item: str, value: Any) -> None:
        self.__setattr__(item, value)

    def __bytes__(self) -> bytes:
        fmt = v23c.HEADER_COMMON_FMT
        keys = v23c.HEADER_COMMON_KEYS
        if self.block_len > v23c.HEADER_COMMON_SIZE:
            fmt += v23c.HEADER_POST_320_EXTRA_FMT
            keys += v23c.HEADER_POST_320_EXTRA_KEYS
        result = pack(fmt, *[self[key] for key in keys])
        return result class ChannelConversion(_ChannelConversionBase):
    """*ChannelConversion* has the following attributes, that are also available as
    dict like key-value pairs

    CCBLOCK common fields

    * ``id`` - bytes : block ID; always b'##CG'
    * ``reserved0`` - int : reserved bytes
    * ``block_len`` - int : block bytes size
    * ``links_nr`` - int : number of links
    * ``name_addr`` - int : address of TXBLOCK that contains the
      conversion name
    * ``unit_addr`` - int : address of TXBLOCK that contains the
      conversion unit
    * ``comment_addr`` - int : address of TXBLOCK/MDBLOCK that contains the
      conversion comment
    * ``inv_conv_addr`` int : address of inverse conversion
    * ``conversion_type`` int : integer code for conversion type
    * ``precision`` - int : integer code for precision
    * ``flags`` - int : conversion block flags
    * ``ref_param_nr`` - int : number fo referenced parameters (linked
      parameters)
    * ``val_param_nr`` - int : number of value parameters
    * ``min_phy_value`` - float : minimum physical channel value
    * ``max_phy_value`` - float : maximum physical channel value

    CCBLOCK specific fields

    * linear conversion

        * ``a`` - float : factor
        * ``b`` - float : offset

    * rational conversion

        * ``P1`` to ``P6`` - float : parameters

    * algebraic conversion

        * ``formula_addr`` - address of TXBLOCK that contains
          the algebraic conversion formula

    * tabular conversion with or without interpolation

        * ``raw_<N>`` - float : N-th raw value
        * ``phys_<N>`` - float : N-th physical value

    * tabular range conversion

        * ``lower_<N>`` - float : N-th lower value
        * ``upper_<N>`` - float : N-th upper value
        * ``phys_<N>`` - float : N-th physical value

    * tabular value to text conversion

        * ``val_<N>`` - float : N-th raw value
        * ``text_<N>`` - int : address of N-th TXBLOCK that
          contains the physical value
        * ``default`` - int : address of TXBLOCK that contains
          the default physical value

    * tabular range to text conversion

        * ``lower_<N>`` - float : N-th lower value
        * ``upper_<N>`` - float : N-th upper value
        * ``text_<N>`` - int : address of N-th TXBLOCK that
          contains the physical value
        * ``default`` - int : address of TXBLOCK that contains
          the default physical value

    * text to value conversion

        * ``val_<N>`` - float : N-th physical value
        * ``text_<N>`` - int : address of N-th TXBLOCK that
          contains the raw value
        * ``val_default`` - float : default physical value

    * text transformation (translation) conversion

        * ``input_<N>_addr`` - int : address of N-th TXBLOCK that
          contains the raw value
        * ``output_<N>_addr`` - int : address of N-th TXBLOCK that
          contains the physical value
        * ``default_addr`` - int : address of TXBLOCK that contains
          the default physical value

    Other attributes

    * ``address`` - int : channel conversion address
    * ``comment`` - str : channel conversion comment
    * ``formula`` - str : algebraic conversion formula; default ''
    * ``referenced_blocks`` - dict : dict of referenced blocks; can be TextBlock
      objects for value to text, and text to text conversions; for partial
      conversions the referenced blocks can be ChannelConversion object as well
    * ``name`` - str : channel conversion name
    * ``unit`` - str : channel conversion unit

    """

    def __init__(self, **kwargs) -> None:

        if "stream" in kwargs:
            stream = kwargs["stream"]
            mapped = kwargs["mapped"]

            try:
                self.address = address = kwargs["address"]
                block = kwargs["raw_bytes"]
                (self.id, self.reserved0, self.block_len, self.links_nr) = COMMON_uf(
                    block
                )

                if self.id != b"##CC":
                    message = (
                        f'Expected "##CC" block @{hex(address)} but found "{self.id}"'
                    )
                    logger.exception(message)
                    raise MdfException(message)

                block = block[COMMON_SIZE:]

            except KeyError:
                self.address = address = kwargs["address"]
                stream.seek(address)

                (self.id, self.reserved0, self.block_len, self.links_nr) = COMMON_u(
                    stream.read(COMMON_SIZE)
                )

                if self.id != b"##CC":
                    message = (
                        f'Expected "##CC" block @{hex(address)} but found "{self.id}"'
                    )
                    logger.exception(message)
                    raise MdfException(message)

                block = stream.read(self.block_len - COMMON_SIZE)

            (conv,) = UINT8_uf(block, self.links_nr * 8)

            if conv == v4c.CONVERSION_TYPE_NON:
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                ) = v4c.CONVERSION_NONE_INIT_u(block)

            elif conv == v4c.CONVERSION_TYPE_LIN:
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                    self.b,
                    self.a,
                ) = v4c.CONVERSION_LINEAR_INIT_u(block)

            elif conv == v4c.CONVERSION_TYPE_RAT:
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                    self.P1,
                    self.P2,
                    self.P3,
                    self.P4,
                    self.P5,
                    self.P6,
                ) = unpack(v4c.FMT_CONVERSION_RAT_INIT, block)

            elif conv == v4c.CONVERSION_TYPE_ALG:
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                    self.formula_addr,
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                ) = unpack(v4c.FMT_CONVERSION_ALGEBRAIC_INIT, block)

            elif conv in (v4c.CONVERSION_TYPE_TABI, v4c.CONVERSION_TYPE_TAB):
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                ) = unpack_from(v4c.FMT_CONVERSION_NONE_INIT, block)

                nr = self.val_param_nr
                values = unpack_from(f"<{nr}d", block, 56)
                for i in range(nr // 2):
                    self[f"raw_{i}"], self[f"phys_{i}"] = (
                        values[i * 2],
                        values[2 * i + 1],
                    )

            elif conv == v4c.CONVERSION_TYPE_RTAB:
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                ) = unpack_from(v4c.FMT_CONVERSION_NONE_INIT, block)
                nr = self.val_param_nr
                values = unpack_from(f"<{nr}d", block, 56)
                for i in range((nr - 1) // 3):
                    (self[f"lower_{i}"], self[f"upper_{i}"], self[f"phys_{i}"]) = (
                        values[i * 3],
                        values[3 * i + 1],
                        values[3 * i + 2],
                    )
                (self.default,) = FLOAT64_u(block[-8:])

            elif conv == v4c.CONVERSION_TYPE_TABX:
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                ) = unpack_from("<4Q", block)

                links_nr = self.links_nr - 4

                links = unpack_from(f"<{links_nr}Q", block, 32)
                for i, link in enumerate(links[:-1]):
                    self[f"text_{i}"] = link
                self.default_addr = links[-1]

                (
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                ) = unpack_from("<2B3H2d", block, 32 + links_nr * 8)

                values = unpack_from(f"<{links_nr - 1}d", block, 32 + links_nr * 8 + 24)
                for i, val in enumerate(values):
                    self[f"val_{i}"] = val

            elif conv == v4c.CONVERSION_TYPE_RTABX:
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                ) = unpack_from("<4Q", block)

                links_nr = self.links_nr - 4

                links = unpack_from(f"<{links_nr}Q", block, 32)
                for i, link in enumerate(links[:-1]):
                    self[f"text_{i}"] = link
                self.default_addr = links[-1]

                (
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                ) = unpack_from("<2B3H2d", block, 32 + links_nr * 8)

                values = unpack_from(
                    f"<{self.val_param_nr}d", block, 32 + links_nr * 8 + 24
                )
                self.default_lower = self.default_upper = 0
                for i in range(self.val_param_nr // 2):
                    j = 2 * i
                    self[f"lower_{i}"] = values[j]
                    self[f"upper_{i}"] = values[j + 1]

            elif conv == v4c.CONVERSION_TYPE_TTAB:
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                ) = unpack_from("<4Q", block)

                links_nr = self.links_nr - 4

                links = unpack_from(f"<{links_nr}Q", block, 32)
                for i, link in enumerate(links):
                    self[f"text_{i}"] = link

                (
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                ) = unpack_from("<2B3H2d", block, 32 + links_nr * 8)

                values = unpack_from(
                    f"<{self.val_param_nr}d", block, 32 + links_nr * 8 + 24
                )
                for i, val in enumerate(values[:-1]):
                    self[f"val_{i}"] = val
                self.val_default = values[-1]

            elif conv == v4c.CONVERSION_TYPE_TRANS:
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                ) = unpack_from("<4Q", block)

                links_nr = self.links_nr - 4

                links = unpack_from(f"<{links_nr}Q", block, 32)

                for i in range((links_nr - 1) // 2):
                    j = 2 * i
                    self[f"input_{i}_addr"] = links[j]
                    self[f"output_{i}_addr"] = links[j + 1]
                self.default_addr = links[-1]

                (
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                ) = unpack_from("<2B3H2d", block, 32 + links_nr * 8)

            elif conv == v4c.CONVERSION_TYPE_BITFIELD:
                (
                    self.name_addr,
                    self.unit_addr,
                    self.comment_addr,
                    self.inv_conv_addr,
                ) = unpack_from("<4Q", block)

                links_nr = self.links_nr - 4

                links = unpack_from(f"<{links_nr}Q", block, 32)
                for i, link in enumerate(links):
                    self[f"text_{i}"] = link

                (
                    self.conversion_type,
                    self.precision,
                    self.flags,
                    self.ref_param_nr,
                    self.val_param_nr,
                    self.min_phy_value,
                    self.max_phy_value,
                ) = unpack_from("<2B3H2d", block, 32 + links_nr * 8)

                values = unpack_from(
                    f"<{self.val_param_nr}Q", block, 32 + links_nr * 8 + 24
                )
                for i, val in enumerate(values):
                    self[f"mask_{i}"] = val

            self.referenced_blocks = None

            tx_map = kwargs["tx_map"]

            addr = self.name_addr
            if addr in tx_map:
                self.name = tx_map[addr]
            else:
                self.name = get_text_v4(addr, stream, mapped=mapped)
                tx_map[addr] = self.name

            addr = self.unit_addr
            if addr in tx_map:
                self.unit = tx_map[addr]
            else:
                self.unit = get_text_v4(addr, stream, mapped=mapped)
                tx_map[addr] = self.unit

            if isinstance(self.unit, bytes):
                self.unit = self.unit.decode("utf-8", errors="ignore")

            addr = self.comment_addr
            if addr in tx_map:
                self.comment = tx_map[addr]
            else:
                self.comment = get_text_v4(addr, stream, mapped=mapped)
                tx_map[addr] = self.comment

            conv_type = conv

            if conv_type == v4c.CONVERSION_TYPE_ALG:
                self.formula = get_text_v4(
                    self.formula_addr, stream, mapped=mapped
                ).replace("x", "X")
            else:
                self.formula = ""

                if conv_type in v4c.TABULAR_CONVERSIONS:
                    refs = self.referenced_blocks = {}
                    if conv_type in (
                        v4c.CONVERSION_TYPE_TTAB,
                        v4c.CONVERSION_TYPE_BITFIELD,
                    ):
                        tabs = self.links_nr - 4
                    else:
                        tabs = self.links_nr - 4 - 1
                    for i in range(tabs):
                        address = self[f"text_{i}"]
                        if address:
                            if address in tx_map:
                                txt = tx_map[address]
                                if not isinstance(txt, bytes):
                                    txt = txt.encode("utf-8")
                                refs[f"text_{i}"] = txt
                            else:
                                stream.seek(address)
                                _id = stream.read(4)

                                if _id == b"##TX":
                                    block = get_text_v4(
                                        address=address,
                                        stream=stream,
                                        mapped=mapped,
                                        decode=False,
                                    )
                                    tx_map[address] = block
                                    refs[f"text_{i}"] = block
                                elif _id == b"##CC":
                                    block = ChannelConversion(
                                        address=address,
                                        stream=stream,
                                        mapped=mapped,
                                        tx_map=tx_map,
                                    )
                                    refs[f"text_{i}"] = block
                                else:
                                    message = f'Expected "##TX" or "##CC" block @{hex(address)} but found "{_id}"'
                                    logger.exception(message)
                                    raise MdfException(message)

                        else:
                            refs[f"text_{i}"] = b""
                    if conv_type not in (
                        v4c.CONVERSION_TYPE_TTAB,
                        v4c.CONVERSION_TYPE_BITFIELD,
                    ):
                        address = self.default_addr
                        if address:
                            if address in tx_map:
                                txt = tx_map[address] or b""
                                if not isinstance(txt, bytes):
                                    txt = txt.encode("utf-8")
                                refs["default_addr"] = txt
                            else:
                                stream.seek(address)
                                _id = stream.read(4)

                                if _id == b"##TX":
                                    block = get_text_v4(
                                        address=address,
                                        stream=stream,
                                        mapped=mapped,
                                        decode=False,
                                    )
                                    tx_map[address] = block
                                    refs["default_addr"] = block
                                elif _id == b"##CC":
                                    block = ChannelConversion(
                                        address=address,
                                        stream=stream,
                                        mapped=mapped,
                                        tx_map=tx_map,
                                    )
                                    refs["default_addr"] = block
                                else:
                                    message = f'Expected "##TX" or "##CC" block @{hex(address)} but found "{_id}"'
                                    logger.exception(message)
                                    raise MdfException(message)
                        else:
                            refs["default_addr"] = b""

                elif conv_type == v4c.CONVERSION_TYPE_TRANS:
                    refs = self.referenced_blocks = {}
                    # link_nr - common links (4) - default text link (1)
                    for i in range((self.links_nr - 4 - 1) // 2):
                        for key in (f"input_{i}_addr", f"output_{i}_addr"):
                            address = self[key]

                            if address:
                                if address in tx_map:
                                    txt = tx_map[address] or b""
                                    if not isinstance(txt, bytes):
                                        txt = txt.encode("utf-8")
                                    refs[key] = txt
                                else:
                                    block = get_text_v4(
                                        address=address,
                                        stream=stream,
                                        mapped=mapped,
                                        decode=False,
                                    )
                                    tx_map[address] = block
                                    refs[key] = block
                            else:
                                refs[key] = b""
                    address = self.default_addr
                    if address:
                        if address in tx_map:
                            txt = tx_map[address] or b""
                            if not isinstance(txt, bytes):
                                txt = txt.encode("utf-8")
                            refs["default_addr"] = txt
                        else:
                            block = get_text_v4(
                                address=address,
                                stream=stream,
                                mapped=mapped,
                                decode=False,
                            )
                            refs["default_addr"] = block
                            tx_map[address] = block
                    else:
                        refs["default_addr"] = b""

        else:

            self.name = kwargs.get("name", "")
            self.unit = kwargs.get("unit", "")
            self.comment = kwargs.get("comment", "")
            self.formula = kwargs.get("formula", "")
            self.referenced_blocks = None

            self.address = 0
            self.id = b"##CC"
            self.reserved0 = 0

            if kwargs["conversion_type"] == v4c.CONVERSION_TYPE_NON:
                self.block_len = v4c.CC_NONE_BLOCK_SIZE
                self.links_nr = 4
                self.name_addr = kwargs.get("name_addr", 0)
                self.unit_addr = kwargs.get("unit_addr", 0)
                self.comment_addr = kwargs.get("comment_addr", 0)
                self.inv_conv_addr = 0
                self.conversion_type = v4c.CONVERSION_TYPE_NON
                self.precision = 1
                self.flags = 0
                self.ref_param_nr = 0
                self.val_param_nr = 0
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)

            elif kwargs["conversion_type"] == v4c.CONVERSION_TYPE_LIN:
                self.block_len = v4c.CC_LIN_BLOCK_SIZE
                self.links_nr = 4
                self.name_addr = kwargs.get("name_addr", 0)
                self.unit_addr = kwargs.get("unit_addr", 0)
                self.comment_addr = kwargs.get("comment_addr", 0)
                self.inv_conv_addr = kwargs.get("inv_conv_addr", 0)
                self.conversion_type = v4c.CONVERSION_TYPE_LIN
                self.precision = kwargs.get("precision", 1)
                self.flags = kwargs.get("flags", 0)
                self.ref_param_nr = 0
                self.val_param_nr = 2
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                self.b = float(kwargs["b"])
                self.a = float(kwargs["a"])

            elif kwargs["conversion_type"] == v4c.CONVERSION_TYPE_ALG:
                self.block_len = v4c.CC_ALG_BLOCK_SIZE
                self.links_nr = 5
                self.name_addr = kwargs.get("name_addr", 0)
                self.unit_addr = kwargs.get("unit_addr", 0)
                self.comment_addr = kwargs.get("comment_addr", 0)
                self.inv_conv_addr = kwargs.get("inv_conv_addr", 0)
                self.formula_addr = kwargs.get("formula_addr", 0)
                self.conversion_type = v4c.CONVERSION_TYPE_ALG
                self.precision = kwargs.get("precision", 1)
                self.flags = kwargs.get("flags", 0)
                self.ref_param_nr = 1
                self.val_param_nr = 0
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)

            elif kwargs["conversion_type"] in (
                v4c.CONVERSION_TYPE_TAB,
                v4c.CONVERSION_TYPE_TABI,
            ):

                nr = kwargs["val_param_nr"]

                self.block_len = 80 + 8 * nr
                self.links_nr = 4
                self.name_addr = kwargs.get("name_addr", 0)
                self.unit_addr = kwargs.get("unit_addr", 0)
                self.comment_addr = kwargs.get("comment_addr", 0)
                self.inv_conv_addr = kwargs.get("inv_conv_addr", 0)
                self.conversion_type = kwargs["conversion_type"]
                self.precision = kwargs.get("precision", 1)
                self.flags = kwargs.get("flags", 0)
                self.ref_param_nr = 0
                self.val_param_nr = nr
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)

                for i in range(nr // 2):
                    self[f"raw_{i}"] = kwargs[f"raw_{i}"]
                    self[f"phys_{i}"] = kwargs[f"phys_{i}"]

            elif kwargs["conversion_type"] == v4c.CONVERSION_TYPE_RTAB:
                self.block_len = kwargs["val_param_nr"] * 8 + 80
                self.links_nr = 4
                self.name_addr = kwargs.get("name_addr", 0)
                self.unit_addr = kwargs.get("unit_addr", 0)
                self.comment_addr = kwargs.get("comment_addr", 0)
                self.inv_conv_addr = kwargs.get("inv_conv_addr", 0)
                self.conversion_type = v4c.CONVERSION_TYPE_RTAB
                self.precision = kwargs.get("precision", 0)
                self.flags = kwargs.get("flags", 0)
                self.ref_param_nr = 0
                self.val_param_nr = kwargs["val_param_nr"]
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                for i in range((kwargs["val_param_nr"] - 1) // 3):
                    self[f"lower_{i}"] = kwargs[f"lower_{i}"]
                    self[f"upper_{i}"] = kwargs[f"upper_{i}"]
                    self[f"phys_{i}"] = kwargs[f"phys_{i}"]
                self.default = kwargs["default"]

            elif kwargs["conversion_type"] == v4c.CONVERSION_TYPE_RAT:

                self.block_len = 80 + 6 * 8
                self.links_nr = 4
                self.name_addr = kwargs.get("name_addr", 0)
                self.unit_addr = kwargs.get("unit_addr", 0)
                self.comment_addr = kwargs.get("comment_addr", 0)
                self.inv_conv_addr = kwargs.get("inv_conv_addr", 0)
                self.conversion_type = kwargs["conversion_type"]
                self.precision = kwargs.get("precision", 1)
                self.flags = kwargs.get("flags", 0)
                self.ref_param_nr = 0
                self.val_param_nr = kwargs.get("val_param_nr", 6)
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)

                for i in range(1, 7):
                    self[f"P{i}"] = kwargs[f"P{i}"]

            elif kwargs["conversion_type"] == v4c.CONVERSION_TYPE_TABX:
                self.referenced_blocks = {}
                nr = kwargs["ref_param_nr"] - 1
                self.block_len = (nr * 8 * 2) + 88
                self.links_nr = nr + 5
                self.name_addr = kwargs.get("name_addr", 0)
                self.unit_addr = kwargs.get("unit_addr", 0)
                self.comment_addr = kwargs.get("comment_addr", 0)
                self.inv_conv_addr = kwargs.get("inv_conv_addr", 0)
                for i in range(nr):
                    key = f"text_{i}"
                    self[key] = 0
                    self.referenced_blocks[key] = kwargs[key]
                self.default_addr = 0
                key = "default_addr"
                if "default_addr" in kwargs:
                    default = kwargs["default_addr"]
                else:
                    default = kwargs.get("default", b"")
                self.referenced_blocks[key] = default
                self.conversion_type = v4c.CONVERSION_TYPE_TABX
                self.precision = kwargs.get("precision", 0)
                self.flags = kwargs.get("flags", 0)
                self.ref_param_nr = nr + 1
                self.val_param_nr = nr
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                for i in range(nr):
                    self[f"val_{i}"] = kwargs[f"val_{i}"]

            elif kwargs["conversion_type"] == v4c.CONVERSION_TYPE_RTABX:
                self.referenced_blocks = {}
                nr = kwargs["ref_param_nr"] - 1
                self.block_len = (nr * 8 * 3) + 88
                self.links_nr = nr + 5
                self.name_addr = kwargs.get("name_addr", 0)
                self.unit_addr = kwargs.get("unit_addr", 0)
                self.comment_addr = kwargs.get("comment_addr", 0)
                self.inv_conv_addr = kwargs.get("inv_conv_addr", 0)
                for i in range(nr):
                    key = f"text_{i}"
                    self[key] = 0
                    self.referenced_blocks[key] = kwargs[key]
                self.default_addr = 0
                self.default_lower = self.default_upper = 0
                if "default_addr" in kwargs:
                    default = kwargs["default_addr"]
                else:
                    default = kwargs.get("default", b"")
                if isinstance(default, bytes) and b"{X}" in default:
                    default = (
                        default.decode("latin-1").replace("{X}", "X").split('"')[1]
                    )
                    default = ChannelConversion(
                        conversion_type=v4c.CONVERSION_TYPE_ALG, formula=default
                    )
                    self.referenced_blocks["default_addr"] = default
                else:
                    self.referenced_blocks["default_addr"] = default
                self.conversion_type = v4c.CONVERSION_TYPE_RTABX
                self.precision = kwargs.get("precision", 0)
                self.flags = kwargs.get("flags", 0)
                self.ref_param_nr = nr + 1
                self.val_param_nr = nr * 2
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                for i in range(nr):
                    self[f"lower_{i}"] = kwargs[f"lower_{i}"]
                    self[f"upper_{i}"] = kwargs[f"upper_{i}"]

            elif kwargs["conversion_type"] == v4c.CONVERSION_TYPE_TTAB:
                self.block_len = ((kwargs["links_nr"] - 4) * 8 * 2) + 88
                self.links_nr = kwargs["links_nr"]
                self.name_addr = kwargs.get("name_addr", 0)
                self.unit_addr = kwargs.get("unit_addr", 0)
                self.comment_addr = kwargs.get("comment_addr", 0)
                self.inv_conv_addr = kwargs.get("inv_conv_addr", 0)
                for i in range(kwargs["links_nr"] - 4):
                    self[f"text_{i}"] = kwargs.get(f"text_{i}", 0)
                self.conversion_type = v4c.CONVERSION_TYPE_TTAB
                self.precision = kwargs.get("precision", 0)
                self.flags = kwargs.get("flags", 0)
                self.ref_param_nr = kwargs["links_nr"] - 4
                self.val_param_nr = kwargs["links_nr"] - 4 + 1
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                for i in range(kwargs["links_nr"] - 4):
                    self[f"val_{i}"] = kwargs[f"val_{i}"]
                self.val_default = kwargs["val_default"]

            elif kwargs["conversion_type"] == v4c.CONVERSION_TYPE_BITFIELD:
                self.referenced_blocks = {}
                nr = kwargs["val_param_nr"]
                self.block_len = (nr * 8 * 2) + 80
                self.links_nr = nr + 4
                self.name_addr = kwargs.get("name_addr", 0)
                self.unit_addr = kwargs.get("unit_addr", 0)
                self.comment_addr = kwargs.get("comment_addr", 0)
                self.inv_conv_addr = kwargs.get("inv_conv_addr", 0)
                for i in range(nr):
                    key = f"text_{i}"
                    self[key] = 0
                    self.referenced_blocks[key] = kwargs[key]

                self.conversion_type = v4c.CONVERSION_TYPE_BITFIELD
                self.precision = kwargs.get("precision", 0)
                self.flags = kwargs.get("flags", 0)
                self.ref_param_nr = nr
                self.val_param_nr = nr
                self.min_phy_value = kwargs.get("min_phy_value", 0)
                self.max_phy_value = kwargs.get("max_phy_value", 0)
                for i in range(nr):
                    self[f"mask_{i}"] = kwargs[f"mask_{i}"]

            else:
                message = "Conversion {} dynamic creation not implemented"
                message = message.format(kwargs["conversion_type"])
                logger.exception(message)
                raise MdfException(message)

    def to_blocks(
        self,
        address: int,
        blocks: list[Any],
        defined_texts: dict[str, int],
        cc_map: dict[bytes, int],
    ) -> int:
        if id(self) in cc_map:
            return address

        text = self.name
        if text:
            if text in defined_texts:
                self.name_addr = defined_texts[text]
            else:
                tx_block = TextBlock(
                    text=text.encode("utf-8", "replace"),
                    meta=False,
                    safe=True,
                )
                self.name_addr = address
                defined_texts[text] = address
                tx_block.address = address
                address += tx_block.block_len
                blocks.append(tx_block)
        else:
            self.name_addr = 0

        text = self.unit
        if text:
            if text in defined_texts:
                self.unit_addr = defined_texts[text]
            else:
                tx_block = TextBlock(
                    text=text.encode("utf-8", "replace"),
                    meta=False,
                    safe=True,
                )
                self.unit_addr = address
                defined_texts[text] = address
                tx_block.address = address
                address += tx_block.block_len
                blocks.append(tx_block)
        else:
            self.unit_addr = 0

        if self.conversion_type == v4c.CONVERSION_TYPE_ALG:
            text = self.formula
            if text:
                if text in defined_texts:
                    self.formula_addr = defined_texts[text]
                else:
                    tx_block = TextBlock(
                        text=text.encode("utf-8", "replace"),
                        meta=False,
                        safe=True,
                    )
                    self.formula_addr = address
                    defined_texts[text] = address
                    tx_block.address = address
                    address += tx_block.block_len
                    blocks.append(tx_block)
            else:
                self.formula_addr = 0

        text = self.comment
        if text:
            if text in defined_texts:
                self.comment_addr = defined_texts[text]
            else:
                meta = text.startswith("<CCcomment")
                tx_block = TextBlock(
                    text=text.encode("utf-8", "replace"),
                    meta=meta,
                    safe=True,
                )
                self.comment_addr = address
                defined_texts[text] = address
                tx_block.address = address
                address += tx_block.block_len
                blocks.append(tx_block)
        else:
            self.comment_addr = 0

        if self.referenced_blocks:
            for key, block in self.referenced_blocks.items():
                if block:
                    if isinstance(block, ChannelConversion):
                        address = block.to_blocks(
                            address, blocks, defined_texts, cc_map
                        )
                        self[key] = block.address
                    else:
                        text = block
                        if text in defined_texts:
                            self[key] = defined_texts[text]
                        else:
                            block = TextBlock(text=text)
                            defined_texts[text] = address
                            blocks.append(block)
                            self[key] = address
                            address += block["block_len"]

                else:
                    self[key] = 0

        bts = bytes(self)
        if bts in cc_map:
            self.address = cc_map[bts]
        else:
            blocks.append(bts)
            self.address = address
            cc_map[bts] = cc_map[id(self)] = address
            address += self.block_len

        return address

    def convert(self, values):
        if not isinstance(values, np.ndarray):
            values = np.array(values)
        conversion_type = self.conversion_type
        if conversion_type == v4c.CONVERSION_TYPE_NON:
            pass
        elif conversion_type == v4c.CONVERSION_TYPE_LIN:
            a = self.a
            b = self.b

            if (a, b) != (1, 0):

                if values.dtype.names:
                    names = values.dtype.names
                    name = names[0]
                    vals = values[name]

                    vals = vals * a
                    if b:
                        vals += b
                    values = np.core.records.fromarrays(
                        [vals] + [values[name] for name in names[1:]],
                        dtype=[(name, vals.dtype, vals.shape[1:])]
                        + [
                            (name, values[name].dtype, values[name].shape[1:])
                            for name in names[1:]
                        ],
                    )

                else:

                    values = values * a
                    if b:
                        values += b

        elif conversion_type == v4c.CONVERSION_TYPE_RAT:
            P1 = self.P1
            P2 = self.P2
            P3 = self.P3
            P4 = self.P4
            P5 = self.P5
            P6 = self.P6

            names = values.dtype.names
            if names:
                name = names[0]
                vals = values[name]
                if (P1, P4, P5, P6) == (0, 0, 0, 1):
                    if (P2, P3) != (1, 0):
                        vals = values[name] * P2
                        if P3:
                            vals = vals + P3

                elif (P3, P4, P5, P6) == (0, 0, 1, 0):
                    if (P1, P2) != (1, 0):
                        vals = values[name] * P1
                        if P2:
                            vals = vals + P2

                else:
                    X = vals
                    try:
                        vals = evaluate(v4c.CONV_RAT_TEXT)
                    except TypeError:
                        vals = (P1 * X**2 + P2 * X + P3) / (P4 * X**2 + P5 * X + P6)

                values = np.core.records.fromarrays(
                    [vals] + [values[name] for name in names[1:]],
                    dtype=[(name, vals.dtype, vals.shape[1:])]
                    + [
                        (name, values[name].dtype, values[name].shape[1:])
                        for name in names[1:]
                    ],
                )

            else:
                X = values
                if (P1, P4, P5, P6) == (0, 0, 0, 1):
                    if (P2, P3) != (1, 0):
                        values = values * P2
                        if P3:
                            values = values + P3
                elif (P3, P4, P5, P6) == (0, 0, 1, 0):
                    if (P1, P2) != (1, 0):
                        values = values * P1
                        if P2:
                            values += P2
                else:
                    try:
                        values = evaluate(v4c.CONV_RAT_TEXT)
                    except TypeError:
                        values = (P1 * X**2 + P2 * X + P3) / (
                            P4 * X**2 + P5 * X + P6
                        )

        elif conversion_type == v4c.CONVERSION_TYPE_ALG:
            X = values
            try:
                values = evaluate(self.formula.replace("X1", "X"))
            except:
                values = evaluate3(self.formula.replace("X1", "X"))

        elif conversion_type in (v4c.CONVERSION_TYPE_TABI, v4c.CONVERSION_TYPE_TAB):
            nr = self.val_param_nr // 2
            raw_vals = np.array([self[f"raw_{i}"] for i in range(nr)])
            phys = np.array([self[f"phys_{i}"] for i in range(nr)])

            if conversion_type == v4c.CONVERSION_TYPE_TABI:
                values = np.interp(values, raw_vals, phys)
            else:

                dim = raw_vals.shape[0]

                inds = np.searchsorted(raw_vals, values)

                inds[inds >= dim] = dim - 1

                inds2 = inds - 1
                inds2[inds2 < 0] = 0

                cond = np.abs(values - raw_vals[inds]) >= np.abs(
                    values - raw_vals[inds2]
                )

                values = np.where(cond, phys[inds2], phys[inds])

        elif conversion_type == v4c.CONVERSION_TYPE_RTAB:
            nr = (self.val_param_nr - 1) // 3
            lower = np.array([self[f"lower_{i}"] for i in range(nr)])
            upper = np.array([self[f"upper_{i}"] for i in range(nr)])
            phys = np.array([self[f"phys_{i}"] for i in range(nr)])
            default = self.default

            if values.dtype.kind == "f":
                idx1 = np.searchsorted(lower, values, side="right") - 1
                idx2 = np.searchsorted(upper, values, side="right")
            else:
                idx1 = np.searchsorted(lower, values, side="right") - 1
                idx2 = np.searchsorted(upper, values, side="right") - 1

            idx_ne = np.nonzero(idx1 != idx2)[0]
            idx_eq = np.nonzero(idx1 == idx2)[0]

            new_values = np.zeros(len(values), dtype=phys.dtype)

            if len(idx_ne):
                new_values[idx_ne] = default
            if len(idx_eq):
                new_values[idx_eq] = phys[idx1[idx_eq]]

            values = new_values

        elif conversion_type == v4c.CONVERSION_TYPE_TABX:
            nr = self.val_param_nr
            raw_vals = [self[f"val_{i}"] for i in range(nr)]

            phys = [self.referenced_blocks[f"text_{i}"] for i in range(nr)]

            default = self.referenced_blocks["default_addr"]

            x = sorted(zip(raw_vals, phys))
            raw_vals = np.array([e[0] for e in x], dtype="<i8")
            phys = [e[1] for e in x]

            names = values.dtype.names

            if names:
                name = names[0]
                vals = values[name]
                shape = vals.shape
                vals = vals.flatten()

                ret = np.full(len(vals), None, "O")

                idx1 = np.searchsorted(raw_vals, vals, side="right") - 1
                idx2 = np.searchsorted(raw_vals, vals, side="left")

                idx = np.argwhere(idx1 != idx2).flatten()

                if isinstance(default, bytes):
                    ret[idx] = default
                else:
                    ret[idx] = default.convert(vals[idx])

                idx = np.argwhere(idx1 == idx2).ravel()
                if idx.size:
                    indexes = idx1[idx]
                    unique = np.unique(indexes)
                    for val in unique:

                        item = phys[val]
                        idx_ = np.argwhere(indexes == val).flatten()
                        if isinstance(item, bytes):
                            ret[idx[idx_]] = item
                        else:
                            ret[idx[idx_]] = item.convert(vals[idx[idx_]])

                all_bytes = True
                for v in ret.tolist():
                    if not isinstance(v, bytes):
                        all_bytes = False
                        break

                if not all_bytes:
                    try:
                        ret = ret.astype("f8")
                    except:
                        ret = np.array(
                            [np.nan if isinstance(v, bytes) else v for v in ret]
                        )

                else:
                    ret = ret.astype(bytes)

                ret = ret.reshape(shape)
                values = np.core.records.fromarrays(
                    [ret] + [values[name] for name in names[1:]],
                    dtype=[(name, ret.dtype, ret.shape[1:])]
                    + [
                        (name, values[name].dtype, values[name].shape[1:])
                        for name in names[1:]
                    ],
                )
            else:

                ret = np.full(values.size, None, "O")

                idx1 = np.searchsorted(raw_vals, values, side="right") - 1
                idx2 = np.searchsorted(raw_vals, values, side="left")

                idx = np.argwhere(idx1 != idx2).ravel()

                if idx.size:
                    # some raw values were not found in the conversion table
                    # so the default physical value must be returned

                    if isinstance(default, bytes):
                        ret[idx] = default
                    else:
                        ret[idx] = default.convert(values[idx])

                    idx = np.argwhere(idx1 == idx2).ravel()

                    if idx.size:
                        indexes = idx1[idx]
                        unique = np.unique(indexes)
                        for val in unique.tolist():

                            item = phys[val]
                            idx_ = np.argwhere(indexes == val).ravel()
                            if isinstance(item, bytes):
                                ret[idx[idx_]] = item
                            else:
                                ret[idx[idx_]] = item.convert(values[idx[idx_]])

                else:
                    # all the raw values are found in the conversion table

                    if idx1.size:
                        unique = np.unique(idx1)
                        for val in unique.tolist():

                            item = phys[val]
                            idx_ = np.argwhere(idx1 == val).ravel()
                            if isinstance(item, bytes):
                                ret[idx_] = item
                            else:
                                ret[idx_] = item.convert(values[idx_])

                try:
                    ret = ret.astype(bytes)
                except:
                    try:
                        ret = ret.astype("<f8")
                    except:
                        ret = np.array(
                            [
                                np.nan if isinstance(v, bytes) else v
                                for v in ret.tolist()
                            ]
                        )

                values = ret

        elif conversion_type == v4c.CONVERSION_TYPE_RTABX:
            nr = self.val_param_nr // 2

            phys = [self.referenced_blocks[f"text_{i}"] for i in range(nr)]

            default = self.referenced_blocks["default_addr"]

            lower = [self[f"lower_{i}"] for i in range(nr)]
            upper = [self[f"upper_{i}"] for i in range(nr)]

            x = sorted(zip(lower, upper, phys))
            lower = np.array([e[0] for e in x], dtype="<i8")
            upper = np.array([e[1] for e in x], dtype="<i8")
            phys = [e[2] for e in x]

            ret = np.full(values.size, None, "O")

            idx1 = np.searchsorted(lower, values, side="right") - 1
            idx2 = np.searchsorted(upper, values, side="left")

            idx_ne = np.argwhere(idx1 != idx2).ravel()
            idx_eq = np.argwhere(idx1 == idx2).ravel()

            if isinstance(default, bytes):
                ret[idx_ne] = default
            else:
                ret[idx_ne] = default.convert(values[idx_ne])

            if idx_eq.size:
                indexes = idx1[idx_eq]
                unique = np.unique(indexes)
                for val in unique:

                    item = phys[val]
                    idx_ = np.argwhere(indexes == val).flatten()

                    if isinstance(item, bytes):
                        ret[idx_eq[idx_]] = item
                    else:
                        try:
                            ret[idx_eq[idx_]] = item.convert(values[idx_eq[idx_]])
                        except:
                            print(self)
                            raise

            try:
                ret = ret.astype(bytes)
            except:
                try:
                    ret = ret.astype("<f8")
                except:
                    ret = np.array([np.nan if isinstance(v, bytes) else v for v in ret])

            values = ret

        elif conversion_type == v4c.CONVERSION_TYPE_TTAB:
            nr = self.val_param_nr - 1

            raw_values = [self.referenced_blocks[f"text_{i}"] for i in range(nr)]
            phys = [self[f"val_{i}"] for i in range(nr)]
            default = self.val_default

            new_values = []
            for val in values:
                try:
                    val = phys[raw_values.index(val)]
                except ValueError:
                    val = default
                new_values.append(val)

            values = np.array(new_values)

        elif conversion_type == v4c.CONVERSION_TYPE_TRANS:
            nr = (self.ref_param_nr - 1) // 2

            in_ = [self.referenced_blocks[f"input_{i}_addr"] for i in range(nr)]

            out_ = [self.referenced_blocks[f"output_{i}_addr"] for i in range(nr)]
            default = self.referenced_blocks["default_addr"]

            new_values = []
            for val in values:
                try:
                    val = out_[in_.index(val.strip(b"\0"))]
                except ValueError:
                    val = default
                new_values.append(val)

            values = np.array(new_values)

        elif conversion_type == v4c.CONVERSION_TYPE_BITFIELD:
            nr = self.val_param_nr

            phys = [self.referenced_blocks[f"text_{i}"] for i in range(nr)]
            masks = np.array([self[f"mask_{i}"] for i in range(nr)], dtype="u8")

            phys = [
                conv
                if isinstance(conv, bytes)
                else (
                    (f"{conv.name}=".encode("utf-8"), conv)
                    if conv.name
                    else (b"", conv)
                )
                for conv in phys
            ]

            new_values = []
            values = values.astype("u8").tolist()
            for val in values:
                new_val = []
                masked_values = (masks & val).tolist()

                for on, conv in zip(masked_values, phys):
                    if not on:
                        continue

                    if isinstance(conv, bytes):
                        if conv:
                            new_val.append(conv)
                    else:
                        prefix, conv = conv
                        converted_val = conv.convert([on])
                        if converted_val:
                            if prefix:
                                new_val.append(prefix + converted_val)
                            else:
                                new_val.append(converted_val)

                new_values.append(b"|".join(new_val))

            values = np.array(new_values)

        return values

    def metadata(self, indent: str = "") -> str:
        if self.conversion_type == v4c.CONVERSION_TYPE_NON:
            keys = v4c.KEYS_CONVERSION_NONE
        elif self.conversion_type == v4c.CONVERSION_TYPE_LIN:
            keys = v4c.KEYS_CONVERSION_LINEAR
        elif self.conversion_type == v4c.CONVERSION_TYPE_RAT:
            keys = v4c.KEYS_CONVERSION_RAT
        elif self.conversion_type == v4c.CONVERSION_TYPE_ALG:
            keys = v4c.KEYS_CONVERSION_ALGEBRAIC
        elif self.conversion_type in (
            v4c.CONVERSION_TYPE_TABI,
            v4c.CONVERSION_TYPE_TAB,
        ):
            keys = v4c.KEYS_CONVERSION_NONE
            for i in range(self.val_param_nr // 2):
                keys += (f"raw_{i}", f"phys_{i}")
        elif self.conversion_type == v4c.CONVERSION_TYPE_RTAB:
            keys = v4c.KEYS_CONVERSION_NONE
            for i in range(self.val_param_nr // 3):
                keys += (f"lower_{i}", f"upper_{i}", f"phys_{i}")
            keys += ("default",)
        elif self.conversion_type == v4c.CONVERSION_TYPE_TABX:
            keys = (
                "id",
                "reserved0",
                "block_len",
                "links_nr",
                "name_addr",
                "unit_addr",
                "comment_addr",
                "inv_conv_addr",
            )
            keys += tuple(f"text_{i}" for i in range(self.links_nr - 4 - 1))
            keys += ("default_addr",)
            keys += (
                "conversion_type",
                "precision",
                "flags",
                "ref_param_nr",
                "val_param_nr",
                "min_phy_value",
                "max_phy_value",
            )
            keys += tuple(f"val_{i}" for i in range(self.val_param_nr))
        elif self.conversion_type == v4c.CONVERSION_TYPE_RTABX:
            keys = (
                "id",
                "reserved0",
                "block_len",
                "links_nr",
                "name_addr",
                "unit_addr",
                "comment_addr",
                "inv_conv_addr",
            )
            keys += tuple(f"text_{i}" for i in range(self.links_nr - 4 - 1))
            keys += ("default_addr",)
            keys += (
                "conversion_type",
                "precision",
                "flags",
                "ref_param_nr",
                "val_param_nr",
                "min_phy_value",
                "max_phy_value",
            )
            for i in range(self.val_param_nr // 2):
                keys += (f"lower_{i}", f"upper_{i}")
        elif self.conversion_type == v4c.CONVERSION_TYPE_TTAB:
            keys = (
                "id",
                "reserved0",
                "block_len",
                "links_nr",
                "name_addr",
                "unit_addr",
                "comment_addr",
                "inv_conv_addr",
            )
            keys += tuple(f"text_{i}" for i in range(self.links_nr - 4))
            keys += (
                "conversion_type",
                "precision",
                "flags",
                "ref_param_nr",
                "val_param_nr",
                "min_phy_value",
                "max_phy_value",
            )
            keys += tuple(f"val_{i}" for i in range(self.val_param_nr - 1))
            keys += ("val_default",)
        elif self.conversion_type == v4c.CONVERSION_TYPE_TRANS:
            keys = (
                "id",
                "reserved0",
                "block_len",
                "links_nr",
                "name_addr",
                "unit_addr",
                "comment_addr",
                "inv_conv_addr",
            )
            for i in range((self.links_nr - 4 - 1) // 2):
                keys += (f"input_{i}_addr", f"output_{i}_addr")
            keys += (
                "default_addr",
                "conversion_type",
                "precision",
                "flags",
                "ref_param_nr",
                "val_param_nr",
                "min_phy_value",
                "max_phy_value",
            )
            keys += tuple(f"val_{i}" for i in range(self.val_param_nr - 1))

        elif self.conversion_type == v4c.CONVERSION_TYPE_BITFIELD:
            keys = (
                "id",
                "reserved0",
                "block_len",
                "links_nr",
                "name_addr",
                "unit_addr",
                "comment_addr",
                "inv_conv_addr",
            )
            keys += tuple(f"text_{i}" for i in range(self.links_nr - 4))
            keys += (
                "conversion_type",
                "precision",
                "flags",
                "ref_param_nr",
                "val_param_nr",
                "min_phy_value",
                "max_phy_value",
            )
            keys += tuple(f"mask_{i}" for i in range(self.val_param_nr))

        max_len = max(len(key) for key in keys)
        template = f"{{: <{max_len}}}: {{}}"

        metadata = []
        lines = f"""
name: {self.name}
unit: {self.unit}
address: {hex(self.address)}
comment: {self.comment}
formula: {self.formula}

""".split(
            "\n"
        )
        for key in keys:
            val = getattr(self, key)
            if key.endswith("addr") or key.startswith("text_"):
                lines.append(template.format(key, hex(val)))
            elif isinstance(val, float):
                lines.append(template.format(key, round(val, 6)))
            else:
                if isinstance(val, bytes):
                    lines.append(template.format(key, val.strip(b"\0")))
                else:
                    lines.append(template.format(key, val))

            if key == "conversion_type":
                lines[-1] += f" = {v4c.CONVERSION_TYPE_TO_STRING[self.conversion_type]}"
            elif self.referenced_blocks and key in self.referenced_blocks:
                val = self.referenced_blocks[key]
                if isinstance(val, bytes):
                    lines[-1] += f" (= {str(val)[1:]})"
                else:
                    lines[-1] += f" (= CCBLOCK @ {hex(val.address)})"

        if self.referenced_blocks:
            max_len = max(len(key) for key in self.referenced_blocks)
            template = f"{{: <{max_len}}}: {{}}"

            lines.append("")
            lines.append("Referenced blocks:")
            for key, block in self.referenced_blocks.items():
                if isinstance(block, ChannelConversion):
                    lines.append(template.format(key, ""))
                    lines.extend(block.metadata(indent + "    ").split("\n"))
                else:
                    lines.append(template.format(key, str(block)[1:]))

        for line in lines:
            if not line:
                metadata.append(line)
            else:
                for wrapped_line in wrap(
                    line, initial_indent=indent, subsequent_indent=indent, width=120
                ):
                    metadata.append(wrapped_line)

        return "\n".join(metadata)

    def __getitem__(self, item: str) -> Any:
        return self.__getattribute__(item)
        try:
            return self.__getattribute__(item)
        except:
            print(self)
            raise

    def __setitem__(self, item: str, value: Any) -> None:
        self.__setattr__(item, value)

    def __contains__(self, item: str) -> bool:
        return hasattr(self, item)

    def __bytes__(self) -> bytes:
        if self.conversion_type == v4c.CONVERSION_TYPE_NON:
            result = v4c.CONVERSION_NONE_PACK(
                self.id,
                self.reserved0,
                self.block_len,
                self.links_nr,
                self.name_addr,
                self.unit_addr,
                self.comment_addr,
                self.inv_conv_addr,
                self.conversion_type,
                self.precision,
                self.flags,
                self.ref_param_nr,
                self.val_param_nr,
                self.min_phy_value,
                self.max_phy_value,
            )
        elif self.conversion_type == v4c.CONVERSION_TYPE_LIN:
            result = v4c.CONVERSION_LINEAR_PACK(
                self.id,
                self.reserved0,
                self.block_len,
                self.links_nr,
                self.name_addr,
                self.unit_addr,
                self.comment_addr,
                self.inv_conv_addr,
                self.conversion_type,
                self.precision,
                self.flags,
                self.ref_param_nr,
                self.val_param_nr,
                self.min_phy_value,
                self.max_phy_value,
                self.b,
                self.a,
            )
        elif self.conversion_type == v4c.CONVERSION_TYPE_RAT:
            result = v4c.CONVERSION_RAT_PACK(
                self.id,
                self.reserved0,
                self.block_len,
                self.links_nr,
                self.name_addr,
                self.unit_addr,
                self.comment_addr,
                self.inv_conv_addr,
                self.conversion_type,
                self.precision,
                self.flags,
                self.ref_param_nr,
                self.val_param_nr,
                self.min_phy_value,
                self.max_phy_value,
                self.P1,
                self.P2,
                self.P3,
                self.P4,
                self.P5,
                self.P6,
            )
        elif self.conversion_type == v4c.CONVERSION_TYPE_ALG:
            result = v4c.CONVERSION_ALGEBRAIC_PACK(
                self.id,
                self.reserved0,
                self.block_len,
                self.links_nr,
                self.name_addr,
                self.unit_addr,
                self.comment_addr,
                self.inv_conv_addr,
                self.formula_addr,
                self.conversion_type,
                self.precision,
                self.flags,
                self.ref_param_nr,
                self.val_param_nr,
                self.min_phy_value,
                self.max_phy_value,
            )
        elif self.conversion_type in (
            v4c.CONVERSION_TYPE_TABI,
            v4c.CONVERSION_TYPE_TAB,
        ):
            fmt = "<4sI{}Q2B3H{}d".format(self.links_nr + 2, self.val_param_nr + 2)
            keys = v4c.KEYS_CONVERSION_NONE
            for i in range(self.val_param_nr // 2):
                keys += (f"raw_{i}", f"phys_{i}")
            result = pack(fmt, *[getattr(self, key) for key in keys])
        elif self.conversion_type == v4c.CONVERSION_TYPE_RTAB:
            fmt = "<4sI{}Q2B3H{}d".format(self.links_nr + 2, self.val_param_nr + 2)
            keys = v4c.KEYS_CONVERSION_NONE
            for i in range(self.val_param_nr // 3):
                keys += (f"lower_{i}", f"upper_{i}", f"phys_{i}")
            keys += ("default",)
            result = pack(fmt, *[getattr(self, key) for key in keys])
        elif self.conversion_type == v4c.CONVERSION_TYPE_TABX:
            fmt = "<4sI{}Q2B3H{}d".format(self.links_nr + 2, self.val_param_nr + 2)
            keys = (
                "id",
                "reserved0",
                "block_len",
                "links_nr",
                "name_addr",
                "unit_addr",
                "comment_addr",
                "inv_conv_addr",
            )
            keys += tuple(f"text_{i}" for i in range(self.links_nr - 4 - 1))
            keys += ("default_addr",)
            keys += (
                "conversion_type",
                "precision",
                "flags",
                "ref_param_nr",
                "val_param_nr",
                "min_phy_value",
                "max_phy_value",
            )
            keys += tuple(f"val_{i}" for i in range(self.val_param_nr))
            result = pack(fmt, *[getattr(self, key) for key in keys])
        elif self.conversion_type == v4c.CONVERSION_TYPE_RTABX:
            fmt = "<4sI{}Q2B3H{}d".format(self.links_nr + 2, self.val_param_nr + 2)
            keys = (
                "id",
                "reserved0",
                "block_len",
                "links_nr",
                "name_addr",
                "unit_addr",
                "comment_addr",
                "inv_conv_addr",
            )
            keys += tuple(f"text_{i}" for i in range(self.links_nr - 4 - 1))
            keys += ("default_addr",)
            keys += (
                "conversion_type",
                "precision",
                "flags",
                "ref_param_nr",
                "val_param_nr",
                "min_phy_value",
                "max_phy_value",
            )
            for i in range(self.val_param_nr // 2):
                keys += (f"lower_{i}", f"upper_{i}")
            result = pack(fmt, *[getattr(self, key) for key in keys])
        elif self.conversion_type == v4c.CONVERSION_TYPE_TTAB:
            fmt = "<4sI{}Q2B3H{}d".format(self.links_nr + 2, self.val_param_nr + 2)
            keys = (
                "id",
                "reserved0",
                "block_len",
                "links_nr",
                "name_addr",
                "unit_addr",
                "comment_addr",
                "inv_conv_addr",
            )
            keys += tuple(f"text_{i}" for i in range(self.links_nr - 4))
            keys += (
                "conversion_type",
                "precision",
                "flags",
                "ref_param_nr",
                "val_param_nr",
                "min_phy_value",
                "max_phy_value",
            )
            keys += tuple(f"val_{i}" for i in range(self.val_param_nr - 1))
            keys += ("val_default",)
            result = pack(fmt, *[getattr(self, key) for key in keys])
        elif self.conversion_type == v4c.CONVERSION_TYPE_TRANS:
            fmt = "<4sI{}Q2B3H{}d".format(self.links_nr + 2, self.val_param_nr + 2)
            keys = (
                "id",
                "reserved0",
                "block_len",
                "links_nr",
                "name_addr",
                "unit_addr",
                "comment_addr",
                "inv_conv_addr",
            )
            for i in range((self.links_nr - 4 - 1) // 2):
                keys += (f"input_{i}_addr", f"output_{i}_addr")
            keys += (
                "default_addr",
                "conversion_type",
                "precision",
                "flags",
                "ref_param_nr",
                "val_param_nr",
                "min_phy_value",
                "max_phy_value",
            )
            keys += tuple(f"val_{i}" for i in range(self.val_param_nr - 1))

            result = pack(fmt, *[getattr(self, key) for key in keys])

        elif self.conversion_type == v4c.CONVERSION_TYPE_BITFIELD:
            fmt = "<4sI{}Q2B3H2d{}Q".format(self.links_nr + 2, self.val_param_nr)
            keys = (
                "id",
                "reserved0",
                "block_len",
                "links_nr",
                "name_addr",
                "unit_addr",
                "comment_addr",
                "inv_conv_addr",
            )
            keys += tuple(f"text_{i}" for i in range(self.val_param_nr))
            keys += (
                "conversion_type",
                "precision",
                "flags",
                "ref_param_nr",
                "val_param_nr",
                "min_phy_value",
                "max_phy_value",
            )
            keys += tuple(f"mask_{i}" for i in range(self.val_param_nr))
            result = pack(fmt, *[getattr(self, key) for key in keys])

        return result

    def __str__(self) -> str:
        return "<ChannelConversion (name: {}, unit: {}, comment: {}, formula: {}, referenced blocks: {}, address: {}, fields: {})>".format(
            self.name,
            self.unit,
            self.comment,
            self.formula,
            self.referenced_blocks,
            self.address,
            block_fields(self),
        ) class EventBlock(_EventBlockBase):
    """
    *EventBlock* has the following attributes, that are also available as
    dict like key-value pairs

    EVBLOCK fields

    * ``id`` - bytes : block ID; always b'##EV'
    * ``reserved0`` - int : reserved bytes
    * ``block_len`` - int : block bytes size
    * ``links_nr`` - int : number of links
    * ``next_ev_addr`` - int : address of next EVBLOCK
    * ``parent_ev_addr`` - int : address of parent EVLBOCK
    * ``range_start_ev_addr`` - int : address of EVBLOCK that is the start of
      the range for which this event is the end
    * ``name_addr`` - int : address of TXBLOCK that contains the event name
    * ``comment_addr`` - int : address of TXBLOCK/MDBLOCK that contains the
      event comment
    * ``scope_<N>_addr`` - int : address of N-th block that represents a scope
      for this event (can be CGBLOCK, CHBLOCK, DGBLOCK)
    * ``attachemnt_<N>_addr`` - int : address of N-th attachment referenced by
      this event
    * ``event_type`` - int : integer code for event type
    * ``sync_type`` - int : integer code for event sync type
    * ``range_type`` - int : integer code for event range type
    * ``cause`` - int : integer code for event cause
    * ``flags`` - int : event flags
    * ``reserved1`` - int : reserved bytes
    * ``scope_nr`` - int : number of scopes referenced by this event
    * ``attachment_nr`` - int : number of attachments referenced by this event
    * ``creator_index`` - int : index of FHBLOCK
    * ``sync_base`` - int : timestamp base value
    * ``sync_factor`` - float : timestamp factor

    Other attributes

    * ``address`` - int : event block address
    * ``comment`` - str : event comment
    * ``name`` - str : event name
    * ``parent`` - int : index of event block that is the parent for the
      current event
    * ``range_start`` - int : index of event block that is the start of the
      range for which the current event is the end
    * ``scopes`` - list : list of (group index, channel index) or channel group
      index that define the scope of the current event

    """

    def __init__(self, **kwargs) -> None:

        self.name = self.comment = self.group_name = ""
        self.scopes = []
        self.parent = None
        self.range_start = None

        if "stream" in kwargs:

            self.address = address = kwargs["address"]
            stream = kwargs["stream"]
            stream.seek(address)

            (self.id, self.reserved0, self.block_len, self.links_nr) = COMMON_u(
                stream.read(COMMON_SIZE)
            )

            block = stream.read(self.block_len - COMMON_SIZE)

            links_nr = self.links_nr

            links = unpack_from(f"<{links_nr}Q", block)
            params = unpack_from(v4c.FMT_EVENT_PARAMS, block, links_nr * 8)

            (
                self.next_ev_addr,
                self.parent_ev_addr,
                self.range_start_ev_addr,
                self.name_addr,
                self.comment_addr,
            ) = links[:5]

            scope_nr = params[6]
            for i in range(scope_nr):
                self[f"scope_{i}_addr"] = links[5 + i]

            attachment_nr = params[7]
            for i in range(attachment_nr):
                self[f"attachment_{i}_addr"] = links[5 + scope_nr + i]

            (
                self.event_type,
                self.sync_type,
                self.range_type,
                self.cause,
                self.flags,
                self.reserved1,
                self.scope_nr,
                self.attachment_nr,
                self.creator_index,
                self.sync_base,
                self.sync_factor,
            ) = params

            if self.flags & v4c.FLAG_EV_GROUP_NAME:
                self.group_name_addr = links[-1]

            if self.id != b"##EV":
                message = f'Expected "##EV" block @{hex(address)} but found "{self.id}"'

                logger.exception(message)
                raise MdfException(message)

            self.name = get_text_v4(self.name_addr, stream)
            self.comment = get_text_v4(self.comment_addr, stream)

        else:
            self.address = 0

            scopes = 0
            while f"scope_{scopes}_addr" in kwargs:
                scopes += 1

            self.id = b"##EV"
            self.reserved0 = 0
            self.block_len = 56 + (scopes + 5) * 8
            self.links_nr = scopes + 5
            self.next_ev_addr = kwargs.get("next_ev_addr", 0)
            self.parent_ev_addr = kwargs.get("parent_ev_addr", 0)
            self.range_start_ev_addr = kwargs.get("range_start_ev_addr", 0)
            self.name_addr = kwargs.get("name_addr", 0)
            self.comment_addr = kwargs.get("comment_addr", 0)

            for i in range(scopes):
                self[f"scope_{i}_addr"] = kwargs[f"scope_{i}_addr"]

            self.event_type = kwargs.get("event_type", v4c.EVENT_TYPE_TRIGGER)
            self.sync_type = kwargs.get("sync_type", v4c.EVENT_SYNC_TYPE_S)
            self.range_type = kwargs.get("range_type", v4c.EVENT_RANGE_TYPE_POINT)
            self.cause = kwargs.get("cause", v4c.EVENT_CAUSE_TOOL)
            self.flags = kwargs.get("flags", v4c.FLAG_EV_POST_PROCESSING)
            self.reserved1 = b"\x00\x00\x00"
            self.scope_nr = scopes
            self.attachment_nr = 0
            self.creator_index = 0
            self.sync_base = kwargs.get("sync_base", 1)
            self.sync_factor = kwargs.get("sync_factor", 1.0)

            if self.flags & v4c.FLAG_EV_GROUP_NAME:
                self.group_name_addr = kwargs.get("group_name_addr", 0)

    def update_references(self, ch_map, cg_map):
        self.scopes.clear()
        for i in range(self.scope_nr):
            addr = self[f"scope_{i}_addr"]
            if addr in ch_map:
                self.scopes.append(ch_map[addr])
            elif addr in cg_map:
                self.scopes.append(cg_map[addr])
            else:
                message = (
                    "{} is not a valid CNBLOCK or CGBLOCK "
                    "address for the event scope"
                )
                message = message.format(hex(addr))
                logger.exception(message)
                raise MdfException(message)

    def __bytes__(self) -> bytes:

        fmt = v4c.FMT_EVENT.format(self.links_nr)

        keys = (
            "id",
            "reserved0",
            "block_len",
            "links_nr",
            "next_ev_addr",
            "parent_ev_addr",
            "range_start_ev_addr",
            "name_addr",
            "comment_addr",
        )

        keys += tuple(f"scope_{i}_addr" for i in range(self.scope_nr))

        keys += tuple(f"attachment_{i}_addr" for i in range(self.attachment_nr))

        if self.flags & v4c.FLAG_EV_GROUP_NAME:
            keys += ("group_name_addr",)

        keys += (
            "event_type",
            "sync_type",
            "range_type",
            "cause",
            "flags",
            "reserved1",
            "scope_nr",
            "attachment_nr",
            "creator_index",
            "sync_base",
            "sync_factor",
        )
        result = pack(fmt, *[getattr(self, key) for key in keys])

        return result

    def __getitem__(self, item: str) -> Any:
        return self.__getattribute__(item)

    def __setitem__(self, item: str, value: Any) -> None:
        self.__setattr__(item, value)

    def __str__(self) -> str:
        return "EventBlock (name: {}, comment: {}, address: {}, scopes: {}, fields: {})".format(
            self.name, self.comment, hex(self.address), self.scopes, super().__str__()
        )

    @property
    def value(self):
        return self.sync_base * self.sync_factor

    @value.setter
    def value(self, val) -> None:
        self.sync_factor = val / self.sync_base

    def to_blocks(self, address: int, blocks: list[Any]) -> int:
        text = self.name
        if text:
            tx_block = TextBlock(text=text)
            self.name_addr = address
            tx_block.address = address
            address += tx_block.block_len
            blocks.append(tx_block)
        else:
            self.name_addr = 0

        text = self.comment
        if text:
            tx_block = TextBlock(text=text)
            self.comment_addr = address
            tx_block.address = address
            address += tx_block.block_len
            blocks.append(tx_block)
        else:
            self.comment_addr = 0

        if self.flags & v4c.FLAG_EV_GROUP_NAME:
            text = self.group_name
            if text:
                tx_block = TextBlock(text=text)
                self.group_name_addr = address
                tx_block.address = address
                address += tx_block.block_len
                blocks.append(tx_block)
            else:
                self.group_name_addr = 0

        blocks.append(self)
        self.address = address
        address += self.block_len

        return address class FileHistory:
    """
    *FileHistory* has the following attributes, that are also available as
    dict like key-value pairs

    FHBLOCK fields

    * ``id`` - bytes : block ID; always b'##FH'
    * ``reserved0`` - int : reserved bytes
    * ``block_len`` - int : block bytes size
    * ``links_nr`` - int : number of links
    * ``next_fh_addr`` - int : address of next FHBLOCK
    * ``comment_addr`` - int : address of TXBLOCK/MDBLOCK that contains the
      file history comment
    * ``abs_time`` - int : time stamp at which the file modification happened
    * ``tz_offset`` - int : UTC time offset in hours (= GMT time zone)
    * ``daylight_save_time`` - int : daylight saving time
    * ``time_flags`` - int : time flags
    * ``reserved1`` - bytes : reserved bytes

    Other attributes

    * ``address`` - int : file history address
    * ``comment`` - str : history comment

    """

    __slots__ = (
        "address",
        "comment",
        "id",
        "reserved0",
        "block_len",
        "links_nr",
        "next_fh_addr",
        "comment_addr",
        "abs_time",
        "tz_offset",
        "daylight_save_time",
        "time_flags",
        "reserved1",
    )

    def __init__(self, **kwargs) -> None:
        super().__init__()

        self.comment = ""

        try:
            self.address = address = kwargs["address"]
            stream = kwargs["stream"]
            stream.seek(address)

            (
                self.id,
                self.reserved0,
                self.block_len,
                self.links_nr,
                self.next_fh_addr,
                self.comment_addr,
                self.abs_time,
                self.tz_offset,
                self.daylight_save_time,
                self.time_flags,
                self.reserved1,
            ) = unpack(v4c.FMT_FILE_HISTORY, stream.read(v4c.FH_BLOCK_SIZE))

            if self.id != b"##FH":
                message = f'Expected "##FH" block @{hex(address)} but found "{self.id}"'

                logger.exception(message)
                raise MdfException(message)

            self.comment = get_text_v4(address=self.comment_addr, stream=stream)

        except KeyError:
            self.id = b"##FH"
            self.reserved0 = kwargs.get("reserved0", 0)
            self.block_len = kwargs.get("block_len", v4c.FH_BLOCK_SIZE)
            self.links_nr = kwargs.get("links_nr", 2)
            self.next_fh_addr = kwargs.get("next_fh_addr", 0)
            self.comment_addr = kwargs.get("comment_addr", 0)
            self.abs_time = kwargs.get("abs_time", int(time.time()) * 10**9)
            self.tz_offset = kwargs.get("tz_offset", 120)
            self.daylight_save_time = kwargs.get("daylight_save_time", 60)
            self.time_flags = kwargs.get("time_flags", 2)
            self.reserved1 = kwargs.get("reserved1", b"\x00" * 3)

    def to_blocks(
        self, address: int, blocks: list[Any], defined_texts: dict[str, int]
    ) -> int:
        text = self.comment
        if text:
            if text in defined_texts:
                self.comment_addr = defined_texts[text]
            else:
                meta = text.startswith("<FHcomment")
                tx_block = TextBlock(text=text, meta=meta)
                self.comment_addr = address
                defined_texts[text] = address
                tx_block.address = address
                address += tx_block.block_len
                blocks.append(tx_block)
        else:
            self.comment_addr = 0

        blocks.append(self)
        self.address = address
        address += self.block_len

        return address

    def __getitem__(self, item: str) -> Any:
        return self.__getattribute__(item)

    def __setitem__(self, item: str, value: Any) -> None:
        self.__setattr__(item, value)

    def __bytes__(self) -> bytes:
        result = pack(
            v4c.FMT_FILE_HISTORY, *[self[key] for key in v4c.KEYS_FILE_HISTORY]
        )
        return result class FileIdentificationBlock:
    """
    *FileIdentificationBlock* has the following attributes, that are also available as
    dict like key-value pairs

    IDBLOCK fields

    * ``file_identification`` -  bytes : file identifier
    * ``version_str`` - bytes : format identifier
    * ``program_identification`` - bytes : creator program identifier
    * ``reserved0`` - bytes : reserved bytes
    * ``mdf_version`` - int : version number of MDF format
    * ``reserved1`` - bytes : reserved bytes
    * ``unfinalized_standard_flags`` - int : standard flags for unfinalized MDF
    * ``unfinalized_custom_flags`` - int : custom flags for unfinalized MDF

    Other attributes

    * ``address`` - int : should always be 0

    """

    __slots__ = (
        "address",
        "file_identification",
        "version_str",
        "program_identification",
        "reserved0",
        "mdf_version",
        "reserved1",
        "unfinalized_standard_flags",
        "unfinalized_custom_flags",
    )

    def __init__(self, **kwargs) -> None:

        super().__init__()

        self.address = 0

        try:

            stream = kwargs["stream"]
            stream.seek(self.address)

            (
                self.file_identification,
                self.version_str,
                self.program_identification,
                self.reserved0,
                self.mdf_version,
                self.reserved1,
                self.unfinalized_standard_flags,
                self.unfinalized_custom_flags,
            ) = unpack(
                v4c.FMT_IDENTIFICATION_BLOCK, stream.read(v4c.IDENTIFICATION_BLOCK_SIZE)
            )

        except KeyError:

            version = kwargs.get("version", "4.00")
            self.file_identification = "MDF     ".encode("utf-8")
            self.version_str = "{}    ".format(version).encode("utf-8")
            self.program_identification = "amdf{}".format(
                __version__.replace(".", "")
            ).encode("utf-8")
            self.reserved0 = b"\0" * 4
            self.mdf_version = int(version.replace(".", ""))
            self.reserved1 = b"\0" * 30
            self.unfinalized_standard_flags = 0
            self.unfinalized_custom_flags = 0

    def __getitem__(self, item: str) -> Any:
        return self.__getattribute__(item)

    def __setitem__(self, item: str, value: Any) -> None:
        self.__setattr__(item, value)

    def __bytes__(self) -> bytes:
        result = pack(
            v4c.FMT_IDENTIFICATION_BLOCK,
            *[self[key] for key in v4c.KEYS_IDENTIFICATION_BLOCK],
        )
        return result class HeaderBlock:
    """
    *HeaderBlock* has the following attributes, that are also available as
    dict like key-value pairs

    HDBLOCK fields

    * ``id`` - bytes : block ID; always b'##HD'
    * ``reserved0`` - int : reserved bytes
    * ``block_len`` - int : block bytes size
    * ``links_nr`` - int : number of links
    * ``first_dg_addr`` - int : address of first DGBLOCK
    * ``file_history_addr`` - int : address of first FHBLOCK
    * ``channel_tree_addr`` - int : address of first CHBLOCK
    * ``first_attachment_addr`` - int : address of first ATBLOCK
    * ``first_event_addr`` - int : address of first EVBLOCK
    * ``comment_addr`` - int : address of TXBLOCK/MDBLOCK that contains the
      file comment
    * ``abs_time`` - int : time stamp at which recording was started in
      nanoseconds.
    * ``tz_offset`` - int : UTC time offset in hours (= GMT time zone)
    * ``daylight_save_time`` - int : daylight saving time
    * ``time_flags`` - int : time flags
    * ``time_quality`` - int : time quality flags
    * ``flags`` - int : file flags
    * ``reserved1`` - int : reserved bytes
    * ``start_angle`` - int : angle value at measurement start
    * ``start_distance`` - int : distance value at measurement start

    Other attributes

    * ``address`` - int : header address
    * ``comment`` - str : file comment
    * ``author`` - str : measurement author
    * ``department`` - str : author's department
    * ``project`` - str : working project
    * ``subject`` - str : measurement subject

    """

    __slots__ = (
        "address",
        "comment",
        "author",
        "department",
        "project",
        "subject",
        "id",
        "reserved0",
        "block_len",
        "links_nr",
        "first_dg_addr",
        "file_history_addr",
        "channel_tree_addr",
        "first_attachment_addr",
        "first_event_addr",
        "comment_addr",
        "abs_time",
        "tz_offset",
        "daylight_save_time",
        "time_flags",
        "time_quality",
        "flags",
        "reserved1",
        "start_angle",
        "start_distance",
    )

    def __init__(self, **kwargs) -> None:
        super().__init__()

        self.comment = ""

        self.author = self.project = self.subject = self.department = ""

        try:
            self.address = address = kwargs["address"]
            stream = kwargs["stream"]
            stream.seek(address)

            (
                self.id,
                self.reserved0,
                self.block_len,
                self.links_nr,
                self.first_dg_addr,
                self.file_history_addr,
                self.channel_tree_addr,
                self.first_attachment_addr,
                self.first_event_addr,
                self.comment_addr,
                self.abs_time,
                self.tz_offset,
                self.daylight_save_time,
                self.time_flags,
                self.time_quality,
                self.flags,
                self.reserved1,
                self.start_angle,
                self.start_distance,
            ) = unpack(v4c.FMT_HEADER_BLOCK, stream.read(v4c.HEADER_BLOCK_SIZE))

            if self.id != b"##HD":
                message = f'Expected "##HD" block @{hex(address)} but found "{self.id}"'

                logger.exception(message)
                raise MdfException(message)

            self.comment = get_text_v4(address=self.comment_addr, stream=stream)

        except KeyError:

            self.address = 0x40
            self.id = b"##HD"
            self.reserved0 = kwargs.get("reserved3", 0)
            self.block_len = kwargs.get("block_len", v4c.HEADER_BLOCK_SIZE)
            self.links_nr = kwargs.get("links_nr", 6)
            self.first_dg_addr = kwargs.get("first_dg_addr", 0)
            self.file_history_addr = kwargs.get("file_history_addr", 0)
            self.channel_tree_addr = kwargs.get("channel_tree_addr", 0)
            self.first_attachment_addr = kwargs.get("first_attachment_addr", 0)
            self.first_event_addr = kwargs.get("first_event_addr", 0)
            self.comment_addr = kwargs.get("comment_addr", 0)
            self.abs_time = kwargs.get("abs_time", 0)
            self.tz_offset = kwargs.get("tz_offset", 0)
            self.daylight_save_time = kwargs.get("daylight_save_time", 0)
            self.time_flags = kwargs.get("time_flags", 0)
            self.time_quality = kwargs.get("time_quality", 0)
            self.flags = kwargs.get("flags", 0)
            self.reserved1 = kwargs.get("reserved4", 0)
            self.start_angle = kwargs.get("start_angle", 0)
            self.start_distance = kwargs.get("start_distance", 0)

            self.start_time = datetime(1980, 1, 1)

        if self.comment.startswith("<HDcomment"):
            comment = self.comment
            try:
                comment_xml = ET.fromstring(comment)
            except ET.ParseError as e:
                logger.error(f"could not parse header block comment; {e}")
            else:
                common_properties = comment_xml.find(".//common_properties")
                if common_properties is not None:
                    for e in common_properties:
                        name = e.get("name")
                        if name == "author":
                            self.author = e.text
                        elif name == "department":
                            self.department = e.text
                        elif name == "project":
                            self.project = e.text
                        elif name == "subject":
                            self.subject = e.text

    def __getitem__(self, item: str) -> Any:
        return self.__getattribute__(item)

    def __setitem__(self, item: str, value: Any) -> None:
        self.__setattr__(item, value)

    @property
    def start_time(self) -> datetime:
        """getter and setter the measurement start timestamp

        Returns
        -------
        timestamp : datetime.datetime
            start timestamp

        """

        timestamp = self.abs_time / 10**9
        if self.time_flags & v4c.FLAG_HD_LOCAL_TIME:
            try:
                timestamp = datetime.fromtimestamp(timestamp)
            except OverflowError:
                timestamp = datetime.fromtimestamp(0) + timedelta(seconds=timestamp)
        else:
            try:
                timestamp = datetime.fromtimestamp(timestamp, timezone.utc)
            except OverflowError:
                timestamp = datetime.fromtimestamp(0, timezone.utc) + timedelta(
                    seconds=timestamp
                )
        return timestamp

    @start_time.setter
    def start_time(self, timestamp: datetime) -> None:

        if timestamp.tzinfo is None:
            self.time_flags = v4c.FLAG_HD_LOCAL_TIME
        else:
            self.time_flags = 0
            timestamp = timestamp.astimezone(timezone.utc)

        timestamp = int(timestamp.timestamp() * 10**9)
        self.abs_time = timestamp
        self.tz_offset = 0
        self.daylight_save_time = 0

    def to_blocks(self, address: int, blocks: list[Any]) -> int:
        blocks.append(self)
        self.address = address
        address += self.block_len

        if self.comment.startswith("<HDcomment"):
            comment = self.comment
            comment = ET.fromstring(comment)
            common_properties = comment.find(".//common_properties")
            if common_properties is not None:
                for e in common_properties:
                    name = e.get("name")
                    if name == "author":
                        e.text = self.author
                        break
                else:
                    author = ET.SubElement(
                        common_properties, "e", name="author"
                    ).text = self.author

                for e in common_properties:
                    name = e.get("name")
                    if name == "department":
                        e.text = self.department
                        break
                else:
                    department = ET.SubElement(
                        common_properties, "e", name="department"
                    ).text = self.department

                for e in common_properties:
                    name = e.get("name")
                    if name == "project":
                        e.text = self.author
                        break
                else:
                    project = ET.SubElement(
                        common_properties, "e", name="project"
                    ).text = self.project

                for e in common_properties:
                    name = e.get("name")
                    if name == "subject":
                        e.text = self.author
                        break
                else:
                    subject = ET.SubElement(
                        common_properties, "e", name="subject"
                    ).text = self.subject

            else:
                common_properties = ET.SubElement(comment, "common_properties")
                author = ET.SubElement(
                    common_properties, "e", name="author"
                ).text = self.author
                department = ET.SubElement(
                    common_properties, "e", name="department"
                ).text = self.department
                project = ET.SubElement(
                    common_properties, "e", name="project"
                ).text = self.project
                subject = ET.SubElement(
                    common_properties, "e", name="subject"
                ).text = self.subject

            comment = ET.tostring(comment, encoding="utf8", method="xml").replace(
                b"<?xml version='1.0' encoding='utf8'?>\n", b""
            )

        else:
            comment = v4c.HD_COMMENT_TEMPLATE.format(
                escape_xml_string(self.comment),
                escape_xml_string(self.author),
                escape_xml_string(self.department),
                escape_xml_string(self.project),
                escape_xml_string(self.subject),
            )

        tx_block = TextBlock(text=comment, meta=True)
        self.comment_addr = address
        tx_block.address = address
        address += tx_block.block_len
        blocks.append(tx_block)

        return address

    def __bytes__(self) -> bytes:
        result = pack(
            v4c.FMT_HEADER_BLOCK, *[self[key] for key in v4c.KEYS_HEADER_BLOCK]
        )
        return result class SourceInformation:
    """
    *SourceInformation* has the following attributes, that are also available as
    dict like key-value pairs

    SIBLOCK fields

    * ``id`` - bytes : block ID; always b'##SI'
    * ``reserved0`` - int : reserved bytes
    * ``block_len`` - int : block bytes size
    * ``links_nr`` - int : number of links
    * ``name_addr`` - int : address of TXBLOCK that contains the source name
    * ``path_addr`` - int : address of TXBLOCK that contains the source path
    * ``comment_addr`` - int : address of TXBLOCK/MDBLOCK that contains the
      source comment
    * ``source_type`` - int : integer code for source type
    * ``bus_type`` - int : integer code for source bus type
    * ``flags`` - int : source flags
    * ``reserved1`` - bytes : reserved bytes

    Other attributes

    * ``address`` - int : source information address
    * ``comment`` - str : source comment
    * ``name`` - str : source name
    * ``path`` - str : source path

    """

    __slots__ = (
        "address",
        "comment",
        "name",
        "path",
        "id",
        "reserved0",
        "block_len",
        "links_nr",
        "name_addr",
        "path_addr",
        "comment_addr",
        "source_type",
        "bus_type",
        "flags",
        "reserved1",
    )

    def __init__(self, **kwargs) -> None:
        self.name = self.path = self.comment = ""

        if "stream" in kwargs:

            stream = kwargs["stream"]
            mapped = kwargs["mapped"]
            try:
                block = kwargs["raw_bytes"]
                self.address = kwargs["address"]
            except KeyError:
                self.address = address = kwargs["address"]
                stream.seek(address)
                block = stream.read(v4c.SI_BLOCK_SIZE)

            (
                self.id,
                self.reserved0,
                self.block_len,
                self.links_nr,
                self.name_addr,
                self.path_addr,
                self.comment_addr,
                self.source_type,
                self.bus_type,
                self.flags,
                self.reserved1,
            ) = unpack(v4c.FMT_SOURCE_INFORMATION, block)

            if self.id != b"##SI":
                message = f'Expected "##SI" block @{hex(address)} but found "{self.id}"'

                logger.exception(message)
                raise MdfException(message)

            tx_map = kwargs["tx_map"]

            address = self.name_addr
            if address in tx_map:
                self.name = tx_map[address]
            else:
                self.name = get_text_v4(
                    address=self.name_addr, stream=stream, mapped=mapped
                )
                tx_map[address] = self.name

            address = self.path_addr
            if address in tx_map:
                self.path = tx_map[address]
            else:
                self.path = get_text_v4(
                    address=self.path_addr, stream=stream, mapped=mapped
                )
                tx_map[address] = self.path

            address = self.comment_addr
            if address in tx_map:
                self.comment = tx_map[address]
            else:
                self.comment = get_text_v4(
                    address=self.comment_addr, stream=stream, mapped=mapped
                )
                tx_map[address] = self.comment

        else:
            self.address = 0
            self.id = b"##SI"
            self.reserved0 = 0
            self.block_len = v4c.SI_BLOCK_SIZE
            self.links_nr = 3
            self.name_addr = 0
            self.path_addr = 0
            self.comment_addr = 0
            self.source_type = kwargs.get("source_type", v4c.SOURCE_TOOL)
            self.bus_type = kwargs.get("bus_type", v4c.BUS_TYPE_NONE)
            self.flags = 0
            self.reserved1 = b"\x00" * 5

    def __getitem__(self, item: str) -> Any:
        return self.__getattribute__(item)

    def __setitem__(self, item: str, value: Any) -> None:
        self.__setattr__(item, value)

    def __contains__(self, item: str) -> bool:
        return hasattr(self, item)

    def copy(self) -> SourceInformation:
        source = SourceInformation(
            source_type=self.source_type,
            bus_type=self.bus_type,
        )
        source.name = self.name
        source.comment = self.comment
        source.path = self.path

        return source

    def metadata(self) -> str:
        max_len = max(len(key) for key in v4c.KEYS_SOURCE_INFORMATION)
        template = f"{{: <{max_len}}}: {{}}"

        metadata = []
        lines = f"""
name: {self.name}
path: {self.path}
address: {hex(self.address)}
comment: {self.comment}

""".split(
            "\n"
        )
        for key in v4c.KEYS_SOURCE_INFORMATION:
            val = getattr(self, key)
            if key.endswith("addr") or key.startswith("text_"):
                lines.append(template.format(key, hex(val)))
            elif isinstance(val, float):
                lines.append(template.format(key, round(val, 6)))
            else:
                if isinstance(val, bytes):
                    lines.append(template.format(key, val.strip(b"\0")))
                else:
                    lines.append(template.format(key, val))

            if key == "source_type":
                lines[-1] += f" = {v4c.SOURCE_TYPE_TO_STRING[self.source_type]}"
            elif key == "bus_type":
                lines[-1] += f" = {v4c.BUS_TYPE_TO_STRING[self.bus_type]}"

        for line in lines:
            if not line:
                metadata.append(line)
            else:
                for wrapped_line in wrap(line, width=120):
                    metadata.append(wrapped_line)

        return "\n".join(metadata)

    def to_blocks(
        self,
        address: int,
        blocks: list[Any],
        defined_texts: dict[str, int],
        si_map: dict[bytes, int],
    ) -> int:

        id_ = id(self)
        if id_ in si_map:
            return address

        text = self.name
        if text:
            if text in defined_texts:
                self.name_addr = defined_texts[text]
            else:
                tx_block = TextBlock(
                    text=text.encode("utf-8", "replace"),
                    meta=False,
                    safe=True,
                )
                self.name_addr = address
                defined_texts[text] = address
                tx_block.address = address
                address += tx_block.block_len
                blocks.append(tx_block)
        else:
            self.name_addr = 0

        text = self.path
        if text:
            if text in defined_texts:
                self.path_addr = defined_texts[text]
            else:
                tx_block = TextBlock(
                    text=text.encode("utf-8", "replace"),
                    meta=False,
                    safe=True,
                )
                self.path_addr = address
                defined_texts[text] = address
                tx_block.address = address
                address += tx_block.block_len
                blocks.append(tx_block)
        else:
            self.path_addr = 0

        text = self.comment
        if text:
            if text in defined_texts:
                self.comment_addr = defined_texts[text]
            else:
                meta = text.startswith("<SI")
                tx_block = TextBlock(
                    text=text.encode("utf-8", "replace"),
                    meta=meta,
                    safe=True,
                )
                self.comment_addr = address
                defined_texts[text] = address
                tx_block.address = address
                address += tx_block.block_len
                blocks.append(tx_block)
        else:
            self.comment_addr = 0

        bts = bytes(self)
        if bts in si_map:
            self.address = si_map[bts]
        else:
            blocks.append(bts)
            si_map[bts] = si_map[id(self)] = address
            self.address = address
            address += self.block_len

        return address

    @classmethod
    def from_common_source(cls, source: Source) -> SourceInformation:
        obj = cls()
        obj.name = source.name
        obj.path = source.path
        obj.comment = source.comment
        obj.source_type = source.source_type
        obj.bus_type = source.bus_type

        return obj

    def __bytes__(self) -> bytes:
        return v4c.SOURCE_INFORMATION_PACK(
            self.id,
            self.reserved0,
            self.block_len,
            self.links_nr,
            self.name_addr,
            self.path_addr,
            self.comment_addr,
            self.source_type,
            self.bus_type,
            self.flags,
            self.reserved1,
        )

    def __str__(self) -> str:
        return "<SourceInformation (name: {}, path: {}, comment: {}, address: {}, fields: {})>".format(
            self.name, self.path, self.comment, hex(self.address), block_fields(self)
        ) class Signal(object):
    """
    The *Signal* represents a channel described by it's samples and timestamps.
    It can perform arithmetic operations against other *Signal* or numeric types.
    The operations are computed in respect to the timestamps (time correct).
    The non-float signals are not interpolated, instead the last value relative
    to the current timestamp is used.
    *samples*, *timestamps* and *name* are mandatory arguments.

    Parameters
    ----------
    samples : numpy.array | list | tuple
        signal samples
    timestamps : numpy.array | list | tuple
        signal timestamps
    unit : str
        signal unit
    name : str
        signal name
    conversion : dict | channel conversion block
        dict that contains extra conversion information about the signal ,
        default *None*
    comment : str
        signal comment, default ''
    raw : bool
        signal samples are raw values, with no physical conversion applied
    master_metadata : list
        master name and sync type
    display_names : dict
        display names used by mdf version 3
    attachment : bytes, name
        channel attachment and name from MDF version 4
    source : Source
        source information named tuple
    bit_count : int
        bit count; useful for integer channels
    stream_sync : bool
        the channel is a synchronisation for the attachment stream (mdf v4 only)
    invalidation_bits : numpy.array | None
        channel invalidation bits, default *None*
    encoding : str | None
        encoding for string signals; default *None*

    """

    def __init__(
        self,
        samples: ArrayLike | None = None,
        timestamps: ArrayLike | None = None,
        unit: str = "",
        name: str = "",
        conversion: dict[str, Any] | ChannelConversionType | None = None,
        comment: str = "",
        raw: bool = True,
        master_metadata: tuple[str, SyncType] | None = None,
        display_names: dict[str, str] | None = None,
        attachment: tuple[bytes, str | None, str | None] | None = None,
        source: SourceType | None = None,
        bit_count: int | None = None,
        stream_sync: bool = False,
        invalidation_bits: ArrayLike | None = None,
        encoding: str | None = None,
        group_index: int = -1,
        channel_index: int = -1,
    ) -> None:

        if samples is None or timestamps is None or not name:
            message = (
                '"samples", "timestamps" and "name" are mandatory '
                f"for Signal class __init__: samples={samples}\n"
                f"timestamps={timestamps}\nname={name}"
            )
            raise MdfException(message)
        else:
            if not isinstance(samples, np.ndarray):
                samples = np.array(samples)
                if samples.dtype.kind == "U":
                    if encoding is None:
                        encodings = ["utf-8", "latin-1"]
                    else:
                        encodings = [encoding, "utf-8", "latin-1"]
                    for encoding in encodings:
                        try:
                            samples = encode(samples, encoding)
                            break
                        except:
                            continue
                    else:
                        samples = encode(samples, encodings[0], errors="ignore")

            if not isinstance(timestamps, np.ndarray):
                timestamps = np.array(timestamps, dtype=np.float64)
            if samples.shape[0] != timestamps.shape[0]:
                message = "{} samples and timestamps length mismatch ({} vs {})"
                message = message.format(name, samples.shape[0], timestamps.shape[0])
                logger.exception(message)
                raise MdfException(message)

            self.samples = samples
            self.timestamps = timestamps
            self.unit = unit
            self.name = name
            self.comment = comment
            self._plot_axis = None
            self.raw = raw
            self.master_metadata = master_metadata
            self.display_names = display_names or {}
            self.attachment = attachment
            self.encoding = encoding
            self.group_index = group_index
            self.channel_index = channel_index

            if source:
                if not isinstance(source, Source):
                    source = Source.from_source(source)
            self.source = source

            if bit_count is None:
                self.bit_count = samples.dtype.itemsize * 8
            else:
                self.bit_count = bit_count

            self.stream_sync = stream_sync

            if invalidation_bits is not None:
                if not isinstance(invalidation_bits, np.ndarray):
                    invalidation_bits = np.array(invalidation_bits)
                if invalidation_bits.shape[0] != samples.shape[0]:
                    message = (
                        "{} samples and invalidation bits length mismatch ({} vs {})"
                    )
                    message = message.format(
                        name, samples.shape[0], invalidation_bits.shape[0]
                    )
                    logger.exception(message)
                    raise MdfException(message)
            self.invalidation_bits = invalidation_bits

            if conversion:
                if not isinstance(
                    conversion, (v4b.ChannelConversion, v3b.ChannelConversion)
                ):
                    conversion = from_dict(conversion)

            self.conversion = conversion

    def __repr__(self):
        return f"""<Signal {self.name}:
\tsamples={self.samples}
\ttimestamps={self.timestamps}
\tinvalidation_bits={self.invalidation_bits}
\tunit="{self.unit}"
\tconversion={self.conversion}
\tsource={self.source}
\tcomment="{self.comment}"
\tmastermeta="{self.master_metadata}"
\traw={self.raw}
\tdisplay_names={self.display_names}
\tattachment={self.attachment}>
"""

    def plot(self, validate: bool = True, index_only: bool = False) -> None:
        """plot Signal samples. Pyqtgraph is used if it is available; in this
        case see the GUI plot documentation to see the available commands

        Parameters
        ----------
        validate (True): bool
            apply the invalidation bits

        index_only (False) : bool
            use index based X axis. This can be useful if the master (usually
            time based) is corrupted with NaN, inf or if it is not strictly
            increasing

        """
        try:

            from .gui.plot import plot

            plot(self, validate=True, index_only=False)
            return

        except:
            try:
                import matplotlib.pyplot as plt
                from matplotlib.widgets import Slider
            except ImportError:
                logging.warning("Signal plotting requires pyqtgraph or matplotlib")
                return

        if len(self.samples.shape) <= 1 and self.samples.dtype.names is None:
            fig = plt.figure()
            fig.canvas.set_window_title(self.name)
            fig.text(
                0.95,
                0.05,
                f"asammdf {__version__}",
                fontsize=8,
                color="red",
                ha="right",
                va="top",
                alpha=0.5,
            )

            name = self.name

            if self.comment:
                comment = self.comment.replace("$", "")
                comment = extract_cncomment_xml(comment)
                comment = fill(comment, 120).replace("\\n", " ")

                title = f"{name}\n({comment})"
                plt.title(title)
            else:
                plt.title(name)
            try:
                if not self.master_metadata:
                    plt.xlabel("Time [s]")
                    plt.ylabel(f"[{self.unit}]")
                    plt.plot(self.timestamps, self.samples, "b")
                    plt.plot(self.timestamps, self.samples, "b.")
                    plt.grid(True)
                    plt.show()
                else:
                    master_name, sync_type = self.master_metadata
                    if sync_type in (0, 1):
                        plt.xlabel(f"{master_name} [s]")
                    elif sync_type == 2:
                        plt.xlabel(f"{master_name} [deg]")
                    elif sync_type == 3:
                        plt.xlabel(f"{master_name} [m]")
                    elif sync_type == 4:
                        plt.xlabel(f"{master_name} [index]")
                    plt.ylabel(f"[{self.unit}]")
                    plt.plot(self.timestamps, self.samples, "b")
                    plt.plot(self.timestamps, self.samples, "b.")
                    plt.grid(True)
                    plt.show()
            except ValueError:
                plt.close(fig)
        else:
            try:
                names = self.samples.dtype.names
                if self.samples.dtype.names is None or len(names) == 1:

                    if names:
                        samples = self.samples[names[0]]
                    else:
                        samples = self.samples

                    shape = samples.shape[1:]

                    fig = plt.figure()
                    fig.canvas.set_window_title(self.name)
                    fig.text(
                        0.95,
                        0.05,
                        f"asammdf {__version__}",
                        fontsize=8,
                        color="red",
                        ha="right",
                        va="top",
                        alpha=0.5,
                    )

                    if self.comment:
                        comment = self.comment.replace("$", "")
                        plt.title(f"{self.name}\n({comment})")
                    else:
                        plt.title(self.name)

                    ax = fig.add_subplot(111, projection="3d")

                    # Grab some test data.
                    X = np.array(range(shape[1]))
                    Y = np.array(range(shape[0]))
                    X, Y = np.meshgrid(X, Y)

                    Z = samples[0]

                    # Plot a basic wireframe.
                    self._plot_axis = ax.plot_wireframe(X, Y, Z, rstride=1, cstride=1)

                    # Place Sliders on Graph
                    ax_a = plt.axes([0.25, 0.1, 0.65, 0.03])

                    # Create Sliders & Determine Range
                    sa = Slider(
                        ax_a,
                        "Time [s]",
                        self.timestamps[0],
                        self.timestamps[-1],
                        valinit=self.timestamps[0],
                    )

                    def update(val):
                        self._plot_axis.remove()
                        idx = np.searchsorted(self.timestamps, sa.val, side="right")
                        Z = samples[idx - 1]
                        self._plot_axis = ax.plot_wireframe(
                            X, Y, Z, rstride=1, cstride=1
                        )
                        fig.canvas.draw_idle()

                    sa.on_changed(update)

                    plt.show()

                else:
                    fig = plt.figure()
                    fig.canvas.set_window_title(self.name)
                    fig.text(
                        0.95,
                        0.05,
                        f"asammdf {__version__}",
                        fontsize=8,
                        color="red",
                        ha="right",
                        va="top",
                        alpha=0.5,
                    )

                    if self.comment:
                        comment = self.comment.replace("$", "")
                        plt.title(f"{self.name}\n({comment})")
                    else:
                        plt.title(self.name)

                    ax = fig.add_subplot(111, projection="3d")

                    samples = self.samples[names[0]]
                    axis1 = self.samples[names[1]]
                    axis2 = self.samples[names[2]]

                    # Grab some test data.
                    X, Y = np.meshgrid(axis2[0], axis1[0])

                    Z = samples[0]

                    # Plot a basic wireframe.
                    self._plot_axis = ax.plot_wireframe(X, Y, Z, rstride=1, cstride=1)

                    # Place Sliders on Graph
                    ax_a = plt.axes([0.25, 0.1, 0.65, 0.03])

                    # Create Sliders & Determine Range
                    sa = Slider(
                        ax_a,
                        "Time [s]",
                        self.timestamps[0],
                        self.timestamps[-1],
                        valinit=self.timestamps[0],
                    )

                    def update(val):
                        self._plot_axis.remove()
                        idx = np.searchsorted(self.timestamps, sa.val, side="right")
                        Z = samples[idx - 1]
                        X, Y = np.meshgrid(axis2[idx - 1], axis1[idx - 1])
                        self._plot_axis = ax.plot_wireframe(
                            X, Y, Z, rstride=1, cstride=1
                        )
                        fig.canvas.draw_idle()

                    sa.on_changed(update)

                    plt.show()

            except Exception as err:
                print(err)

    def cut(
        self,
        start: float | None = None,
        stop: float | None = None,
        include_ends: bool = True,
        integer_interpolation_mode: IntInterpolationModeType
        | IntegerInterpolation = IntegerInterpolation.REPEAT_PREVIOUS_SAMPLE,
        float_interpolation_mode: FloatInterpolationModeType
        | FloatInterpolation = FloatInterpolation.LINEAR_INTERPOLATION,
    ) -> Signal:
        """
        Cuts the signal according to the *start* and *stop* values, by using
        the insertion indexes in the signal's *time* axis.

        Parameters
        ----------
        start : float
            start timestamp for cutting
        stop : float
            stop timestamp for cutting
        include_ends : bool
            include the *start* and *stop* timestamps after cutting the signal.
            If *start* and *stop* are found in the original timestamps, then
            the new samples will be computed using interpolation. Default *True*

        integer_interpolation_mode : int
            interpolation mode for integer signals; default 0

                * 0 - repeat previous samples
                * 1 - linear interpolation
                * 2 - hybrid interpolation: channels with integer data type (raw values) that have a
                  conversion that outputs float values will use linear interpolation, otherwise
                  the previous sample is used

                .. versionadded:: 6.2.0

        float_interpolation_mode : int
            interpolation mode for float channels; default 1

                * 0 - repeat previous sample
                * 1 - use linear interpolation

                .. versionadded:: 6.2.0

        Returns
        -------
        result : Signal
            new *Signal* cut from the original

        Examples
        --------
        >>> new_sig = old_sig.cut(1.0, 10.5)
        >>> new_sig.timestamps[0], new_sig.timestamps[-1]
        0.98, 10.48

        """

        integer_interpolation_mode = IntegerInterpolation(integer_interpolation_mode)
        float_interpolation_mode = FloatInterpolation(float_interpolation_mode)

        original_start, original_stop = (start, stop)

        if len(self) == 0:
            result = Signal(
                np.array([], dtype=self.samples.dtype),
                np.array([], dtype=self.timestamps.dtype),
                self.unit,
                self.name,
                self.conversion,
                self.comment,
                self.raw,
                self.master_metadata,
                self.display_names,
                self.attachment,
                self.source,
                self.bit_count,
                self.stream_sync,
                encoding=self.encoding,
                group_index=self.group_index,
                channel_index=self.channel_index,
            )

        elif start is None and stop is None:
            # return the channel uncut
            result = Signal(
                self.samples.copy(),
                self.timestamps.copy(),
                self.unit,
                self.name,
                self.conversion,
                self.comment,
                self.raw,
                self.master_metadata,
                self.display_names,
                self.attachment,
                self.source,
                self.bit_count,
                self.stream_sync,
                invalidation_bits=self.invalidation_bits.copy()
                if self.invalidation_bits is not None
                else None,
                encoding=self.encoding,
                group_index=self.group_index,
                channel_index=self.channel_index,
            )

        else:
            if start is None:
                # cut from begining to stop
                if stop < self.timestamps[0]:
                    result = Signal(
                        np.array([], dtype=self.samples.dtype),
                        np.array([], dtype=self.timestamps.dtype),
                        self.unit,
                        self.name,
                        self.conversion,
                        self.comment,
                        self.raw,
                        self.master_metadata,
                        self.display_names,
                        self.attachment,
                        self.source,
                        self.bit_count,
                        self.stream_sync,
                        encoding=self.encoding,
                        group_index=self.group_index,
                        channel_index=self.channel_index,
                    )

                else:
                    stop = np.searchsorted(self.timestamps, stop, side="right")
                    if (
                        include_ends
                        and original_stop not in self.timestamps
                        and original_stop < self.timestamps[-1]
                    ):
                        interpolated = self.interp(
                            [original_stop],
                            integer_interpolation_mode=integer_interpolation_mode,
                            float_interpolation_mode=float_interpolation_mode,
                        )

                        if len(interpolated):
                            samples = np.append(
                                self.samples[:stop], interpolated.samples, axis=0
                            )
                            timestamps = np.append(
                                self.timestamps[:stop], interpolated.timestamps
                            )
                            if self.invalidation_bits is not None:
                                invalidation_bits = np.append(
                                    self.invalidation_bits[:stop],
                                    interpolated.invalidation_bits,
                                )
                            else:
                                invalidation_bits = None
                    else:
                        samples = self.samples[:stop].copy()
                        timestamps = self.timestamps[:stop].copy()
                        if self.invalidation_bits is not None:
                            invalidation_bits = self.invalidation_bits[:stop].copy()
                        else:
                            invalidation_bits = None
                    result = Signal(
                        samples,
                        timestamps,
                        self.unit,
                        self.name,
                        self.conversion,
                        self.comment,
                        self.raw,
                        self.master_metadata,
                        self.display_names,
                        self.attachment,
                        self.source,
                        self.bit_count,
                        self.stream_sync,
                        invalidation_bits=invalidation_bits,
                        encoding=self.encoding,
                        group_index=self.group_index,
                        channel_index=self.channel_index,
                    )

            elif stop is None:
                # cut from start to end
                if start > self.timestamps[-1]:
                    result = Signal(
                        np.array([], dtype=self.samples.dtype),
                        np.array([], dtype=self.timestamps.dtype),
                        self.unit,
                        self.name,
                        self.conversion,
                        self.comment,
                        self.raw,
                        self.master_metadata,
                        self.display_names,
                        self.attachment,
                        self.source,
                        self.bit_count,
                        self.stream_sync,
                        encoding=self.encoding,
                        group_index=self.group_index,
                        channel_index=self.channel_index,
                    )

                else:
                    start = np.searchsorted(self.timestamps, start, side="left")
                    if (
                        include_ends
                        and original_start not in self.timestamps
                        and original_start > self.timestamps[0]
                    ):
                        interpolated = self.interp(
                            [original_start],
                            integer_interpolation_mode=integer_interpolation_mode,
                            float_interpolation_mode=float_interpolation_mode,
                        )
                        if len(interpolated):
                            samples = np.append(
                                interpolated.samples, self.samples[start:], axis=0
                            )
                            timestamps = np.append(
                                interpolated.timestamps, self.timestamps[start:]
                            )
                            if self.invalidation_bits is not None:
                                invalidation_bits = np.append(
                                    interpolated.invalidation_bits,
                                    self.invalidation_bits[start:],
                                )
                            else:
                                invalidation_bits = None
                    else:
                        samples = self.samples[start:].copy()
                        timestamps = self.timestamps[start:].copy()
                        if self.invalidation_bits is not None:
                            invalidation_bits = self.invalidation_bits[start:].copy()
                        else:
                            invalidation_bits = None
                    result = Signal(
                        samples,
                        timestamps,
                        self.unit,
                        self.name,
                        self.conversion,
                        self.comment,
                        self.raw,
                        self.master_metadata,
                        self.display_names,
                        self.attachment,
                        self.source,
                        self.bit_count,
                        self.stream_sync,
                        invalidation_bits=invalidation_bits,
                        encoding=self.encoding,
                        group_index=self.group_index,
                        channel_index=self.channel_index,
                    )

            else:
                # cut between start and stop
                if start > self.timestamps[-1] or stop < self.timestamps[0]:
                    result = Signal(
                        np.array([], dtype=self.samples.dtype),
                        np.array([], dtype=self.timestamps.dtype),
                        self.unit,
                        self.name,
                        self.conversion,
                        self.comment,
                        self.raw,
                        self.master_metadata,
                        self.display_names,
                        self.attachment,
                        self.source,
                        self.bit_count,
                        self.stream_sync,
                        encoding=self.encoding,
                        group_index=self.group_index,
                        channel_index=self.channel_index,
                    )
                else:
                    start = np.searchsorted(self.timestamps, start, side="left")
                    stop = np.searchsorted(self.timestamps, stop, side="right")

                    if start == stop:
                        if include_ends:
                            if original_start == original_stop:
                                ends = np.array(
                                    [original_start], dtype=self.timestamps.dtype
                                )
                            else:
                                ends = np.array(
                                    [original_start, original_stop],
                                    dtype=self.timestamps.dtype,
                                )

                            interpolated = self.interp(
                                ends,
                                integer_interpolation_mode=integer_interpolation_mode,
                                float_interpolation_mode=float_interpolation_mode,
                            )
                            samples = interpolated.samples
                            timestamps = ends
                            invalidation_bits = interpolated.invalidation_bits
                        else:
                            samples = np.array([], dtype=self.samples.dtype)
                            timestamps = np.array([], dtype=self.timestamps.dtype)
                            if self.invalidation_bits is not None:
                                invalidation_bits = np.array([], dtype=bool)
                            else:
                                invalidation_bits = None
                    else:
                        samples = self.samples[start:stop].copy()
                        timestamps = self.timestamps[start:stop].copy()
                        if self.invalidation_bits is not None:
                            invalidation_bits = self.invalidation_bits[
                                start:stop
                            ].copy()
                        else:
                            invalidation_bits = None

                        if (
                            include_ends
                            and original_stop not in self.timestamps
                            and original_stop < self.timestamps[-1]
                        ):
                            interpolated = self.interp(
                                [original_stop],
                                integer_interpolation_mode=integer_interpolation_mode,
                                float_interpolation_mode=float_interpolation_mode,
                            )

                            if len(interpolated):
                                samples = np.append(
                                    samples, interpolated.samples, axis=0
                                )
                                timestamps = np.append(
                                    timestamps, interpolated.timestamps
                                )
                                if invalidation_bits is not None:
                                    invalidation_bits = np.append(
                                        invalidation_bits,
                                        interpolated.invalidation_bits,
                                    )

                        if (
                            include_ends
                            and original_start not in self.timestamps
                            and original_start > self.timestamps[0]
                        ):
                            interpolated = self.interp(
                                [original_start],
                                integer_interpolation_mode=integer_interpolation_mode,
                                float_interpolation_mode=float_interpolation_mode,
                            )

                            if len(interpolated):
                                samples = np.append(
                                    interpolated.samples, samples, axis=0
                                )
                                timestamps = np.append(
                                    interpolated.timestamps, timestamps
                                )

                                if invalidation_bits is not None:
                                    invalidation_bits = np.append(
                                        interpolated.invalidation_bits,
                                        invalidation_bits,
                                    )

                    result = Signal(
                        samples,
                        timestamps,
                        self.unit,
                        self.name,
                        self.conversion,
                        self.comment,
                        self.raw,
                        self.master_metadata,
                        self.display_names,
                        self.attachment,
                        self.source,
                        self.bit_count,
                        self.stream_sync,
                        invalidation_bits=invalidation_bits,
                        encoding=self.encoding,
                        group_index=self.group_index,
                        channel_index=self.channel_index,
                    )

        return result

    def extend(self, other: Signal) -> Signal:
        """extend signal with samples from another signal

        Parameters
        ----------
        other : Signal

        Returns
        -------
        signal : Signal
            new extended *Signal*

        """
        if len(self.timestamps):
            last_stamp = self.timestamps[-1]
        else:
            last_stamp = 0
        if len(other):
            other_first_sample = other.timestamps[0]
            if last_stamp >= other_first_sample:
                timestamps = other.timestamps + last_stamp
            else:
                timestamps = other.timestamps

            if self.invalidation_bits is None and other.invalidation_bits is None:
                invalidation_bits = None
            elif self.invalidation_bits is None and other.invalidation_bits is not None:
                invalidation_bits = np.concatenate(
                    (np.zeros(len(self), dtype=bool), other.invalidation_bits)
                )
            elif self.invalidation_bits is not None and other.invalidation_bits is None:
                invalidation_bits = np.concatenate(
                    (self.invalidation_bits, np.zeros(len(other), dtype=bool))
                )
            else:
                invalidation_bits = np.append(
                    self.invalidation_bits, other.invalidation_bits
                )

            result = Signal(
                np.append(self.samples, other.samples, axis=0),
                np.append(self.timestamps, timestamps),
                self.unit,
                self.name,
                self.conversion,
                self.comment,
                self.raw,
                self.master_metadata,
                self.display_names,
                self.attachment,
                self.source,
                self.bit_count,
                self.stream_sync,
                invalidation_bits=invalidation_bits,
                encoding=self.encoding,
                group_index=self.group_index,
                channel_index=self.channel_index,
            )
        else:
            result = self

        return result

    def interp(
        self,
        new_timestamps: NDArray[Any],
        integer_interpolation_mode: IntInterpolationModeType
        | IntegerInterpolation = IntegerInterpolation.REPEAT_PREVIOUS_SAMPLE,
        float_interpolation_mode: FloatInterpolationModeType
        | FloatInterpolation = FloatInterpolation.LINEAR_INTERPOLATION,
    ) -> Signal:
        """returns a new *Signal* interpolated using the *new_timestamps*

        Parameters
        ----------
        new_timestamps : np.array
            timestamps used for interpolation

        integer_interpolation_mode : int
            interpolation mode for integer signals; default 0

                * 0 - repeat previous samples
                * 1 - linear interpolation
                * 2 - hybrid interpolation: channels with integer data type (raw values) that have a
                  conversion that outputs float values will use linear interpolation, otherwise
                  the previous sample is used

                .. versionadded:: 6.2.0

        float_interpolation_mode : int
            interpolation mode for float channels; default 1

                * 0 - repeat previous sample
                * 1 - use linear interpolation

                .. versionadded:: 6.2.0

        Returns
        -------
        signal : Signal
            new interpolated *Signal*

        """

        integer_interpolation_mode = IntegerInterpolation(integer_interpolation_mode)
        float_interpolation_mode = FloatInterpolation(float_interpolation_mode)

        if not len(self.samples) or not len(new_timestamps):

            return Signal(
                self.samples[:0].copy(),
                self.timestamps[:0].copy(),
                self.unit,
                self.name,
                comment=self.comment,
                conversion=self.conversion,
                raw=self.raw,
                master_metadata=self.master_metadata,
                display_names=self.display_names,
                attachment=self.attachment,
                stream_sync=self.stream_sync,
                invalidation_bits=None,
                encoding=self.encoding,
                group_index=self.group_index,
                channel_index=self.channel_index,
            )
        else:

            # # we need to validate first otherwise we can get false invalid data
            # # if the new timebase and the invaldiation bits are alligned in an
            # # infavorable way
            #
            # if self.invalidation_bits is not None:
            #     signal = self.validate()
            #     has_invalidation = True
            # else:
            #     signal = self
            #     has_invalidation = False

            signal = self
            invalidation_bits = signal.invalidation_bits

            if not len(signal.samples):
                return Signal(
                    self.samples[:0].copy(),
                    self.timestamps[:0].copy(),
                    self.unit,
                    self.name,
                    comment=self.comment,
                    conversion=self.conversion,
                    raw=self.raw,
                    master_metadata=self.master_metadata,
                    display_names=self.display_names,
                    attachment=self.attachment,
                    stream_sync=self.stream_sync,
                    invalidation_bits=None
                    if invalidation_bits is None
                    else np.array([], dtype=bool),
                    encoding=self.encoding,
                    group_index=self.group_index,
                    channel_index=self.channel_index,
                )

            if len(signal.samples.shape) > 1:
                idx = np.searchsorted(signal.timestamps, new_timestamps, side="right")
                idx -= 1
                idx[idx < 0] = 0
                s = signal.samples[idx]
                if invalidation_bits is not None:
                    invalidation_bits = invalidation_bits[idx]
            else:

                kind = signal.samples.dtype.kind

                if kind == "f":

                    if (
                        float_interpolation_mode
                        == FloatInterpolation.REPEAT_PREVIOUS_SAMPLE
                    ):
                        idx = np.searchsorted(
                            signal.timestamps, new_timestamps, side="right"
                        )
                        idx -= 1
                        idx[idx < 0] = 0
                        s = signal.samples[idx]

                        if invalidation_bits is not None:
                            invalidation_bits = invalidation_bits[idx]

                    else:
                        s = np.interp(new_timestamps, signal.timestamps, signal.samples)

                        if invalidation_bits is not None:
                            idx = np.searchsorted(
                                signal.timestamps, new_timestamps, side="right"
                            )
                            idx -= 1
                            idx[idx < 0] = 0
                            invalidation_bits = invalidation_bits[idx]

                elif kind in "ui":
                    if (
                        integer_interpolation_mode
                        == IntegerInterpolation.HYBRID_INTERPOLATION
                    ):
                        if signal.raw and signal.conversion:
                            kind = signal.conversion.convert(
                                signal.samples[:1]
                            ).dtype.kind
                            if kind == "f":
                                integer_interpolation_mode = (
                                    IntegerInterpolation.LINEAR_INTERPOLATION
                                )

                    if (
                        integer_interpolation_mode
                        == IntegerInterpolation.HYBRID_INTERPOLATION
                    ):
                        integer_interpolation_mode = (
                            IntegerInterpolation.REPEAT_PREVIOUS_SAMPLE
                        )

                    if (
                        integer_interpolation_mode
                        == IntegerInterpolation.LINEAR_INTERPOLATION
                    ):
                        s = np.interp(
                            new_timestamps, signal.timestamps, signal.samples
                        ).astype(signal.samples.dtype)

                        if invalidation_bits is not None:
                            idx = np.searchsorted(
                                signal.timestamps, new_timestamps, side="right"
                            )
                            idx -= 1
                            idx[idx < 0] = 0
                            invalidation_bits = invalidation_bits[idx]

                    elif (
                        integer_interpolation_mode
                        == IntegerInterpolation.REPEAT_PREVIOUS_SAMPLE
                    ):
                        idx = np.searchsorted(
                            signal.timestamps, new_timestamps, side="right"
                        )
                        idx -= 1
                        idx[idx < 0] = 0

                        s = signal.samples[idx]

                        if invalidation_bits is not None:
                            invalidation_bits = invalidation_bits[idx]

                else:
                    idx = np.searchsorted(
                        signal.timestamps, new_timestamps, side="right"
                    )
                    idx -= 1
                    idx[idx < 0] = 0
                    s = signal.samples[idx]

                    if invalidation_bits is not None:
                        invalidation_bits = invalidation_bits[idx]

            return Signal(
                s,
                new_timestamps,
                self.unit,
                self.name,
                comment=self.comment,
                conversion=self.conversion,
                source=self.source,
                raw=self.raw,
                master_metadata=self.master_metadata,
                display_names=self.display_names,
                attachment=self.attachment,
                stream_sync=self.stream_sync,
                invalidation_bits=invalidation_bits,
                encoding=self.encoding,
                group_index=self.group_index,
                channel_index=self.channel_index,
            )

    def __apply_func(
        self, other: Signal | NDArray[Any] | None, func_name: str
    ) -> Signal:
        """delegate operations to the *samples* attribute, but in a time
        correct manner by considering the *timestamps*

        """

        if isinstance(other, Signal):
            if len(self) and len(other):
                start = max(self.timestamps[0], other.timestamps[0])
                stop = min(self.timestamps[-1], other.timestamps[-1])
                s1 = self.physical().cut(start, stop)
                s2 = other.physical().cut(start, stop)
            else:
                s1 = self
                s2 = other
            time = np.union1d(s1.timestamps, s2.timestamps)
            s = s1.interp(time).samples
            o = s2.interp(time).samples
            func = getattr(s, func_name)
            conversion = None
            s = func(o)
        elif other is None:
            s = self.samples
            conversion = self.conversion
            time = self.timestamps
        else:
            func = getattr(self.samples, func_name)
            s = func(other)
            conversion = self.conversion
            time = self.timestamps
        return Signal(
            samples=s,
            timestamps=time,
            unit=self.unit,
            name=self.name,
            conversion=conversion,
            raw=self.raw,
            master_metadata=self.master_metadata,
            display_names=self.display_names,
            attachment=self.attachment,
            stream_sync=self.stream_sync,
            invalidation_bits=self.invalidation_bits,
            source=self.source,
            encoding=self.encoding,
            group_index=self.group_index,
            channel_index=self.channel_index,
        )

    def __pos__(self) -> Signal:
        return self

    def __neg__(self) -> Signal:
        return Signal(
            np.negative(self.samples),
            self.timestamps,
            unit=self.unit,
            name=self.name,
            conversion=self.conversion,
            raw=self.raw,
            master_metadata=self.master_metadata,
            display_names=self.display_names,
            attachment=self.attachment,
            stream_sync=self.stream_sync,
            invalidation_bits=self.invalidation_bits,
            source=self.source,
            encoding=self.encoding,
        )

    def __round__(self, n: int) -> Signal:
        return Signal(
            np.around(self.samples, n),
            self.timestamps,
            unit=self.unit,
            name=self.name,
            conversion=self.conversion,
            raw=self.raw,
            master_metadata=self.master_metadata,
            display_names=self.display_names,
            attachment=self.attachment,
            stream_sync=self.stream_sync,
            invalidation_bits=self.invalidation_bits,
            source=self.source,
            encoding=self.encoding,
        )

    def __sub__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__sub__")

    def __isub__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__sub__(other)

    def __rsub__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return -self.__sub__(other)

    def __add__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__add__")

    def __iadd__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__add__(other)

    def __radd__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__add__(other)

    def __truediv__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__truediv__")

    def __itruediv__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__truediv__(other)

    def __rtruediv__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__rtruediv__")

    def __mul__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__mul__")

    def __imul__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__mul__(other)

    def __rmul__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__mul__(other)

    def __floordiv__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__floordiv__")

    def __ifloordiv__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__truediv__(other)

    def __rfloordiv__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return 1 / self.__apply_func(other, "__rfloordiv__")

    def __mod__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__mod__")

    def __pow__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__pow__")

    def __and__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__and__")

    def __or__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__or__")

    def __xor__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__xor__")

    def __invert__(self) -> Signal:
        s = ~self.samples
        time = self.timestamps
        return Signal(
            s,
            time,
            unit=self.unit,
            name=self.name,
            conversion=self.conversion,
            raw=self.raw,
            master_metadata=self.master_metadata,
            display_names=self.display_names,
            attachment=self.attachment,
            stream_sync=self.stream_sync,
            invalidation_bits=self.invalidation_bits,
            source=self.source,
            encoding=self.encoding,
        )

    def __lshift__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__lshift__")

    def __rshift__(self, other: Signal | NDArray[Any] | None) -> Signal:
        return self.__apply_func(other, "__rshift__")

    def __lt__(self, other: Signal | NDArray[Any] | None) -> bool:
        return self.__apply_func(other, "__lt__")

    def __le__(self, other: Signal | NDArray[Any] | None) -> bool:
        return self.__apply_func(other, "__le__")

    def __gt__(self, other: Signal | NDArray[Any] | None) -> bool:
        return self.__apply_func(other, "__gt__")

    def __ge__(self, other: Signal | NDArray[Any] | None) -> bool:
        return self.__apply_func(other, "__ge__")

    def __eq__(self, other: Signal | NDArray[Any] | None) -> bool:
        return self.__apply_func(other, "__eq__")

    def __ne__(self, other: Signal | NDArray[Any] | None) -> bool:
        return self.__apply_func(other, "__ne__")

    def __iter__(self) -> Iterator[Any]:
        for item in (self.samples, self.timestamps, self.unit, self.name):
            yield item

    def __reversed__(self) -> Iterator[tuple[int, tuple[Any, Any]]]:
        return enumerate(zip(reversed(self.samples), reversed(self.timestamps)))

    def __len__(self) -> int:
        return len(self.samples)

    def __abs__(self) -> Signal:
        return Signal(
            np.fabs(self.samples),
            self.timestamps,
            unit=self.unit,
            name=self.name,
            conversion=self.conversion,
            raw=self.raw,
            master_metadata=self.master_metadata,
            display_names=self.display_names,
            attachment=self.attachment,
            stream_sync=self.stream_sync,
            invalidation_bits=self.invalidation_bits,
            source=self.source,
        )

    def __getitem__(self, val: int) -> Any:
        return self.samples[val]

    def __setitem__(self, idx: int, val: Any) -> None:
        self.samples[idx] = val

    def astype(self, np_type: DTypeLike) -> Signal:
        """returns new *Signal* with samples of dtype *np_type*

        Parameters
        ----------
        np_type : np.dtype
            new numpy dtye

        Returns
        -------
        signal : Signal
            new *Signal* with the samples of *np_type* dtype

        """
        return Signal(
            self.samples.astype(np_type),
            self.timestamps,
            unit=self.unit,
            name=self.name,
            conversion=self.conversion,
            raw=self.raw,
            master_metadata=self.master_metadata,
            display_names=self.display_names,
            attachment=self.attachment,
            stream_sync=self.stream_sync,
            invalidation_bits=self.invalidation_bits,
            source=self.source,
            encoding=self.encoding,
        )

    def physical(self) -> Signal:
        """
        get the physical samples values

        Returns
        -------
        phys : Signal
            new *Signal* with physical values

        """

        if not self.raw or self.conversion is None:
            samples = self.samples.copy()
            encoding = None
        else:
            samples = self.conversion.convert(self.samples)
            if samples.dtype.kind == "S":
                encoding = "utf-8" if self.conversion.id == b"##CC" else "latin-1"
            else:
                encoding = None

        return Signal(
            samples,
            self.timestamps.copy(),
            unit=self.unit,
            name=self.name,
            conversion=None,
            raw=False,
            master_metadata=self.master_metadata,
            display_names=self.display_names,
            attachment=self.attachment,
            stream_sync=self.stream_sync,
            invalidation_bits=self.invalidation_bits,
            source=self.source,
            encoding=encoding,
            group_index=self.group_index,
            channel_index=self.channel_index,
        )

    def validate(self, copy: bool = True) -> Signal:
        """appply invalidation bits if they are available for this signal

        Parameters
        ----------
        copy (True) : bool
            return a copy of the result

            .. versionadded:: 5.12.0

        """
        if self.invalidation_bits is None:
            signal = self

        else:
            idx = np.nonzero(~self.invalidation_bits)[0]
            signal = Signal(
                self.samples[idx],
                self.timestamps[idx],
                self.unit,
                self.name,
                self.conversion,
                self.comment,
                self.raw,
                self.master_metadata,
                self.display_names,
                self.attachment,
                self.source,
                self.bit_count,
                self.stream_sync,
                invalidation_bits=None,
                encoding=self.encoding,
                group_index=self.group_index,
                channel_index=self.channel_index,
            )

        if copy:
            signal = signal.copy()

        return signal

    def copy(self) -> Signal:
        """copy all attributes to a new Signal"""
        return Signal(
            self.samples.copy(),
            self.timestamps.copy(),
            self.unit,
            self.name,
            self.conversion,
            self.comment,
            self.raw,
            self.master_metadata,
            self.display_names,
            self.attachment,
            self.source,
            self.bit_count,
            self.stream_sync,
            invalidation_bits=self.invalidation_bits.copy()
            if self.invalidation_bits is not None
            else None,
            encoding=self.encoding,
            group_index=self.group_index,
            channel_index=self.channel_index,
        )
import bz2
import csv
import fnmatch
import gzip
import logging
import os
import re
import sys
import xml.etree.ElementTree as ET
import zipfile
import numpy as np
import pandas as pd
                import snappy
from collections import defaultdict, OrderedDict
from collections.abc import Iterable, Iterator, Sequence
from copy import deepcopy
from datetime import datetime, timezone
from enum import Enum
from functools import reduce
from io import BytesIO
from pathlib import Path
from shutil import copy, move
from struct import unpack
from tempfile import gettempdir, mkdtemp
from traceback import format_exc
from types import TracebackType
from typing import Any, overload, Type
from warnings import warn
from canmatrix import CanMatrix
from numpy.typing import NDArray
from typing_extensions import Literal
from .blocks import v2_v3_constants as v23c
from .blocks import v4_constants as v4c
from .blocks.bus_logging_utils import extract_mux
from .blocks.conversion_utils import from_dict
from .blocks.mdf_v2 import MDF2
from .blocks.mdf_v3 import MDF3
from .blocks.mdf_v4 import MDF4
from .blocks.options import FloatInterpolation, IntegerInterpolation
from .blocks.source_utils import Source
from .blocks.utils import (
    components,
    csv_bytearray2hex,
    csv_int2hex,
    downcast,
    is_file_like,
    load_can_database,
    master_using_raster,
    matlab_compatible,
    MDF2_VERSIONS,
    MDF3_VERSIONS,
    MDF4_VERSIONS,
    MdfException,
    plausible_timestamps,
    randomized_string,
    SUPPORTED_VERSIONS,
    UINT16_u,
    UINT64_u,
    UniqueDB,
    validate_version_argument,
)
from .blocks.v2_v3_blocks import ChannelConversion as ChannelConversionV3
from .blocks.v2_v3_blocks import ChannelExtension
from .blocks.v2_v3_blocks import HeaderBlock as HeaderV3
from .blocks.v4_blocks import ChannelConversion as ChannelConversionV4
from .blocks.v4_blocks import EventBlock, FileHistory, FileIdentificationBlock
from .blocks.v4_blocks import HeaderBlock as HeaderV4
from .blocks.v4_blocks import SourceInformation
from .signal import Signal
from .types import (
    BusType,
    ChannelGroupType,
    ChannelsType,
    DbcFileType,
    EmptyChannelsType,
    FloatInterpolationModeType,
    InputType,
    IntInterpolationModeType,
    MDF_v2_v3_v4,
    RasterType,
    ReadableBufferType,
    StrOrBytesPathType,
    StrPathType,
)
from .version import __version__
                from fastparquet import write as write_parquet
                from h5py import File as HDF5
                    from hdf5storage import savemat
                    from scipy.io import savemat

__all__ = ["MDF", "SUPPORTED_VERSIONS"]


class SearchMode(Enum):
    plain = "plain"
    regex = "regex"
    wildcard = "wildcard"


def get_measurement_timestamp_and_version(
    mdf: ReadableBufferType,
) -> tuple[datetime, str]:
    id_block = FileIdentificationBlock(address=0, stream=mdf)

    version = id_block.mdf_version
    if version >= 400:
        header = HeaderV4
    else:
        header = HeaderV3

    header = header(address=64, stream=mdf)
    main_version, revision = divmod(version, 100)
    version = f"{main_version}.{revision}"

    return header.start_time, version


def get_temporary_filename(
    path: Path = Path("temporary.mf4"), dir: str | Path | None = None
) -> Path:
    if not dir:
        folder = gettempdir()
    else:
        folder = dir
    mf4_path = path.with_suffix(".mf4")
    idx = 0
    while True:
        tmp_path = (Path(folder) / mf4_path.name).with_suffix(f".{idx}.mf4")
        if not tmp_path.exists():
            break
        else:
            idx += 1

    return tmp_path


class MDF:
    """Unified access to MDF v3 and v4 files. Underlying _mdf's attributes and
    methods are linked to the `MDF` object via *setattr*. This is done to expose
    them to the user code and for performance considerations.

    Parameters
    ----------
    name : string | BytesIO | zipfile.ZipFile | bz2.BZ2File | gzip.GzipFile
        mdf file name (if provided it must be a real file name), file-like object or
        compressed file opened as Python object

        .. versionchanged:: 6.2.0

            added support for zipfile.ZipFile, bz2.BZ2File and gzip.GzipFile

    version : string
        mdf file version from ('2.00', '2.10', '2.14', '3.00', '3.10', '3.20',
        '3.30', '4.00', '4.10', '4.11', '4.20'); default '4.10'. This argument is
        only used for MDF objects created from scratch; for MDF objects created
        from a file the version is set to file version

    channels (None) : iterable
        channel names that will used for selective loading. This can dramatically
        improve the file loading time. Default None -> load all channels

        .. versionadded:: 6.1.0

        .. versionchanged:: 6.3.0 make the default None


    callback (\*\*kwargs) : function
        keyword only argument: function to call to update the progress; the
        function must accept two arguments (the current progress and maximum
        progress value)
    use_display_names (\*\*kwargs) : bool
        keyword only argument: for MDF4 files parse the XML channel comment to
        search for the display name; XML parsing is quite expensive so setting
        this to *False* can decrease the loading times very much; default
        *False*
    remove_source_from_channel_names (\*\*kwargs) : bool
        remove source from channel names ("Speed\XCP3" -> "Speed")
    copy_on_get (\*\*kwargs) : bool
        copy arrays in the get method; default *True*
    expand_zippedfile (\*\*kwargs) : bool
        only for bz2.BZ2File and gzip.GzipFile, load the file content into a
        BytesIO before parsing (avoids the huge performance penalty of doing
        random reads from the zipped file); default *True*
    raise_on_multiple_occurrences (\*\*kwargs) : bool
        raise exception when there are multiple channel occurrences in the file and
        the `get` call is ambiguous; default True

        .. versionadded:: 7.0.0

    temporary_folder (\*\*kwargs) : str | pathlib.Path
        folder to use for temporary files

        .. versionadded:: 7.0.0

    Examples
    --------
    >>> mdf = MDF(version='3.30') # new MDF object with version 3.30
    >>> mdf = MDF('path/to/file.mf4') # MDF loaded from file
    >>> mdf = MDF(BytesIO(data)) # MDF from file contents
    >>> mdf = MDF(zipfile.ZipFile('data.zip')) # MDF creating using the first valid MDF from archive
    >>> mdf = MDF(bz2.BZ2File('path/to/data.bz2', 'rb')) # MDF from bz2 object
    >>> mdf = MDF(gzip.GzipFile('path/to/data.gzip', 'rb')) # MDF from gzip object

    """

    _terminate = False

    def __init__(
        self,

        name: InputType | None = None,