Skip to content

Conversation

@ckuethe
Copy link
Contributor

@ckuethe ckuethe commented Apr 29, 2025

The read request should be encoded as int(n)+list[int(vsfr]); telling the device first how many VSFRs you wish to read, followed by their IDs. Incorrect encoding will crash your device.

Then, depending on which VSFRs you read, you might want to decode them as something other than ints. By way of example, you could retrieve the calibration coefficients by hand with

resp = rc103.batch_read_vsfrs(
    [VSFR.CHN_TO_keV_A0, VSFR.CHN_TO_keV_A1, VSFR.CHN_TO_keV_A2],
	"<4x3f")

Inspecting the result from execute(), I get back the following 16 byte response: '07000000f5dbc4c084f51f40bc5ff339'. Without providing a format that would unhelpfully decode to [7, 3234126837, 1075836292, 972251068]

By providing the format string <4x3f, that decodes to (-6.15185022354126, 2.4993600845336914, 0.00046419899445027113) which are in fact the calibration coefficients for my device. I get the same values from the energy_calib() function, as well as from querying the VSFRs individually.

The read request should be encoded as int(n)+list[int(vsfr]); telling
the device first how many VSFRs you wish to read, followed by their IDs.
Incorrect encoding will crash your device.

Then, depending on which VSFRs you read, you might want to decode them
as something other than ints. By way of example, you could retrieve the
calibration coefficients by hand with

```
resp = rc103.batch_read_vsfrs(
    [VSFR.CHN_TO_keV_A0, VSFR.CHN_TO_keV_A1, VSFR.CHN_TO_keV_A2],
	"<4x3f")
```

Inspecting the result from `execute()`, I get back the following 16 byte
response: '07000000f5dbc4c084f51f40bc5ff339'. Without providing a format
that would unhelpfully decode to [7, 3234126837, 1075836292, 972251068]

By providing the format string `<4x3f`, that decodes to
(-6.15185022354126, 2.4993600845336914, 0.00046419899445027113) which
are in fact the calibration coefficients for my device. I get the same
values from the `energy_calib()` function, as well as from querying the
VSFRs individually.
@ckuethe
Copy link
Contributor Author

ckuethe commented Apr 29, 2025

This will be used to implement the ability to query and set alarms individually or in a batch. Let me know if you want smaller PRs that do one thing each, or if you want a single PR that just adds this all.

@ckuethe
Copy link
Contributor Author

ckuethe commented Apr 30, 2025

May as well include a consumer of batch_read_vfsrs, specifically the ability to set/get alarm limits. I didn't bother to create a generic `batch_write_vsfrs. I'm guessing that there are few situations aside from setting up alarm thresholds where you'd want to change a whole bunch of things on your device all at once. Maybe the display settings, but I'm either connected over bluetooth and looking at the app, or connected over usb and logging to some custom tooling.

@ckuethe ckuethe changed the title Fix batch_read_vsfrs Fix batch_read_vsfrs, and use it to implement alarm level set/get Apr 30, 2025
if unpack_format:
ret = r.unpack(unpack_format)
else:
ret = [r.unpack('<I')[0] for _ in range(len(vsfr_ids) + 1)]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why +1 and 'skip first 4 bytes' in '<4x3f to skip the first 4 bytes,`?
What's inside those 4 bytes?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if it's unknown at the moment, I think it's better to explicitly read it and ignore it (with a comment in the code), and not take it into account in unpack_format / return array values

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the first 4 bytes are a bitfield that indicates whether the requested VSFR is valid. in the example, I requested 3 VSRFs (the ones containing the energy calibration), and those are valid, so 3 bits are set.

the point of unpack_format is to allow users to build their own queries in whatever way they want. <4x3f was simply an example; <I3f or <Ifff are just as valid.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, maybe it's better to unpack only 4 bytes in batch_read_vsfrs, parse it (to list or tuple of bools) and return it with raw response - if we can't distinguish replies to different requests of batch in this function, then I think it's better to construct and parse data on the caller's side.
So, my proposal is:

def batch_read_vsfrs(self, vsfr_ids: list[VSFR]) -> tuple[list[bool], BytesBuffer]

@ckuethe
Copy link
Contributor Author

ckuethe commented May 1, 2025

I'd need to do some experiments to see what happens if I batch_read_vsfrs([ CHN_TO_keV_A0, 0xc0ffee00, CHN_TO_keV_A1, 0xbad1dea, CHN_TO_keV_A2]). I would expect the first 32-bits are 0x15 (0b00010101), but I don't know if I'd get back enough other data to say which reads were valid. Would it only give me back 12 more bytes and I'd have to try correlate the valid bits against number of items unpacked.

I kind of did that with get_alarm_limits though; the expected validity bits are computed, and then checked after the read. That's easy to do when we construct the batch read. Not as easy to inter anticipate https://github.com/cdump/radiacode/pull/48/files#diff-8432e46afca3af1ff9194c636d8c2359c1067eef0a13e9749fcd88f533441c95R415

@cdump
Copy link
Owner

cdump commented May 1, 2025

For [ CHN_TO_keV_A0, 0xc0ffee00, CHN_TO_keV_A1, 0xbad1dea, CHN_TO_keV_A2] I received:

first 4bytes as <I = 0b10101
other part of response as r.unpack('<fff') = (-0.7972345948219299, 2.404771089553833, 0.00039592900429852307) (== energy_calib())

@ckuethe
Copy link
Contributor Author

ckuethe commented May 1, 2025

I'm glad my interpretation of the first 32 bits was correct.

In my head maybe return type is something like a list of tuples or dataclasses:

[
   VSFRData(which=CHN_TO_keV_A0, valid=True, value=-0.7972345948219299),
   VSFRData(which=0xc0ffee00, valid=False, value=None),
   VSFRData(which=CHN_TO_keV_A1, valid=True, value=2.404771089553833),
   VSFRData(which=0xbad1dea, valid=False, value=None),
   VSFRData(which=CHN_TO_keV_A2, valid=True, value=0.00039592900429852307),
]

You asked to read 5 VSFRs, you get back 5 elements, in the same order and same identifier as you asked for. And you get an indicator of whether or not it's data or garbage.

actually, value=None would be sufficient to indicate no valid data for that VSFR. you asked for a thing... I got nothin'.

Callers of batch_read_vsfrs should know the data type of each requested
VSFR, and should therefore be able to specify a correct format string.
Using an inappropriate format string will raise an error.

Require that vsfr_ids is a list of VSFR dataclasses. This prevents the use
of arbitrary integers as a VSFR to read. Using other data types will raise
an error.

Add a check for valid reads. The first item returned by the device is a
bitfield indicating which VSFRs were read successfully. Use this to check
that batch read operation was completely successful. As only the defined
VSFRs are permitted, and those were retrieved from the device, there should
be no reason for any read to fail as it might by retrieving an arbitrary
location (eg. 0xABAD1DEA). If such a failure is detected, an execption is
raised.
@ckuethe ckuethe force-pushed the fix_batch_vsfr_read branch from c681e03 to c85131b Compare May 1, 2025 23:19
@ckuethe
Copy link
Contributor Author

ckuethe commented May 2, 2025

new version - batch read now requires a format string, which should be easy to construct. it only accept a list of defined VSFRs so you can't query an invalid VSFR, and successful read of all requested VSFRs is checked.

@cdump
Copy link
Owner

cdump commented May 2, 2025

I don't like unpack_format[i] - it limits us to a one-character format string per VSFR, and it becomes unusable if any VSFR returns something like "2 floats in response," whether such a case exists now or might be added in the future. Without protocol documentation or insight into the internals of the RadiaCode firmware, we can't guarantee that this won’t happen.

However, we can easily prepare for that possibility, and the API will be even clearer, something like:

def batch_read_vsfrs(self, vsfr_ids: list[tuple[VSFR, str]]) -> list[Optional[Any]]

Your proposal using tuples or dataclasses is even better in terms of clarity, though it might be slightly overkill in this context - it's up to you.

@ckuethe
Copy link
Contributor Author

ckuethe commented May 2, 2025

I was vaguely thinking about adding an unpack format member to the VSFR definition. Then you wouldn't need to pass in a format specifier, you'd just ask for whatever VSFRs you wanted, and you'd get back a list of decoded values, or as close to decoded as we can figure. But that seemed like too much overengineering without a demonstrated need for it.

None of the VSFRs return more than 4 bytes of data when queried. Some appear to only have 1 meaningful byte, though they can be decoded as an int. If you look at the output of the commands() function, you'll see that none of the VSFRs that the device supports have a size greater than 4. All of the more complicated responses, eg. energy_calib() seem to have a dedicated command.

@ckuethe
Copy link
Contributor Author

ckuethe commented May 2, 2025

Test Script

import radiacode
from io import StringIO
import configobj

rc103 = radiacode.RadiaCode()
sio = StringIO(rc103.commands())
commz = configobj.ConfigObj(sio)

for k,v in commz.items():
    v.pop('Virtual', None) # they're all "virtual"...
    print(f"{k} ->  {int(v['Addr'], 16)} : {v}")

Output

VSFR_DEVICE_CTRL ->  1280 : {'Addr': '0x00000500', 'Size': '1'}
VSFR_DEVICE_LANG ->  1282 : {'Addr': '0x00000502', 'DisplayFormat': '1', 'Size': '1'}
VSFR_DEVICE_ON ->  1283 : {'Addr': '0x00000503', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
VSFR_DEVICE_TIME ->  1284 : {'Addr': '0x00000504', 'DisplayFormat': '1', 'Size': '4'}
VSFR_BLE_TX_PWR ->  1792 : {'Addr': '0x00000700', 'DisplayFormat': '1', 'Size': '1'}
VSFR_DISP_CTRL ->  1296 : {'Addr': '0x00000510', 'Size': '1'}
VSFR_DISP_BRT ->  1297 : {'Addr': '0x00000511', 'DisplayFormat': '1', 'Size': '1'}
VSFR_DISP_CONTR ->  1298 : {'Addr': '0x00000512', 'DisplayFormat': '1', 'Size': '1'}
VSFR_DISP_OFF_TIME ->  1299 : {'Addr': '0x00000513', 'DisplayFormat': '1', 'Size': '4'}
VSFR_DISP_ON ->  1300 : {'Addr': '0x00000514', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
VSFR_DISP_DIR ->  1301 : {'Addr': '0x00000515', 'DisplayFormat': '1', 'Size': '1'}
VSFR_DISP_BACKLT_ON ->  1302 : {'Addr': '0x00000516', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
VSFR_SOUND_CTRL ->  1312 : {'Addr': '0x00000520', 'Size': '2'}
VSFR_SOUND_VOL ->  1313 : {'Addr': '0x00000521', 'DisplayFormat': '1', 'Size': '1'}
VSFR_SOUND_ON ->  1314 : {'Addr': '0x00000522', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
VSFR_VIBRO_CTRL ->  1328 : {'Addr': '0x00000530', 'Size': '1'}
VSFR_VIBRO_ON ->  1329 : {'Addr': '0x00000531', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
VSFR_ALARM_MODE ->  1504 : {'Addr': '0x000005E0', 'DisplayFormat': '1', 'Size': '1'}
VSFR_MS_RUN ->  1539 : {'Addr': '0x00000603', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
VSFR_PLAY_SIGNAL ->  1505 : {'Addr': '0x000005E1', 'DisplayFormat': '1', 'Size': '1'}
RC_VSFR_DR_LEV1_uR_h ->  32768 : {'Addr': '0x00008000', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_DR_LEV2_uR_h ->  32769 : {'Addr': '0x00008001', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_DS_LEV1_100uR ->  32770 : {'Addr': '0x00008002', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_DS_LEV2_100uR ->  32771 : {'Addr': '0x00008003', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_DS_LEV1_uR ->  32788 : {'Addr': '0x00008014', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_DS_LEV2_uR ->  32789 : {'Addr': '0x00008015', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_DS_UNITS ->  32772 : {'Addr': '0x00008004', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
RC_VSFR_USE_nSv_h ->  32780 : {'Addr': '0x0000800C', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
RC_VSFR_CR_UNITS ->  32787 : {'Addr': '0x00008013', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
RC_VSFR_CPS_FILTER ->  32773 : {'Addr': '0x00008005', 'DisplayFormat': '1', 'Size': '1'}
RC_VSFR_CR_LEV1_cp10s ->  32776 : {'Addr': '0x00008008', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_CR_LEV2_cp10s ->  32777 : {'Addr': '0x00008009', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_DOSE_RESET ->  32775 : {'Addr': '0x00008007', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
RC_VSFR_CHN_TO_keV_A0 ->  32784 : {'Addr': '0x00008010', 'Type': 'float', 'Size': '4'}
RC_VSFR_CHN_TO_keV_A1 ->  32785 : {'Addr': '0x00008011', 'Type': 'float', 'Size': '4'}
RC_VSFR_CHN_TO_keV_A2 ->  32786 : {'Addr': '0x00008012', 'Type': 'float', 'Size': '4'}
RC_VSFR_CPS ->  32800 : {'Addr': '0x00008020', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_DR_uR_h ->  32801 : {'Addr': '0x00008021', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_DS_uR ->  32802 : {'Addr': '0x00008022', 'DisplayFormat': '1', 'Size': '4'}
RC_VSFR_TEMP_degC ->  32804 : {'Addr': '0x00008024', 'Type': 'float', 'Size': '4'}
RC_VSFR_RAW_TEMP_degC ->  32819 : {'Addr': '0x00008033', 'Type': 'float', 'Size': '4'}
RC_VSFR_TEMP_UP_degC ->  32820 : {'Addr': '0x00008034', 'Type': 'float', 'Size': '4'}
RC_VSFR_TEMP_DN_degC ->  32821 : {'Addr': '0x00008035', 'Type': 'float', 'Size': '4'}
RC_VSFR_ACC_X ->  32805 : {'Addr': '0x00008025', 'DisplayFormat': '1', 'Signed': '1', 'Size': '2'}
RC_VSFR_ACC_Y ->  32806 : {'Addr': '0x00008026', 'DisplayFormat': '1', 'Signed': '1', 'Size': '2'}
RC_VSFR_ACC_Z ->  32807 : {'Addr': '0x00008027', 'DisplayFormat': '1', 'Signed': '1', 'Size': '2'}
RC_VSFR_OPT ->  32808 : {'Addr': '0x00008028', 'DisplayFormat': '1', 'Size': '2'}
RC_VSFR_VBIAS_mV ->  49152 : {'Addr': '0x0000C000', 'DisplayFormat': '1', 'Size': '2'}
RC_VSFR_COMP_LEV ->  49153 : {'Addr': '0x0000C001', 'DisplayFormat': '1', 'Signed': '1', 'Size': '2'}
RC_VSFR_CALIB_MODE ->  49154 : {'Addr': '0x0000C002', 'DisplayFormat': '1', 'Size': '1', 'Min': '0', 'Max': '1'}
VSFR_SYS_MCU_ID0 ->  4294901760 : {'Addr': '0xFFFF0000', 'Size': '4'}
VSFR_SYS_MCU_ID1 ->  4294901761 : {'Addr': '0xFFFF0001', 'Size': '4'}
VSFR_SYS_MCU_ID2 ->  4294901762 : {'Addr': '0xFFFF0002', 'Size': '4'}
VSFR_SYS_DEVICE_ID ->  4294901765 : {'Addr': '0xFFFF0005', 'Size': '4'}
VSFR_SYS_SIGNATURE ->  4294901766 : {'Addr': '0xFFFF0006', 'Size': '4'}
VSFR_SYS_RX_SIZE ->  4294901767 : {'Addr': '0xFFFF0007', 'DisplayFormat': '1', 'Size': '2'}
VSFR_SYS_TX_SIZE ->  4294901768 : {'Addr': '0xFFFF0008', 'DisplayFormat': '1', 'Size': '2'}
VSFR_SYS_BOOT_VERSION ->  4294901769 : {'Addr': '0xFFFF0009', 'Size': '4'}
VSFR_SYS_TARGET_VERSION ->  4294901770 : {'Addr': '0xFFFF000A', 'Size': '4'}
VSFR_SYS_STATUS ->  4294901771 : {'Addr': '0xFFFF000B', 'Size': '4'}
VSFR_SYS_MCU_VREF ->  4294901772 : {'Addr': '0xFFFF000C', 'DisplayFormat': '1', 'Signed': '1', 'Size': '4'}
VSFR_SYS_MCU_TEMP ->  4294901773 : {'Addr': '0xFFFF000D', 'DisplayFormat': '1', 'Signed': '1', 'Size': '4'}
RC_VSFR_DPOT_RDAC ->  49156 : {'Addr': '0x0000C004', 'DisplayFormat': '1', 'Size': '1'}
RC_VSFR_DPOT_RDAC_EEPROM ->  49157 : {'Addr': '0x0000C005', 'DisplayFormat': '1', 'Size': '1'}
RC_VSFR_DPOT_TOLER ->  49158 : {'Addr': '0x0000C006', 'DisplayFormat': '1', 'Size': '1'}

But even the Size:1 items are 4 bytes on the wire.

@cdump
Copy link
Owner

cdump commented May 2, 2025

I'm not 100% in agreement with you on this, but I found an assert related to the unpack_format length, and this function seems to be more of an "internal API" for the library (I don't think many users will use it directly). So I'll go ahead and merge it, and we can fix it later if and when a real issue arises

@cdump cdump merged commit 8e1cdcb into cdump:master May 2, 2025
1 check passed
@ckuethe ckuethe deleted the fix_batch_vsfr_read branch May 2, 2025 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants