///|
/// SII EEPROM word addresses for fixed fields.
/// These are word offsets (each word = 2 bytes).

///|
/// Vendor ID (words 8-9, 32-bit).
let sii_vendor_id : Int = 8

///|
/// Product Code (words 10-11, 32-bit).
let sii_product_code : Int = 10

///|
/// PDI control word (word 0).
let sii_pdi_control : Int = 0

///|
/// PDI config word (word 1).
let sii_pdi_config : Int = 1

///|
/// Sync impulse length (word 2), stored in units of 10 ns.
let sii_sync_impulse_length : Int = 2

///|
/// PDI config 2 word (word 3).
let sii_pdi_config2 : Int = 3

///|
/// Configured station alias (word 4).
let sii_configured_station_alias : Int = 4

///|
/// Preamble checksum (word 7).
let sii_preamble_checksum : Int = 7

///|
/// Revision Number (words 12-13, 32-bit).
let sii_revision : Int = 12

///|
/// Serial Number (words 14-15, 32-bit).
let sii_serial : Int = 14

///|
/// Bootstrap Receive Mailbox Offset (word 20).
let sii_bootstrap_rx_offset : Int = 20

///|
/// Bootstrap Receive Mailbox Size (word 21).
let sii_bootstrap_rx_size : Int = 21

///|
/// Bootstrap Send Mailbox Offset (word 22).
let sii_bootstrap_tx_offset : Int = 22

///|
/// Bootstrap Send Mailbox Size (word 23).
let sii_bootstrap_tx_size : Int = 23

///|
/// Standard Receive Mailbox Offset (word 24).
let sii_std_rx_offset : Int = 24

///|
/// Standard Receive Mailbox Size (word 25).
let sii_std_rx_size : Int = 25

///|
/// Standard Send Mailbox Offset (word 26).
let sii_std_tx_offset : Int = 26

///|
/// Standard Send Mailbox Size (word 27).
let sii_std_tx_size : Int = 27

///|
/// Mailbox Protocol word (word 28).
let sii_mailbox_protocol : Int = 28

///|
/// Firmware Version (word 32).
let sii_firmware_version : Int = 32

///|
/// Hardware Version (word 33).
let sii_hardware_version : Int = 33

///|
/// Production Date (word 34).
let sii_production_date : Int = 34

///|
/// PDI1 Control / ESC Configuration B0 (word 40).
let sii_pdi1_control : Int = 40

///|
/// PDI1 Configuration (word 41).
let sii_pdi1_config : Int = 41

///|
/// Extended PDI1 Configuration (word 43).
let sii_pdi1_config_ext : Int = 43

///|
/// ESC Configuration B4 (word 44).
let sii_esc_config_b4 : Int = 44

///|
/// ESC Configuration B5 (word 45).
let sii_esc_config_b5 : Int = 45

///|
/// ESC Configuration B6 (word 46).
let sii_esc_config_b6 : Int = 46

///|
/// Checksum Configuration Area B (word 47).
let sii_checksum_b : Int = 47

///|
/// Encoded EEPROM size word (word 62).
let sii_eeprom_size : Int = 62

///|
/// SII version word (word 63).
let sii_eeprom_version : Int = 63

///|
/// First category header starts at word 64.
pub let sii_category_start : Int = 64

///|
/// Category type codes.
let sii_cat_strings : UInt = 10U

///|
let sii_cat_datatypes : UInt = 20U

///|
let sii_cat_general : UInt = 30U

///|
let sii_cat_fmmu : UInt = 40U

///|
let sii_cat_sm : UInt = 41U

///|
let sii_cat_fmmux : UInt = 42U

///|
let sii_cat_sync_unit : UInt = 43U

///|
let sii_cat_txpdo : UInt = 50U

///|
let sii_cat_rxpdo : UInt = 51U

///|
let sii_cat_dc : UInt = 60U

///|
let sii_cat_timeouts : UInt = 70U

///|
let sii_cat_dictionary : UInt = 80U

///|
let sii_cat_hardware : UInt = 90U

///|
let sii_cat_vendor_information : UInt = 100U

///|
let sii_cat_images : UInt = 110U

///|
/// End marker for SII categories.
let sii_cat_end : UInt = 0xFFFFU

///|
/// Older synthetic test data used 0x7FFF as the category terminator.
/// Keep accepting it while emitting the standards-compliant 0xFFFF marker.
let sii_cat_legacy_end : UInt = 0x7FFFU

///|
/// Helper: read a little-endian UInt16 from a BytesView at a word offset.
fn read_sii_word(data : BytesView, word_offset : Int) -> UInt {
  let byte_offset = word_offset * 2
  if byte_offset + 1 >= data.length() {
    return 0U
  }
  data[byte_offset].to_int().reinterpret_as_uint() |
  (data[byte_offset + 1].to_int().reinterpret_as_uint() << 8)
}

///|
/// Helper: read a little-endian UInt32 from two consecutive words.
fn read_sii_dword(data : BytesView, word_offset : Int) -> UInt {
  read_sii_word(data, word_offset) |
  (read_sii_word(data, word_offset + 1) << 16)
}

///|
fn read_sii_word_at_byte(data : BytesView, byte_offset : Int) -> UInt {
  if byte_offset + 1 >= data.length() {
    return 0U
  }
  data[byte_offset].to_int().reinterpret_as_uint() |
  (data[byte_offset + 1].to_int().reinterpret_as_uint() << 8)
}

///|
fn read_sii_dword_at_byte(data : BytesView, byte_offset : Int) -> UInt {
  read_sii_word_at_byte(data, byte_offset) |
  (read_sii_word_at_byte(data, byte_offset + 2) << 16)
}

///|
fn read_sii_i16_at_byte(data : BytesView, byte_offset : Int) -> Int {
  let value = read_sii_word_at_byte(data, byte_offset).reinterpret_as_int()
  if value >= 0x8000 {
    value - 0x10000
  } else {
    value
  }
}

///|
fn crc8_update(crc : Int, value : Byte) -> Int {
  let mut current = (crc ^ value.to_int()) & 0xFF
  for _ in 0..<8 {
    current = if (current & 0x80) != 0 {
      ((current << 1) & 0xFF) ^ 0x07
    } else {
      (current << 1) & 0xFF
    }
  }
  current
}

///|
/// Parse the fixed SII preamble.
pub fn parse_sii_preamble(data : BytesView) -> SiiPreamble raise @hal.EcError {
  if data.length() < 16 {
    raise @hal.EcError::SiiReadFailed(
      "SII preamble too short: " + data.length().to_string() + " bytes",
    )
  }
  let mut crc = 0xFF
  for i = 0; i < 16; i = i + 1 {
    crc = crc8_update(crc, data[i])
  }
  {
    pdi_control: read_sii_word(data, sii_pdi_control),
    pdi_config: read_sii_word(data, sii_pdi_config),
    sync_impulse_length_raw: read_sii_word(data, sii_sync_impulse_length),
    pdi_config2: read_sii_word(data, sii_pdi_config2),
    configured_station_alias: read_sii_word(data, sii_configured_station_alias),
    checksum: read_sii_word(data, sii_preamble_checksum),
    checksum_ok: crc == 0,
  }
}

///|
/// Parse the fixed SII header fields from raw EEPROM data.
pub fn parse_sii_header(data : BytesView) -> SiiData raise @hal.EcError {
  // Standard header is complete at word 28. Vendor Info and Area B are optional
  // extensions that may not be present in shorter SII reads.
  if data.length() < 58 {
    raise @hal.EcError::SiiReadFailed(
      "SII data too short: " + data.length().to_string() + " bytes",
    )
  }
  let config_a6 = read_sii_word(data, 6)
  let has_vendor_info = data.length() >= 70
  let has_config_b = data.length() >= 96
  let pdi1_control_word = if has_config_b {
    read_sii_word(data, sii_pdi1_control)
  } else {
    0U
  }
  {
    vendor_id: read_sii_dword(data, sii_vendor_id),
    product_code: read_sii_dword(data, sii_product_code),
    revision: read_sii_dword(data, sii_revision),
    serial: read_sii_dword(data, sii_serial),
    bootstrap_rx_offset: read_sii_word(data, sii_bootstrap_rx_offset),
    bootstrap_rx_size: read_sii_word(data, sii_bootstrap_rx_size),
    bootstrap_tx_offset: read_sii_word(data, sii_bootstrap_tx_offset),
    bootstrap_tx_size: read_sii_word(data, sii_bootstrap_tx_size),
    mailbox_rx_offset: read_sii_word(data, sii_std_rx_offset),
    mailbox_rx_size: read_sii_word(data, sii_std_rx_size),
    mailbox_tx_offset: read_sii_word(data, sii_std_tx_offset),
    mailbox_tx_size: read_sii_word(data, sii_std_tx_size),
    mailbox_protocols: read_sii_word(data, sii_mailbox_protocol),
    firmware_version: if has_vendor_info {
      read_sii_word(data, sii_firmware_version)
    } else {
      0U
    },
    hardware_version: if has_vendor_info {
      read_sii_word(data, sii_hardware_version)
    } else {
      0U
    },
    production_date_raw: if has_vendor_info {
      read_sii_word(data, sii_production_date)
    } else {
      0U
    },
    pdi_control_1: (pdi1_control_word & 0x00FFU).reinterpret_as_int().to_byte(),
    esc_config_b0: ((pdi1_control_word >> 8) & 0x00FFU)
    .reinterpret_as_int()
    .to_byte(),
    pdi_config_1: if has_config_b {
      read_sii_word(data, sii_pdi1_config)
    } else {
      0U
    },
    pdi_config_1_ext: if has_config_b {
      read_sii_word(data, sii_pdi1_config_ext)
    } else {
      0U
    },
    esc_config_b4: if has_config_b {
      read_sii_word(data, sii_esc_config_b4)
    } else {
      0U
    },
    esc_config_b5: if has_config_b {
      read_sii_word(data, sii_esc_config_b5)
    } else {
      0U
    },
    esc_config_b6: if has_config_b {
      read_sii_word(data, sii_esc_config_b6)
    } else {
      0U
    },
    checksum_b: if has_config_b {
      read_sii_word(data, sii_checksum_b)
    } else {
      0U
    },
    esc_config_b_present: has_config_b && (config_a6 & 0x0001U) != 0U,
  }
}

///|
/// Parse standard SII metadata fields after the mailbox configuration block.
pub fn parse_sii_standard_metadata(
  data : BytesView,
) -> SiiStandardMetadata raise @hal.EcError {
  if data.length() < 128 {
    raise @hal.EcError::SiiReadFailed(
      "SII metadata too short: " + data.length().to_string() + " bytes",
    )
  }
  {
    eeprom_size_raw: read_sii_word(data, sii_eeprom_size),
    version: read_sii_word(data, sii_eeprom_version),
  }
}

///|
/// Convert a category type code to the SiiCategory enum.
fn category_from_code(code : UInt) -> SiiCategory {
  match code {
    c if c == sii_cat_strings => Strings
    c if c == sii_cat_datatypes => DataTypes
    c if c == sii_cat_general => General
    c if c == sii_cat_fmmu => Fmmu
    c if c == sii_cat_sm => SyncManager
    c if c == sii_cat_fmmux => FmmuEx
    c if c == sii_cat_sync_unit => SyncUnit
    c if c == sii_cat_txpdo => TxPdo
    c if c == sii_cat_rxpdo => RxPdo
    c if c == sii_cat_dc => Dc
    c if c == sii_cat_timeouts => Timeouts
    c if c == sii_cat_dictionary => Dictionary
    c if c == sii_cat_hardware => Hardware
    c if c == sii_cat_vendor_information => VendorInformation
    c if c == sii_cat_images => Images
    _ => Raw
  }
}

///|
/// Parse all SII categories from the category area of EEPROM data.
pub fn parse_sii_categories(data : BytesView) -> Array[SiiCategoryEntry] {
  let categories : Array[SiiCategoryEntry] = []
  let mut word_offset = sii_category_start
  while word_offset * 2 + 3 < data.length() {
    let cat_type = read_sii_word(data, word_offset)
    let cat_size_words = read_sii_word(data, word_offset + 1)
    if cat_type == sii_cat_end ||
      cat_type == sii_cat_legacy_end ||
      cat_type == 0U {
      break
    }
    let cat_size_bytes = cat_size_words.reinterpret_as_int() * 2
    let cat_data_start = (word_offset + 2) * 2
    if cat_data_start + cat_size_bytes <= data.length() {
      categories.push({
        category: category_from_code(cat_type),
        type_code: cat_type,
        data: data[cat_data_start:cat_data_start + cat_size_bytes],
      })
    }
    word_offset = word_offset + 2 + cat_size_words.reinterpret_as_int()
  }
  categories
}

///|
/// Parse the complete SII model once and expose all commonly used projections.
/// Single-pass dispatch over categories to avoid 10 separate linear scans.
pub fn parse_sii_full_info(data : Bytes) -> SiiFullInfo raise @hal.EcError {
  let view = data[:]
  let preamble = parse_sii_preamble(view)
  let standard = parse_sii_standard_metadata(view)
  let header = parse_sii_header(view)
  let categories = parse_sii_categories(view)
  // Single-pass: collect all category projections in one iteration
  let mut strings : Array[String] = []
  let mut general : SiiGeneral? = None
  let mut timeouts : SiiTimeouts? = None
  let mut fmmu : Array[FmmuDirection] = []
  let mut fmmu_ex : Array[SiiFmmuEx] = []
  let mut sync_managers : Array[SiiSmEntry] = []
  let mut sync_units : Array[SiiSyncUnit] = []
  let tx_pdo : Array[SiiPdo] = []
  let rx_pdo : Array[SiiPdo] = []
  let dc_clocks : Array[SiiDcClock] = []
  for category in categories {
    match category.category {
      Strings => strings = parse_sii_strings(category.data)
      General => {
        let parsed : Result[SiiGeneral, Error] = Result::Ok(
          parse_sii_general(category.data),
        ) catch {
          err => Result::Err(err)
        }
        match parsed {
          Ok(g) => general = Some(g)
          Err(_) => ()
        }
      }
      Timeouts => {
        let parsed : Result[SiiTimeouts, Error] = Result::Ok(
          parse_sii_timeouts(category.data),
        ) catch {
          err => Result::Err(err)
        }
        match parsed {
          Ok(t) => timeouts = Some(t)
          Err(_) => ()
        }
      }
      Fmmu => fmmu = parse_sii_fmmu(category.data)
      FmmuEx => fmmu_ex = parse_sii_fmmu_ex(category.data)
      SyncManager => sync_managers = parse_sii_sm(category.data)
      SyncUnit => sync_units = parse_sii_sync_units(category.data)
      TxPdo =>
        for pdo in parse_sii_pdo(category.data) {
          tx_pdo.push(pdo)
        }
      RxPdo =>
        for pdo in parse_sii_pdo(category.data) {
          rx_pdo.push(pdo)
        }
      Dc => {
        let parsed : Result[SiiDcClock, Error] = Result::Ok(
          parse_sii_dc(category.data),
        ) catch {
          err => Result::Err(err)
        }
        match parsed {
          Ok(clock) => dc_clocks.push(clock)
          Err(_) => ()
        }
      }
      _ => ()
    }
  }
  {
    eeprom: view,
    preamble,
    standard,
    header,
    categories,
    strings,
    general,
    timeouts,
    fmmu,
    fmmu_ex,
    sync_managers,
    sync_units,
    tx_pdo,
    rx_pdo,
    dc_clocks,
  }
}

///|
/// General category data extracted from SII.
pub(all) struct SiiGeneral {
  group_idx : Byte
  image_idx : Byte
  order_idx : Byte
  name_idx : Byte
  coe_details : Byte
  foe_supported : Bool
  eoe_supported : Bool
  soe_supported : Bool
  soe_channels : Byte
  ds402_channels : Byte
  sysman_class : Byte
  flags : Byte
  current_on_ebus : Int
  group_idx_dup : Byte
  physical_port_0 : Byte
  physical_port_1 : Byte
  physical_port_2 : Byte
  physical_port_3 : Byte
  physical_memory_address : UInt
} derive(Eq, Debug)

///|
/// Parse the General category data.
pub fn parse_sii_general(data : BytesView) -> SiiGeneral raise @hal.EcError {
  if data.length() < 16 {
    raise @hal.EcError::EsiParseFailed(
      "General category too short: " + data.length().to_string(),
    )
  }
  {
    group_idx: data[0],
    image_idx: data[1],
    order_idx: data[2],
    name_idx: data[3],
    coe_details: data[5],
    foe_supported: (data[6].to_int() & 0x01) != 0,
    eoe_supported: (data[7].to_int() & 0x01) != 0,
    soe_supported: data[8] != b'\x00',
    soe_channels: if data.length() > 8 {
      data[8]
    } else {
      b'\x00'
    },
    ds402_channels: if data.length() > 9 {
      data[9]
    } else {
      b'\x00'
    },
    sysman_class: if data.length() > 10 {
      data[10]
    } else {
      b'\x00'
    },
    flags: data[11],
    current_on_ebus: read_sii_i16_at_byte(data, 12),
    group_idx_dup: if data.length() > 14 {
      data[14]
    } else {
      b'\x00'
    },
    physical_port_0: if data.length() > 16 {
      data[16] & b'\x0F'
    } else {
      b'\x00'
    },
    physical_port_1: if data.length() > 16 {
      (data[16].to_int() >> 4).to_byte() & b'\x0F'
    } else {
      b'\x00'
    },
    physical_port_2: if data.length() > 17 {
      data[17] & b'\x0F'
    } else {
      b'\x00'
    },
    physical_port_3: if data.length() > 17 {
      (data[17].to_int() >> 4).to_byte() & b'\x0F'
    } else {
      b'\x00'
    },
    physical_memory_address: if data.length() > 19 {
      read_sii_word_at_byte(data, 18)
    } else {
      0U
    },
  }
}

///|
/// Parse the Timeouts category data (ETG.2010 Table 13).
pub fn parse_sii_timeouts(data : BytesView) -> SiiTimeouts raise @hal.EcError {
  if data.length() < 16 {
    raise @hal.EcError::EsiParseFailed(
      "Timeouts category too short: " + data.length().to_string(),
    )
  }
  {
    preop_timeout_ms: read_sii_word_at_byte(data, 0),
    safeop_op_timeout_ms: read_sii_word_at_byte(data, 2),
    back_to_init_timeout_ms: read_sii_word_at_byte(data, 4),
    back_to_safeop_timeout_ms: read_sii_word_at_byte(data, 6),
    mailbox_request_timeout_ms: read_sii_word_at_byte(data, 8),
    mailbox_response_timeout_ms: read_sii_word_at_byte(data, 10),
  }
}

///|
/// Parse the FMMU category data. Each entry is 1 byte indicating FMMU usage.
/// Per ETG1000.6 Table 22: 0x01 = Outputs, 0x02 = Inputs, 0x03 = mailbox state.
pub fn parse_sii_fmmu(data : BytesView) -> Array[FmmuDirection] {
  let result : Array[FmmuDirection] = []
  for i = 0; i < data.length(); i = i + 1 {
    match data[i] {
      b'\x01' => result.push(Outputs)
      b'\x02' => result.push(Inputs)
      b'\x03' => result.push(MailboxState)
      b'\x05' => result.push(DynamicOutputs)
      b'\x06' => result.push(DynamicInputs)
      _ => ()
    }
  }
  result
}

///|
/// Parse the FMMU_EX category data (ETG.2010 Table 10).
pub fn parse_sii_fmmu_ex(data : BytesView) -> Array[SiiFmmuEx] {
  let result : Array[SiiFmmuEx] = []
  let mut offset = 0
  while offset + 4 <= data.length() {
    let flags = read_sii_word_at_byte(data, offset)
    result.push({
      op_only: (flags & 0x0001U) != 0U,
      sm_defined: (flags & 0x0002U) != 0U,
      su_defined: (flags & 0x0004U) != 0U,
      sync_manager: data[offset + 2],
      sync_unit: data[offset + 3],
    })
    offset = offset + 4
  }
  result
}

///|
/// Parse the SyncUnit category data (ETG.2010 Table 14).
pub fn parse_sii_sync_units(data : BytesView) -> Array[SiiSyncUnit] {
  let result : Array[SiiSyncUnit] = []
  for raw_flags in data {
    result.push({
      separate_su: (raw_flags.to_int() & 0x01) != 0,
      separate_frame: (raw_flags.to_int() & 0x02) != 0,
      depend_on_input_state: (raw_flags.to_int() & 0x04) != 0,
      frame_repeat_support: (raw_flags.to_int() & 0x08) != 0,
      raw_flags,
    })
  }
  result
}

///|
/// SM info from SII SM category. Each SM entry is 8 bytes.
pub(all) struct SiiSmEntry {
  physical_start : UInt
  length : UInt
  control : Byte
  status : Byte
  activate : Byte
  sm_type : SmType
} derive(Eq, Debug)

///|
/// Parse the SyncManager category data.
pub fn parse_sii_sm(data : BytesView) -> Array[SiiSmEntry] {
  let result : Array[SiiSmEntry] = []
  let entry_size = 8
  let mut offset = 0
  while offset + entry_size <= data.length() {
    let phys = read_sii_word(data, offset / 2)
    let length = read_sii_word(data, offset / 2 + 1)
    let control = data[offset + 4]
    let status = data[offset + 5]
    let activate = data[offset + 6]
    let type_byte = data[offset + 7]
    let sm_type : SmType = match type_byte {
      b'\x01' => MailboxOut
      b'\x02' => MailboxIn
      b'\x03' => ProcessDataOut
      b'\x04' => ProcessDataIn
      b'\x05' => DynamicProcessDataOut
      b'\x06' => DynamicProcessDataIn
      _ => Unused
    }
    result.push({
      physical_start: phys,
      length,
      control,
      status,
      activate,
      sm_type,
    })
    offset = offset + entry_size
  }
  result
}

///|
/// Parse the Strings category data (ETG1000.6 Table 20).
/// Returns an array of strings indexed from 1 (index 0 is unused by convention).
pub fn parse_sii_strings(data : BytesView) -> Array[String] {
  let result : Array[String] = []
  if data.length() < 1 {
    return result
  }
  let n_strings = data[0].to_int()
  let mut offset = 1
  for _i = 0; _i < n_strings; _i = _i + 1 {
    if offset >= data.length() {
      break
    }
    let str_len = data[offset].to_int()
    offset = offset + 1
    if offset + str_len > data.length() {
      break
    }
    let slice = data[offset:offset + str_len]
    let bytes = slice.to_owned()
    let decoded : Result[String, Error] = Result::Ok(@utf8.decode(bytes)) catch {
      err => Result::Err(err)
    }
    match decoded {
      Ok(text) => result.push(text)
      Err(_) => result.push(bytes.to_unchecked_string())
    }
    offset = offset + str_len
  }
  result
}

///|
/// Parse TxPdo or RxPdo category data (ETG1000.6 Tables 24+25).
/// Each PDO has a fixed 8-byte header followed by nEntry × 8-byte entries.
pub fn parse_sii_pdo(data : BytesView) -> Array[SiiPdo] {
  let result : Array[SiiPdo] = []
  let mut offset = 0
  while offset + 8 <= data.length() {
    let pdo_index = read_sii_word(data, offset / 2)
    let entry_count = data[offset + 2]
    let sync_manager = data[offset + 3]
    let synchronization = data[offset + 4]
    let name_idx = data[offset + 5]
    let flags = read_sii_word(data, (offset + 6) / 2)
    let n_entries = entry_count.to_int()
    offset = offset + 8
    let entries : Array[SiiPdoEntry] = []
    for _j = 0; _j < n_entries; _j = _j + 1 {
      if offset + 8 > data.length() {
        break
      }
      let entry_index = read_sii_word(data, offset / 2)
      let subindex = data[offset + 2]
      let entry_name_idx = data[offset + 3]
      let data_type = data[offset + 4]
      let bit_length = data[offset + 5]
      let entry_flags = read_sii_word(data, (offset + 6) / 2)
      entries.push({
        entry_index,
        subindex,
        name_idx: entry_name_idx,
        data_type,
        bit_length,
        flags: entry_flags,
      })
      offset = offset + 8
    }
    result.push({
      pdo_index,
      entry_count,
      sync_manager,
      synchronization,
      name_idx,
      flags,
      entries,
    })
  }
  result
}

///|
/// Parse a DC clock category entry.
pub fn parse_sii_dc(data : BytesView) -> SiiDcClock raise @hal.EcError {
  if data.length() < 24 {
    raise @hal.EcError::EsiParseFailed(
      "DC category too short: " + data.length().to_string(),
    )
  }
  {
    cycle_time_0: read_sii_dword_at_byte(data, 0),
    shift_time_0: read_sii_dword_at_byte(data, 4),
    shift_time_1: read_sii_dword_at_byte(data, 8),
    sync1_cycle_factor: read_sii_i16_at_byte(data, 12),
    assign_activate: read_sii_word_at_byte(data, 14),
    sync0_cycle_factor: read_sii_i16_at_byte(data, 16),
    name_idx: data[18],
    description_idx: data[19],
  }
}