"""
JD-Xi Device Information Module
This module defines the `DeviceInfo` class, which represents identity information
for a Roland JD-Xi synthesizer. It provides utilities for checking manufacturer
and model identity, extracting firmware version, and constructing an instance
from a MIDI Identity Reply message.
Usage Example:
--------------
>>> identity_data = bytes([0xF0, 0x7E, 0x10, 0x06, 0x02, 0x41, 0x0E, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF7])
>>> device = DeviceInfo.from_identity_reply(identity_data)
>>> device is not None
True
>>> device.version_string
'v0.03'
>>> device.is_jdxi
True
Classes:
--------
- DeviceInfo: Represents JD-Xi device information, including manufacturer, model,
and firmware version. Provides utility properties for checking if the device
is a JD-Xi and formatting the version string.
Methods:
--------
- `from_identity_reply(data: bytes) -> Optional[DeviceInfo]`
Parses a MIDI Identity Reply message to create a `DeviceInfo` instance.
- `is_roland -> bool`
Returns `True` if the device manufacturer is Roland.
- `is_jdxi -> bool`
Returns `True` if the device is identified as a JD-Xi.
- `version_string -> str`
Returns the firmware version as a formatted string.
Dependencies:
-------------
- dataclasses
- typing (List, Optional)
"""
from dataclasses import dataclass
from typing import List, Optional
from picomidi.constant import Midi
from jdxi_editor.jdxi.midi.constant import JDXiMidi
@dataclass
[docs]
class DeviceInfo:
"""JD-Xi device information parser from MIDI Identity Reply."""
[docs]
manufacturer: List[int] # Roland = [0x41]
[docs]
family: List[int] # JD-Xi = [0x0E, 0x03]
[docs]
model: List[int] # JD-Xi model identifier
[docs]
version: List[int] # Software version [Major, Minor, Patch, Build]
@property
[docs]
def is_roland(self) -> bool:
"""Check if the device is from Roland."""
return self.manufacturer == [0x41]
@property
[docs]
def is_jdxi(self) -> bool:
"""Check if the device is a JD-Xi."""
return self.is_roland and self.family == [0x0E, 0x03]
@property
[docs]
def version_string(self) -> str:
"""Format the version number as a string (e.g., 'v1.03')."""
if len(self.version) >= 2:
return f"v{self.version[0]}.{self.version[1]:02d}"
return "unknown"
@property
[docs]
def to_string(self) -> str:
"""Generate a readable string describing the device."""
if self.is_jdxi:
return f"Device: Roland JD-Xi, Firmware: {self.version_string}"
elif self.is_roland:
return f"Device: Roland, Firmware: {self.version_string}"
return "Unknown device"
@classmethod
[docs]
def from_identity_reply(cls, data: bytes) -> Optional["DeviceInfo"]:
"""
Parse an Identity Reply SysEx message into a DeviceInfo object.
:param data: bytes
:return: DeviceInfo
"""
try:
if (
len(data)
< JDXiMidi.SYSEX.IDENTITY.LAYOUT.expected_length() # Minimum length check
or data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.START]
!= Midi.SYSEX.START # SysEx Start
or data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.ID.NUMBER]
!= JDXiMidi.SYSEX.IDENTITY.CONST.NUMBER # 0x7E # Universal Non-Realtime
or data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.ID.SUB1]
!= JDXiMidi.SYSEX.IDENTITY.CONST.SUB1_GENERAL_INFORMATION # 0x06 General Info
or data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.ID.SUB2]
!= JDXiMidi.SYSEX.IDENTITY.CONST.SUB2_IDENTITY_REPLY # 0x02 Identity Reply
):
return None # Invalid Identity Reply
return cls(
device_id=data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.ID.DEVICE],
manufacturer=[
data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.ID.ROLAND]
], # Manufacturer ID (Roland = 0x41)
family=[
data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.DEVICE.FAMILY_CODE_1],
data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.DEVICE.FAMILY_CODE_2],
], # Family Code (JD-Xi = [0x0E, 0x03])
model=[
data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.DEVICE.FAMILY_NUMBER_CODE_1],
data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.DEVICE.FAMILY_NUMBER_CODE_2],
], # Model Number
version=[
data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.SOFTWARE.REVISION_1],
data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.SOFTWARE.REVISION_2],
data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.SOFTWARE.REVISION_3],
data[JDXiMidi.SYSEX.IDENTITY.LAYOUT.SOFTWARE.REVISION_4],
], # Firmware Version
)
except Exception:
return None