Skip to content

Device API

spectrum_channel_to_energy

spectrum_channel_to_energy(
    channel_number: int, a0: float, a1: float, a2: float
) -> float

Convert spectrometer channel number to energy in keV using quadratic calibration.

Parameters:

Name Type Description Default
channel_number int

Channel number from the spectrometer (integer)

required
a0 float

Constant term coefficient (keV)

required
a1 float

Linear term coefficient (keV/channel)

required
a2 float

Quadratic term coefficient (keV/channel^2)

required

Returns:

Name Type Description
float float

Energy value in keV corresponding to the channel number

Source code in src/radiacode/radiacode.py
def spectrum_channel_to_energy(channel_number: int, a0: float, a1: float, a2: float) -> float:
    """Convert spectrometer channel number to energy in keV using quadratic calibration.

    Args:
        channel_number: Channel number from the spectrometer (integer)
        a0: Constant term coefficient (keV)
        a1: Linear term coefficient (keV/channel)
        a2: Quadratic term coefficient (keV/channel^2)

    Returns:
        float: Energy value in keV corresponding to the channel number
    """
    return a0 + a1 * channel_number + a2 * channel_number * channel_number

RadiaCode

RadiaCode(
    bluetooth_mac: Optional[str] = None,
    serial_number: Optional[str] = None,
    ignore_firmware_compatibility_check: bool = False,
)

A synchronous connection to a RadiaCode device.

Constructing this class opens either a USB or Bluetooth connection and initializes the device. Use it as a context manager whenever possible so that transport resources are released promptly.

Only one transport is selected: supplying bluetooth_mac selects Bluetooth; otherwise USB is used. serial_number only selects between USB devices.

Initialize a RadiaCode device connection.

This constructor establishes a connection to a RadiaCode device either via Bluetooth or USB, initializes the device, and performs firmware compatibility checks.

Parameters:

Name Type Description Default
bluetooth_mac Optional[str]

Optional Bluetooth device identifier. This is normally a MAC address on Linux and Windows, and a CoreBluetooth UUID on macOS.

None
serial_number Optional[str]

Optional USB serial number to connect to a specific device when multiple devices are connected. Used only for USB connections.

None
ignore_firmware_compatibility_check bool

If True, skips the firmware version compatibility check. Default is False.

False

Raises:

Type Description
Exception

If the device firmware version is incompatible (< 4.8) and ignore_firmware_compatibility_check is False.

Note
  • If both bluetooth_mac and serial_number are None, connects to the first available USB device
  • The device is initialized with the current system time
Source code in src/radiacode/radiacode.py
def __init__(
    self,
    bluetooth_mac: Optional[str] = None,
    serial_number: Optional[str] = None,
    ignore_firmware_compatibility_check: bool = False,
):
    """Initialize a RadiaCode device connection.

    This constructor establishes a connection to a RadiaCode device either via Bluetooth
    or USB, initializes the device, and performs firmware compatibility checks.

    Args:
        bluetooth_mac: Optional Bluetooth device identifier. This is normally a MAC
                     address on Linux and Windows, and a CoreBluetooth UUID on macOS.
        serial_number: Optional USB serial number to connect to a specific device when
                     multiple devices are connected. Used only for USB connections.
        ignore_firmware_compatibility_check: If True, skips the firmware version
                                          compatibility check. Default is False.

    Raises:
        Exception: If the device firmware version is incompatible (< 4.8) and
                  ignore_firmware_compatibility_check is False.

    Note:
        - If both bluetooth_mac and serial_number are None, connects to the first
          available USB device
        - The device is initialized with the current system time
    """
    self._seq = 0

    if bluetooth_mac is not None:
        self._connection = Bluetooth(bluetooth_mac)
    else:
        self._connection = Usb(serial_number=serial_number)
    self._finalizer = weakref.finalize(self, self._connection.close)

    try:
        # init
        self.execute(COMMAND.SET_EXCHANGE, b'\x01\xff\x12\xff')
        self.set_local_time(datetime.datetime.now())
        self.device_time(0)
        self._base_time = datetime.datetime.now() + datetime.timedelta(seconds=128)

        (_, (vmaj, vmin, _)) = self.fw_version()
        if ignore_firmware_compatibility_check is False and vmaj < 4 or (vmaj == 4 and vmin < 8):
            raise Exception(
                f'Incompatible firmware version {vmaj}.{vmin}, >=4.8 required. Upgrade device firmware or use radiacode==0.2.2'
            )

        self._spectrum_format_version = 0
        for line in self.configuration().split('\n'):
            if line.startswith('SpecFormatVersion'):
                self._spectrum_format_version = int(line.split('=')[1])
                break
    except BaseException:
        self.close()
        raise

close

close() -> None

Close the device connection and release its resources.

Source code in src/radiacode/radiacode.py
def close(self) -> None:
    """Close the device connection and release its resources."""
    self._finalizer()

base_time

base_time() -> datetime

Return the host-derived base time used for buffered timestamps.

Source code in src/radiacode/radiacode.py
def base_time(self) -> datetime.datetime:
    """Return the host-derived base time used for buffered timestamps."""
    return self._base_time

status

status() -> str

Return the device status flags as a diagnostic string.

Source code in src/radiacode/radiacode.py
def status(self) -> str:
    """Return the device status flags as a diagnostic string."""
    r = self.execute(COMMAND.GET_STATUS)
    flags = r.unpack('<I')
    assert r.size() == 0
    return f'status flags: {flags}'

set_local_time

set_local_time(dt: datetime) -> None

Set the device's local time.

Parameters:

Name Type Description Default
dt datetime

datetime.datetime object containing the time to set on the device. The time components used are: year, month, day, hour, minute, second. Microseconds are ignored.

required
Source code in src/radiacode/radiacode.py
def set_local_time(self, dt: datetime.datetime) -> None:
    """Set the device's local time.

    Args:
        dt: datetime.datetime object containing the time to set on the device.
            The time components used are: year, month, day, hour, minute, second.
            Microseconds are ignored.
    """
    d = struct.pack('<BBBBBBBB', dt.day, dt.month, dt.year - 2000, 0, dt.second, dt.minute, dt.hour, 0)
    self.execute(COMMAND.SET_TIME, d)

fw_signature

fw_signature() -> str

Return a formatted firmware signature and identity string.

Source code in src/radiacode/radiacode.py
def fw_signature(self) -> str:
    """Return a formatted firmware signature and identity string."""
    r = self.execute(COMMAND.FW_SIGNATURE)
    signature = r.unpack('<I')[0]
    filename = r.unpack_string()
    idstring = r.unpack_string()
    return f'Signature: {signature:08X}, FileName="{filename}", IdString="{idstring}"'

fw_version

fw_version() -> tuple[
    tuple[int, int, str], tuple[int, int, str]
]

Get firmware version information.

Returns:

Name Type Description
tuple tuple[tuple[int, int, str], tuple[int, int, str]]

A tuple containing two tuples: - Boot version: (major, minor, date string) - Target version: (major, minor, date string)

Source code in src/radiacode/radiacode.py
def fw_version(self) -> tuple[tuple[int, int, str], tuple[int, int, str]]:
    """Get firmware version information.

    Returns:
        tuple: A tuple containing two tuples:
            - Boot version: (major, minor, date string)
            - Target version: (major, minor, date string)
    """
    r = self.execute(COMMAND.GET_VERSION)
    boot_minor, boot_major = r.unpack('<HH')
    boot_date = r.unpack_string()
    target_minor, target_major = r.unpack('<HH')
    target_date = r.unpack_string()
    assert r.size() == 0
    return ((boot_major, boot_minor, boot_date), (target_major, target_minor, target_date.strip('\x00')))

hw_serial_number

hw_serial_number() -> str

Get hardware serial number.

Returns:

Name Type Description
str str

Hardware serial number formatted as hyphen-separated hexadecimal groups (e.g. "12345678-9ABCDEF0")

Source code in src/radiacode/radiacode.py
def hw_serial_number(self) -> str:
    """Get hardware serial number.

    Returns:
        str: Hardware serial number formatted as hyphen-separated hexadecimal groups
            (e.g. "12345678-9ABCDEF0")
    """
    r = self.execute(COMMAND.GET_SERIAL)
    serial_len = r.unpack('<I')[0]
    assert serial_len % 4 == 0
    serial_groups = [r.unpack('<I')[0] for _ in range(serial_len // 4)]
    assert r.size() == 0
    return '-'.join(f'{v:08X}' for v in serial_groups)

configuration

configuration() -> str

Return the device configuration file decoded as CP1251 text.

Source code in src/radiacode/radiacode.py
def configuration(self) -> str:
    """Return the device configuration file decoded as CP1251 text."""
    r = self.read_request(VS.CONFIGURATION)
    return r.data().decode('cp1251')

text_message

text_message() -> str

Return the current device text message.

Source code in src/radiacode/radiacode.py
def text_message(self) -> str:
    """Return the current device text message."""
    r = self.read_request(VS.TEXT_MESSAGE)
    return r.data().decode('ascii')

serial_number

serial_number() -> str

Get the device serial number.

Returns:

Name Type Description
str str

The device serial number as an ASCII string

Source code in src/radiacode/radiacode.py
def serial_number(self) -> str:
    """Get the device serial number.

    Returns:
        str: The device serial number as an ASCII string
    """
    r = self.read_request(VS.SERIAL_NUMBER)
    return r.data().decode('ascii')

data_buf

data_buf() -> list[
    DoseRateDB | RareData | RealTimeData | RawData | Event
]

Get buffered measurement data from the device.

Source code in src/radiacode/radiacode.py
def data_buf(self) -> list[DoseRateDB | RareData | RealTimeData | RawData | Event]:
    """Get buffered measurement data from the device."""
    r = self.read_request(VS.DATA_BUF)
    return decode_VS_DATA_BUF(r, self._base_time)

spectrum

spectrum() -> Spectrum

Get current spectrum data from the device.

Returns:

Name Type Description
Spectrum Spectrum

Object containing the current spectrum data

Source code in src/radiacode/radiacode.py
def spectrum(self) -> Spectrum:
    """Get current spectrum data from the device.

    Returns:
        Spectrum: Object containing the current spectrum data
    """
    r = self.read_request(VS.SPECTRUM)
    return decode_RC_VS_SPECTRUM(r, self._spectrum_format_version)

spectrum_accum

spectrum_accum() -> Spectrum

Get accumulated spectrum data from the device.

Returns:

Name Type Description
Spectrum Spectrum

Object containing the accumulated spectrum data

Source code in src/radiacode/radiacode.py
def spectrum_accum(self) -> Spectrum:
    """Get accumulated spectrum data from the device.

    Returns:
        Spectrum: Object containing the accumulated spectrum data
    """
    r = self.read_request(VS.SPEC_ACCUM)
    return decode_RC_VS_SPECTRUM(r, self._spectrum_format_version)

dose_reset

dose_reset() -> None

Reset the accumulated dose measurements to zero.

This clears all accumulated dose rate history and statistics.

Source code in src/radiacode/radiacode.py
def dose_reset(self) -> None:
    """Reset the accumulated dose measurements to zero.

    This clears all accumulated dose rate history and statistics.
    """
    self.write_request(VSFR.DOSE_RESET)

spectrum_reset

spectrum_reset() -> None

Reset the current spectrum data to zero.

This clears the current spectrum data buffer, effectively resetting the spectrum measurement to start fresh.

Source code in src/radiacode/radiacode.py
def spectrum_reset(self) -> None:
    """Reset the current spectrum data to zero.

    This clears the current spectrum data buffer, effectively resetting the spectrum
    measurement to start fresh.
    """
    r = self.execute(COMMAND.WR_VIRT_STRING, struct.pack('<II', int(VS.SPECTRUM), 0))
    retcode = r.unpack('<I')[0]
    assert retcode == 1
    assert r.size() == 0

energy_calib

energy_calib() -> list[float]

Get the energy calibration coefficients.

Returns:

Type Description
list[float]

list[float]: List of 3 calibration coefficients [a0, a1, a2] where: - a0: Constant term coefficient (keV) - a1: Linear term coefficient (keV/channel) - a2: Quadratic term coefficient (keV/channel^2)

Source code in src/radiacode/radiacode.py
def energy_calib(self) -> list[float]:
    """Get the energy calibration coefficients.

    Returns:
        list[float]: List of 3 calibration coefficients [a0, a1, a2] where:
            - a0: Constant term coefficient (keV)
            - a1: Linear term coefficient (keV/channel)
            - a2: Quadratic term coefficient (keV/channel^2)
    """
    r = self.read_request(VS.ENERGY_CALIB)
    return list(r.unpack('<fff'))

set_energy_calib

set_energy_calib(coef: list[float]) -> None

Set the energy calibration coefficients.

Parameters:

Name Type Description Default
coef list[float]

List of 3 calibration coefficients [a0, a1, a2] where: - a0: Constant term coefficient (keV) - a1: Linear term coefficient (keV/channel) - a2: Quadratic term coefficient (keV/channel^2)

required
Source code in src/radiacode/radiacode.py
def set_energy_calib(self, coef: list[float]) -> None:
    """Set the energy calibration coefficients.

    Args:
        coef: List of 3 calibration coefficients [a0, a1, a2] where:
            - a0: Constant term coefficient (keV)
            - a1: Linear term coefficient (keV/channel)
            - a2: Quadratic term coefficient (keV/channel^2)
    """
    assert len(coef) == 3
    pc = struct.pack('<fff', *coef)
    r = self.execute(COMMAND.WR_VIRT_STRING, struct.pack('<II', int(VS.ENERGY_CALIB), len(pc)) + pc)
    retcode = r.unpack('<I')[0]
    assert retcode == 1

set_language

set_language(lang: str = 'ru') -> None

Set the device interface language.

Parameters:

Name Type Description Default
lang str

Language code string, either 'ru' for Russian or 'en' for English. Defaults to 'ru'.

'ru'
Source code in src/radiacode/radiacode.py
def set_language(self, lang: str = 'ru') -> None:
    """Set the device interface language.

    Args:
        lang: Language code string, either 'ru' for Russian or 'en' for English.
            Defaults to 'ru'.
    """
    assert lang in {'ru', 'en'}, 'unsupported lang value - use "ru" or "en"'
    self.write_request(VSFR.DEVICE_LANG, struct.pack('<I', bool(lang == 'en')))

set_device_on

set_device_on(on: bool) -> None

Turn the device on or off.

Parameters:

Name Type Description Default
on bool

True to turn device on, False to turn it off

required
Source code in src/radiacode/radiacode.py
def set_device_on(self, on: bool) -> None:
    """Turn the device on or off.

    Args:
        on: True to turn device on, False to turn it off
    """
    self.write_request(VSFR.DEVICE_ON, struct.pack('<I', bool(on)))

set_sound_on

set_sound_on(on: bool) -> None

Enable or disable device sounds.

Parameters:

Name Type Description Default
on bool

True to enable sounds, False to disable

required
Source code in src/radiacode/radiacode.py
def set_sound_on(self, on: bool) -> None:
    """Enable or disable device sounds.

    Args:
        on: True to enable sounds, False to disable
    """
    self.write_request(VSFR.SOUND_ON, struct.pack('<I', bool(on)))

set_vibro_on

set_vibro_on(on: bool) -> None

Enable or disable device vibration.

Parameters:

Name Type Description Default
on bool

True to enable vibration, False to disable

required
Source code in src/radiacode/radiacode.py
def set_vibro_on(self, on: bool) -> None:
    """Enable or disable device vibration.

    Args:
        on: True to enable vibration, False to disable
    """
    self.write_request(VSFR.VIBRO_ON, struct.pack('<I', bool(on)))

set_sound_ctrl

set_sound_ctrl(ctrls: list[CTRL]) -> None

Configure which events trigger device sounds.

Parameters:

Name Type Description Default
ctrls list[CTRL]

List of CTRL enum values specifying which events should trigger sounds

required
Source code in src/radiacode/radiacode.py
def set_sound_ctrl(self, ctrls: list[CTRL]) -> None:
    """Configure which events trigger device sounds.

    Args:
        ctrls: List of CTRL enum values specifying which events should trigger sounds
    """
    flags = 0
    for c in ctrls:
        flags |= int(c)
    self.write_request(VSFR.SOUND_CTRL, struct.pack('<I', flags))

set_display_off_time

set_display_off_time(seconds: int) -> None

Set the display auto-off timeout.

Parameters:

Name Type Description Default
seconds int

Time in seconds before display turns off automatically. Must be one of: 5, 10, 15, or 30 seconds.

required
Source code in src/radiacode/radiacode.py
def set_display_off_time(self, seconds: int) -> None:
    """Set the display auto-off timeout.

    Args:
        seconds: Time in seconds before display turns off automatically.
                Must be one of: 5, 10, 15, or 30 seconds.
    """
    assert seconds in {5, 10, 15, 30}
    v = 3 if seconds == 30 else (seconds // 5) - 1
    self.write_request(VSFR.DISP_OFF_TIME, struct.pack('<I', v))

set_display_brightness

set_display_brightness(brightness: int) -> None

Set the display brightness level.

Parameters:

Name Type Description Default
brightness int

Brightness level from 0 (minimum) to 9 (maximum)

required
Source code in src/radiacode/radiacode.py
def set_display_brightness(self, brightness: int) -> None:
    """Set the display brightness level.

    Args:
        brightness: Brightness level from 0 (minimum) to 9 (maximum)
    """
    assert 0 <= brightness <= 9
    self.write_request(VSFR.DISP_BRT, struct.pack('<I', brightness))

set_display_direction

set_display_direction(direction: DisplayDirection) -> None

Set the display orientation direction.

Parameters:

Name Type Description Default
direction DisplayDirection

DisplayDirection enum value specifying the desired orientation

required
Source code in src/radiacode/radiacode.py
def set_display_direction(self, direction: DisplayDirection) -> None:
    """Set the display orientation direction.

    Args:
        direction: DisplayDirection enum value specifying the desired orientation
    """
    assert isinstance(direction, DisplayDirection)
    self.write_request(VSFR.DISP_DIR, struct.pack('<I', int(direction)))

set_vibro_ctrl

set_vibro_ctrl(ctrls: list[CTRL]) -> None

Configure which events trigger device vibration.

Parameters:

Name Type Description Default
ctrls list[CTRL]

List of CTRL enum values specifying which events should trigger vibration. Note: CTRL.CLICKS is not supported for vibration control.

required
Source code in src/radiacode/radiacode.py
def set_vibro_ctrl(self, ctrls: list[CTRL]) -> None:
    """Configure which events trigger device vibration.

    Args:
        ctrls: List of CTRL enum values specifying which events should trigger vibration.
              Note: CTRL.CLICKS is not supported for vibration control.
    """
    flags = 0
    for c in ctrls:
        assert c != CTRL.CLICKS, 'CTRL.CLICKS not supported for vibro'
        flags |= int(c)
    self.write_request(VSFR.VIBRO_CTRL, struct.pack('<I', flags))

get_alarm_limits

get_alarm_limits() -> AlarmLimits

Return the device's current alarm thresholds and configured units.

Source code in src/radiacode/radiacode.py
def get_alarm_limits(self) -> AlarmLimits:
    """Return the device's current alarm thresholds and configured units."""
    regs = [
        VSFR.CR_LEV1_cp10s,
        VSFR.CR_LEV2_cp10s,
        VSFR.DR_LEV1_uR_h,
        VSFR.DR_LEV2_uR_h,
        VSFR.DS_LEV1_uR,
        VSFR.DS_LEV2_uR,
        VSFR.DS_UNITS,
        VSFR.CR_UNITS,
    ]

    resp = self._batch_read_vsfrs(regs)

    dose_multiplier = 100 if resp[6] else 1
    count_multiplier = 60 if resp[7] else 1
    return AlarmLimits(
        l1_count_rate=resp[0] / 10 * count_multiplier,
        l2_count_rate=resp[1] / 10 * count_multiplier,
        l1_dose_rate=resp[2] / dose_multiplier,
        l2_dose_rate=resp[3] / dose_multiplier,
        l1_dose=resp[4] / 1e6 / dose_multiplier,
        l2_dose=resp[5] / 1e6 / dose_multiplier,
        dose_unit='Sv' if resp[6] else 'R',
        count_unit='cpm' if resp[7] else 'cps',
    )

set_alarm_limits

set_alarm_limits(
    l1_count_rate: int | float | None = None,
    l2_count_rate: int | float | None = None,
    l1_dose_rate: int | float | None = None,
    l2_dose_rate: int | float | None = None,
    l1_dose: int | float | None = None,
    l2_dose: int | float | None = None,
    dose_unit_sv: bool | None = None,
    count_unit_cpm: bool | None = None,
) -> bool

Set one or more alarm limits.

Parameters:

Name Type Description Default
l1_count_rate int | float | None

Count rate at which to raise a level-one alarm.

None
l2_count_rate int | float | None

Count rate at which to raise a level-two alarm.

None
l1_dose_rate int | float | None

Dose rate in micro-units per hour at which to raise a level-one alarm.

None
l2_dose_rate int | float | None

Dose rate in micro-units per hour at which to raise a level-two alarm.

None
l1_dose int | float | None

Accumulated dose in micro-units at which to raise a level-one alarm.

None
l2_dose int | float | None

Accumulated dose in micro-units at which to raise a level-two alarm.

None
dose_unit_sv bool | None

If true, interpret dose values as Sievert and set the display unit to Sievert. If false, use Roentgen. If omitted, do not update the display unit or scale values.

None
count_unit_cpm bool | None

If true, interpret count-rate values as counts per minute and set that display unit. If false, use counts per second. If omitted, do not update the display unit or scale values.

None

Returns:

Type Description
bool

True if the device reports that every supplied value was written.

Raises:

Type Description
ValueError

If no limits are supplied or a supplied limit is negative.

Internally, the device stores count rate in counts/10s and dose in uR. It appears that the device uses a fixed 100 Sv/R conversion.

If count_unit_cpm is not specified, the count rate register(s) will be set to the specified values without any conversion. If it is specified, count rate will be scaled, and the display units register will also be set.

If dose_unit_sv is not specified the dose argument is assumed to be in uR, and the dose alarm register will be set. If dose_unit_sv is true, the dose argument will be assumed to be in uSv, will be converted to uR and stored, and the display unit will be set to Sv. If dose_unit_sv is False, the dose argument will be assumed to be in uR, will be stored as such, and the display unit will be set to R.

Source code in src/radiacode/radiacode.py
def set_alarm_limits(
    self,
    l1_count_rate: int | float | None = None,
    l2_count_rate: int | float | None = None,
    l1_dose_rate: int | float | None = None,
    l2_dose_rate: int | float | None = None,
    l1_dose: int | float | None = None,
    l2_dose: int | float | None = None,
    dose_unit_sv: bool | None = None,
    count_unit_cpm: bool | None = None,
) -> bool:
    """Set one or more alarm limits.

    Args:
        l1_count_rate: Count rate at which to raise a level-one alarm.
        l2_count_rate: Count rate at which to raise a level-two alarm.
        l1_dose_rate: Dose rate in micro-units per hour at which to raise a
            level-one alarm.
        l2_dose_rate: Dose rate in micro-units per hour at which to raise a
            level-two alarm.
        l1_dose: Accumulated dose in micro-units at which to raise a
            level-one alarm.
        l2_dose: Accumulated dose in micro-units at which to raise a
            level-two alarm.
        dose_unit_sv: If true, interpret dose values as Sievert and set the
            display unit to Sievert. If false, use Roentgen. If omitted, do
            not update the display unit or scale values.
        count_unit_cpm: If true, interpret count-rate values as counts per
            minute and set that display unit. If false, use counts per
            second. If omitted, do not update the display unit or scale
            values.

    Returns:
        True if the device reports that every supplied value was written.

    Raises:
        ValueError: If no limits are supplied or a supplied limit is
            negative.

    Internally, the device stores count rate in counts/10s and dose in uR. It
    appears that the device uses a fixed 100 Sv/R conversion.

    If count_unit_cpm is not specified, the count rate register(s) will be set to
    the specified values without any conversion. If it is specified, count rate
    will be scaled, and the display units register will also be set.

    If dose_unit_sv is not specified the dose argument is assumed to be in uR,
    and the dose alarm register will be set. If dose_unit_sv is true, the dose
    argument will be assumed to be in uSv, will be converted to uR and stored,
    and the display unit will be set to Sv.  If dose_unit_sv is False, the dose
    argument will be assumed to be in uR, will be stored as such, and the display
    unit will be set to R.
    """

    which_limits = []
    limit_values = []

    dose_multiplier = 100 if dose_unit_sv is True else 1
    if isinstance(count_unit_cpm, bool):
        count_multiplier = 1 / 6 if count_unit_cpm else 10
    else:
        count_multiplier = 1

    if isinstance(l1_count_rate, (int, float)):
        if l1_count_rate < 0:
            raise ValueError('bad l1_count_rate')
        which_limits.append(VSFR.CR_LEV1_cp10s)
        limit_values.append(round(l1_count_rate * count_multiplier))

    if isinstance(l2_count_rate, (int, float)):
        if l2_count_rate < 0:
            raise ValueError('bad l2_count_rate')
        which_limits.append(VSFR.CR_LEV2_cp10s)
        limit_values.append(round(l2_count_rate * count_multiplier))

    if isinstance(l1_dose_rate, (int, float)):
        if l1_dose_rate < 0:
            raise ValueError('bad l1_dose_rate')
        which_limits.append(VSFR.DR_LEV1_uR_h)
        limit_values.append(round(l1_dose_rate * dose_multiplier))

    if isinstance(l2_dose_rate, (int, float)):
        if l2_dose_rate < 0:
            raise ValueError('bad l2_dose_rate')
        which_limits.append(VSFR.DR_LEV2_uR_h)
        limit_values.append(round(l2_dose_rate * dose_multiplier))

    if isinstance(l1_dose, (int, float)):
        if l1_dose < 0:
            raise ValueError('bad l1_dose')
        which_limits.append(VSFR.DS_LEV1_uR)
        limit_values.append(round(l1_dose * dose_multiplier))

    if isinstance(l2_dose, (int, float)):
        if l2_dose < 0:
            raise ValueError('bad l2_dose')
        which_limits.append(VSFR.DS_LEV2_uR)
        limit_values.append(round(l2_dose * dose_multiplier))

    if isinstance(dose_unit_sv, bool):
        which_limits.append(VSFR.DS_UNITS)
        limit_values.append(int(dose_unit_sv))

    if isinstance(count_unit_cpm, bool):
        which_limits.append(VSFR.CR_UNITS)
        limit_values.append(int(count_unit_cpm))

    num_to_set = len(which_limits)
    if not num_to_set:
        raise ValueError('No limits specified')

    pack_items = [num_to_set] + [int(x) for x in which_limits] + limit_values
    pack_format = f'<I{num_to_set}I{num_to_set}I'
    resp = self.execute(COMMAND.WR_VIRT_SFR_BATCH, struct.pack(pack_format, *pack_items))
    expected_valid = (1 << len(which_limits)) - 1
    return expected_valid == resp.unpack('<I')[0]