///   [2-3]   positionL     0.05mm/unit, uint16 BE  좌측 모터 위치 (엔코더)

///   [14-15] icmdL         mA, int16 BE            좌측 전류 명령 (Icmd.q × 1000)

///   [18-19] ifbL          mA, int16 BE            좌측 피드백 전류 (Ie.q × 1000)

///   [22-23] fLoadL        0.01kg, int16 BE        좌측 계산 부하 (Iq/Scale × 100)

  /// Whether this is a full dev report (36B) or v3 fallback (12B).

  double get positionLMm => PhysicalUnits.reportPositionToMm(positionLRaw);

  double get positionRMm => PhysicalUnits.reportPositionToMm(positionRRaw);

  double get voltage => PhysicalUnits.voltageToVolts(voltageRaw);

      ? 'Report(t=${timeMs}ms, L=${positionLMm}/${positionRMm}mm, '

        'spd=$speedL/${speedR}mm/s, load=${fLoadLKg}/${fLoadRKg}kg, ${voltage}V)'

      : 'Report(t=${timeMs}ms, L=${positionLMm}/${positionRMm}mm, '

  double get positionLMm => PhysicalUnits.positionToMm(positionLRaw);

  double get positionRMm => PhysicalUnits.positionToMm(positionRRaw);

  String toString() => 'Position(L=${positionLMm}mm, R=${positionRMm}mm)';

  VoltageResponse._({required this.rawData, required this.voltageRaw});

/// NOTE: Current MCU (v2.0.0) does NOT send 0x06 response directly.

/// Instead it sends 3 separate packets: 0x66(weight), 0x05(voltage), 0x65(onoff).

/// Those are handled by WeightResponse, VoltageResponse, WeightPowerResponse.

  double get leftWeightKg => PhysicalUnits.weightToKg(leftWeightRaw);

  double get rightWeightKg => PhysicalUnits.weightToKg(rightWeightRaw);

      'SystemInfo(L=${leftWeightKg}kg mode=$leftWeightMode, R=${rightWeightKg}kg mode=$rightWeightMode)';

/// Weight response (WEIGHT_PLUS 0x66 / WEIGHT_MINUS 0x67 / SET_WEIGHT 0x61).

      'Weight(L=${leftWeightKg}kg mode=$leftMode, R=${rightWeightKg}kg mode=$rightMode)';

  WeightPowerResponse._({required this.rawData, required this.onOffStatus});

    return WeightPowerResponse._(rawData: d, onOffStatus: d.isNotEmpty ? d[0] : 0);

  String toString() => 'EccLevel(L=$leftEccRaw, R=$rightEccRaw, err=$errorCode)';

  String toString() => 'WeightMode(L=$leftMode, R=$rightMode, err=$errorCode)';

  factory FirmwareVersionResponse.fromFrame(RoomfitFrame frame) {

  String get version => pre > 0 ? '$major.$minor.$sub-dev.$pre' : '$major.$minor.$sub';

  AutoWeightResponse._({required this.rawData, required this.currentActive});

  String toString() => 'AutoWeightActive(${isActive ? "ON" : "OFF"})';

  factory AutoWeightStatusResponse.fromFrame(RoomfitFrame frame) {

  String toString() => 'AutoWeightStatus(L=$leftStatus, R=$rightStatus)';

  ResetRangeResponse._({required this.rawData, required this.isFail});

    return ResetRangeResponse._(rawData: d, isFail: d.isNotEmpty ? d[0] : 0);

  String toString() => 'ResetRange(${isSuccess ? "OK" : "FAIL"})';

  SetPositionResponse._({required this.rawData, required this.appliedFlag});

    return SetPositionResponse._(rawData: d, appliedFlag: d.isNotEmpty ? d[0] : 0);

  String toString() => 'SetPosition(${isApplied ? "APPLIED" : "NOT_APPLIED"})';

  RelaxZoneResponse._({required this.rawData, required this.activeStatus});

  String toString() => 'RelaxZoneActive(${isActive ? "ON" : "OFF"})';

  SetRangeAckResponse._({required this.rawData, required this.result});

    return SetRangeAckResponse._(rawData: d, result: d.isNotEmpty ? d[0] : 0);

  String toString() => 'SetRangeAck(${isApplied ? "OK" : "FAIL"})';

/// v1.0.4 (23-byte payload): ISR + position + speed + Iq + mode + region

      ? 'DebugReport(ISR avg=${avgUs}µs max=${maxUs}µs usage=$usagePercent% '

        'pos=${positionMmL}/${positionMmR}mm spd=$speedL/${speedR}mm/s)'

      : 'DebugReport(ISR avg=${avgUs}µs max=${maxUs}µs usage=$usagePercent%)';

/// Fail-safe error response (0xF3). MCU sends 4-byte error flag.

  FailSafeErrorResponse._({required this.rawData, required this.errorFlag});

    return FailSafeErrorResponse._(rawData: d, errorFlag: flag);

  String toString() => 'FailSafeError(0x${errorFlag.toRadixString(16)})';

  ForceCalibrateResponse._({required this.rawData, required this.status});

  factory ForceCalibrateResponse.fromFrame(RoomfitFrame frame) {

    return ForceCalibrateResponse._(rawData: d, status: d.isNotEmpty ? d[0] : 0);