From 80aa6d393a214fa97502560b77d279a8afba90c5 Mon Sep 17 00:00:00 2001 From: Chris Kuethe Date: Sun, 4 May 2025 17:16:17 -0700 Subject: [PATCH] Embed our knowledge of VSFR types into the batch reader As best I can tell, radiacode transports all VSFRs as uint32le on the wire, no matter how many information-bearing bits they contain: a bool takes 32 bits, just like a float. Conveniently, the SFR_FILE VS tells us which VSFR IDs are defined, and how to interpret them. It is fairly straightforward to parse the output of the `commands()` function which reads the `SFR_FILE` to produce a dict of format strings. Update `batch_read_vsfrs()` and its callers to omit the format argument, and just use the lookup dict to figure out what format to use. As an initial test, the `get_alarm_limits()` function works correctly, as does the hand-rolled equivalent of `energy_calib()`: `rc.batch_read_vsfrs( [ CHN_TO_keV_A0, CHN_TO_keV_A1, CHN_TO_keV_A2 ] )` ``` import iniconfig cf = iniconfig.IniConfig('', data=radiacode.RadiaCode().commands()) for k in cf.sections: v = dict(cf[k].items()) k = k.removeprefix('RC_').removeprefix('VSFR_') c = None if v.get('Type', "") == 'float': c = 'f' else: if v.get('Size', False) == '1': if v.get('Min', False) == '0' and v.get('Max', False) == '1': c = '3x?' else: c = '3xb' if v.get('Signed', False) == '1' else '3xB' elif v.get('Size', False) == '2': c = '2xh' if v.get('Signed', False) == '1' else '2xH' elif v.get('Size', False) == '4': c = 'i' if v.get('Signed', False) == '1' else 'I' print(f"\tVSFR.{k}: '{c}',") ``` --- radiacode/radiacode.py | 44 +++++++++++++++---------- radiacode/types.py | 73 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 17 deletions(-) diff --git a/radiacode/radiacode.py b/radiacode/radiacode.py index b534061..b9f2e62 100644 --- a/radiacode/radiacode.py +++ b/radiacode/radiacode.py @@ -16,6 +16,7 @@ from radiacode.transports.bluetooth import Bluetooth from radiacode.transports.usb import Usb from radiacode.types import ( + _VSFR_FORMATS, COMMAND, CTRL, VS, @@ -139,41 +140,50 @@ def write_request(self, command_id: int | VSFR, data: Optional[bytes] = None) -> assert retcode == 1 assert r.size() == 0 - def batch_read_vsfrs(self, vsfr_ids: list[VSFR], unpack_format: str) -> list[int | float]: + def batch_read_vsfrs(self, vsfr_ids: list[VSFR]) -> tuple[int | float]: """Read multiple VSFRs Args: vsfr_ids: a list of VSFRs to fetch - unpack_format: a `struct` format string used to unpack the response. - Byte order and word length indicators in unpack_format may be omitted - and will be removed if given, as the device uses standard size little - endian format. - - Repeat count is not supported (use "ffff" instead of "4f") as the length - of the unpack_format string must equal the number of VSFRs being fetched. + Returns a tuple of decoded items in appropriate formats, such as + floating point for calibration, integers for counts, """ nvsfr = len(vsfr_ids) if nvsfr == 0: raise ValueError('No VSFRs specified') - if not all([isinstance(i, VSFR) for i in vsfr_ids]): - raise ValueError('vsfr_ids must be a list of VSFRs') - - unpack_format = unpack_format.strip('@<=>!') - if not (isinstance(unpack_format, str) and len(unpack_format) == nvsfr): - raise ValueError(f'invalid unpack_format `{unpack_format}`') - + # The batch read VSFR command payload is a bunch of little-endian uint32. + # The first one is the number of VSFRs to read, followed by the VSFR ids + # themselves. msg = [struct.pack(' AlarmLimits: VSFR.CR_UNITS, ] - resp = self.batch_read_vsfrs(regs, 'I' * len(regs)) + resp = self.batch_read_vsfrs(regs) dose_multiplier = 100 if resp[6] else 1 count_multiplier = 60 if resp[7] else 1 diff --git a/radiacode/types.py b/radiacode/types.py index b533870..768c30c 100644 --- a/radiacode/types.py +++ b/radiacode/types.py @@ -174,6 +174,10 @@ def __int__(self) -> int: return self.value +# Conveniently, the radiacode tells us what VSFRs are available and what their +# data types are. By reading the SFR_FILE (Special Function Register?) string, +# we get a list of all the SFRs with their address (opcode), their size in bytes, +# their type (int or float), and whether they're signed or not. class VSFR(Enum): DEVICE_CTRL = 0x0500 DEVICE_LANG = 0x0502 @@ -273,6 +277,75 @@ def __int__(self) -> int: return self.value +_VSFR_FORMATS: dict = { + VSFR.DEVICE_CTRL: '3xB', + VSFR.DEVICE_LANG: '3xB', + VSFR.DEVICE_ON: '3x?', + VSFR.DEVICE_TIME: 'I', + VSFR.BLE_TX_PWR: '3xB', + VSFR.DISP_CTRL: '3xB', + VSFR.DISP_BRT: '3xB', + VSFR.DISP_CONTR: '3xB', + VSFR.DISP_OFF_TIME: 'I', + VSFR.DISP_ON: '3x?', + VSFR.DISP_DIR: '3xB', + VSFR.DISP_BACKLT_ON: '3x?', + VSFR.SOUND_CTRL: '2xH', + VSFR.SOUND_VOL: '3xB', + VSFR.SOUND_ON: '3x?', + VSFR.VIBRO_CTRL: '3xB', + VSFR.VIBRO_ON: '3x?', + VSFR.ALARM_MODE: '3xB', + VSFR.MS_RUN: '3x?', + VSFR.PLAY_SIGNAL: '3xB', + VSFR.DR_LEV1_uR_h: 'I', + VSFR.DR_LEV2_uR_h: 'I', + VSFR.DS_LEV1_100uR: 'I', + VSFR.DS_LEV2_100uR: 'I', + VSFR.DS_LEV1_uR: 'I', + VSFR.DS_LEV2_uR: 'I', + VSFR.DS_UNITS: '3x?', + VSFR.USE_nSv_h: '3x?', + VSFR.CR_UNITS: '3x?', + VSFR.CPS_FILTER: '3xB', + VSFR.CR_LEV1_cp10s: 'I', + VSFR.CR_LEV2_cp10s: 'I', + VSFR.DOSE_RESET: '3x?', + VSFR.CHN_TO_keV_A0: 'f', + VSFR.CHN_TO_keV_A1: 'f', + VSFR.CHN_TO_keV_A2: 'f', + VSFR.CPS: 'I', + VSFR.DR_uR_h: 'I', + VSFR.DS_uR: 'I', + VSFR.TEMP_degC: 'f', + VSFR.RAW_TEMP_degC: 'f', + VSFR.TEMP_UP_degC: 'f', + VSFR.TEMP_DN_degC: 'f', + VSFR.ACC_X: '2xh', + VSFR.ACC_Y: '2xh', + VSFR.ACC_Z: '2xh', + VSFR.OPT: '2xH', + VSFR.VBIAS_mV: '2xH', + VSFR.COMP_LEV: '2xh', + VSFR.CALIB_MODE: '3x?', + VSFR.SYS_MCU_ID0: 'I', + VSFR.SYS_MCU_ID1: 'I', + VSFR.SYS_MCU_ID2: 'I', + VSFR.SYS_DEVICE_ID: 'I', + VSFR.SYS_SIGNATURE: 'I', + VSFR.SYS_RX_SIZE: '2xH', + VSFR.SYS_TX_SIZE: '2xH', + VSFR.SYS_BOOT_VERSION: 'I', + VSFR.SYS_TARGET_VERSION: 'I', + VSFR.SYS_STATUS: 'I', + VSFR.SYS_MCU_VREF: 'i', + VSFR.SYS_MCU_TEMP: 'i', + VSFR.DPOT_RDAC: '3xB', + VSFR.DPOT_RDAC_EEPROM: '3xB', + VSFR.DPOT_TOLER: '3xB', +} + + class VS(Enum): CONFIGURATION = 2 FW_DESCRIPTOR = 3