diff --git a/src/pandablocks_ioc/_pvi.py b/src/pandablocks_ioc/_pvi.py
index b19f4e8d..48785a3d 100644
--- a/src/pandablocks_ioc/_pvi.py
+++ b/src/pandablocks_ioc/_pvi.py
@@ -89,6 +89,7 @@ class PviGroup(Enum):
CAPTURE = "Capture"
HDF = "HDF"
TABLE = "Table" # TODO: May not need this anymore
+ VERSIONS = "Versions"
@dataclass
diff --git a/src/pandablocks_ioc/ioc.py b/src/pandablocks_ioc/ioc.py
index 35fae5c0..452ceb80 100644
--- a/src/pandablocks_ioc/ioc.py
+++ b/src/pandablocks_ioc/ioc.py
@@ -14,6 +14,7 @@
Arm,
ChangeGroup,
Disarm,
+ Get,
GetBlockInfo,
GetChanges,
GetFieldInfo,
@@ -66,6 +67,7 @@
trim_description,
trim_string_value,
)
+from ._version import __version__
# TODO: Try turning python.analysis.typeCheckingMode on, as it does highlight a couple
# of possible errors
@@ -175,6 +177,51 @@ def create_softioc(
asyncio.run_coroutine_threadsafe(client.close(), dispatcher.loop).result()
+def get_panda_versions(idn_repsonse: str) -> dict[EpicsName, str]:
+ """Function that parses version info from the PandA's response to the IDN command
+
+ See: https://pandablocks-server.readthedocs.io/en/latest/commands.html#system-commands
+
+ Args:
+ idn_response (str): Response from PandA to Get(*IDN) command
+
+ Returns:
+ dict[EpicsName, str]: Dictionary mapping firmware record name to version
+ """
+
+ # Currently, IDN reports sw, fpga, and rootfs versions
+ firmware_versions = {"PandA SW": "Unknown", "FPGA": "Unknown", "rootfs": "Unknown"}
+
+ # If the *IDN response contains too many keys, break and leave versions as "Unknown"
+ # Since spaces are used to deliminate versions and can also be in the keys and
+ # values, if an additional key is present that we don't explicitly handle,
+ # our approach of using regex matching will not work.
+ if sum(name in idn_repsonse for name in firmware_versions) < idn_repsonse.count(
+ ":"
+ ):
+ logging.error(
+ f"Recieved unexpected version numbers in version string {idn_repsonse}!"
+ )
+ else:
+ for firmware_name in firmware_versions:
+ pattern = re.compile(
+ rf'{re.escape(firmware_name)}:\s*([^:]+?)(?=\s*\b(?: \
+ {"|".join(map(re.escape, firmware_versions))}):|$)'
+ )
+ if match := pattern.search(idn_repsonse):
+ firmware_versions[firmware_name] = match.group(1).strip()
+ logging.info(
+ f"{firmware_name} Version: {firmware_versions[firmware_name]}"
+ )
+ else:
+ logging.warning(f"Failed to get {firmware_name} version information!")
+
+ return {
+ EpicsName(firmware_name.upper().replace(" ", "_")): version
+ for firmware_name, version in firmware_versions.items()
+ }
+
+
async def introspect_panda(
client: AsyncioClient,
) -> tuple[dict[str, _BlockAndFieldInfo], dict[EpicsName, RecordValue]]:
@@ -1824,6 +1871,40 @@ def create_block_records(
return record_dict
+ def create_version_records(self, firmware_versions: dict[EpicsName, str]):
+ """Creates handful of records for tracking versions of IOC/Firmware via EPICS
+
+ Args:
+ firmware_versions (dict[str, str]): Dictionary mapping firmwares to versions
+ """
+
+ system_block_prefix = "SYSTEM"
+
+ ioc_version_record_name = EpicsName(system_block_prefix + ":IOC_VERSION")
+ ioc_version_record = builder.stringIn(
+ ioc_version_record_name, DESC="IOC Version", initial_value=__version__
+ )
+ add_automatic_pvi_info(
+ PviGroup.VERSIONS,
+ ioc_version_record,
+ ioc_version_record_name,
+ builder.stringIn,
+ )
+
+ for firmware_name, version in firmware_versions.items():
+ firmware_record_name = EpicsName(
+ system_block_prefix + f":{firmware_name}_VERSION"
+ )
+ firmware_ver_record = builder.stringIn(
+ firmware_record_name, DESC=firmware_name, initial_value=version
+ )
+ add_automatic_pvi_info(
+ PviGroup.VERSIONS,
+ firmware_ver_record,
+ firmware_record_name,
+ builder.stringIn,
+ )
+
def initialise(self, dispatcher: asyncio_dispatcher.AsyncioDispatcher) -> None:
"""Perform any final initialisation code to create the records. No new
records may be created after this method is called.
@@ -1851,6 +1932,10 @@ async def create_records(
"""Query the PandA and create the relevant records based on the information
returned"""
+ # Get version information from PandA using IDN command
+ idn_response = await client.send(Get("*IDN"))
+ fw_vers_dict = get_panda_versions(idn_response)
+
(panda_dict, all_values_dict) = await introspect_panda(client)
# Dictionary containing every record of every type
@@ -1858,8 +1943,10 @@ async def create_records(
record_factory = IocRecordFactory(client, record_prefix, all_values_dict)
- # For each field in each block, create block_num records of each field
+ # Add records for version of IOC, FPGA, and software to SYSTEM block
+ record_factory.create_version_records(fw_vers_dict)
+ # For each field in each block, create block_num records of each field
for block, panda_info in panda_dict.items():
block_info = panda_info.block_info
values = panda_info.values
diff --git a/tests/fixtures/mocked_panda.py b/tests/fixtures/mocked_panda.py
index e7165d76..533ed76b 100644
--- a/tests/fixtures/mocked_panda.py
+++ b/tests/fixtures/mocked_panda.py
@@ -22,6 +22,7 @@
ChangeGroup,
Command,
Disarm,
+ Get,
GetBlockInfo,
GetChanges,
GetFieldInfo,
@@ -430,6 +431,10 @@ def multiple_seq_responses(table_field_info, table_data_1, table_data_2):
GetChanges is polled at 10Hz if a different command isn't made.
"""
return {
+ command_to_key(Get(field="*IDN")): repeat(
+ "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \
+ rootfs: PandA 3.1a1-1-g22fdd94"
+ ),
command_to_key(
Put(
field="SEQ1.TABLE",
@@ -564,6 +569,10 @@ def no_numbered_suffix_to_metadata_responses(table_field_info, table_data_1):
doesn't have a suffixed number.
"""
return {
+ command_to_key(Get(field="*IDN")): repeat(
+ "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \
+ rootfs: PandA 3.1a1-1-g22fdd94"
+ ),
command_to_key(
Put(
field="SEQ.TABLE",
@@ -639,6 +648,10 @@ def faulty_multiple_pcap_responses():
),
}
return {
+ command_to_key(Get(field="*IDN")): repeat(
+ "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \
+ rootfs: PandA 3.1a1-1-g22fdd94"
+ ),
command_to_key(GetFieldInfo(block="PCAP1", extended_metadata=True)): repeat(
pcap_info
),
@@ -681,6 +694,10 @@ def standard_responses_no_panda_update(table_field_info, table_data_1):
Used to test if the softioc can be started.
"""
return {
+ command_to_key(Get(field="*IDN")): repeat(
+ "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \
+ rootfs: PandA 3.1a1-1-g22fdd94"
+ ),
command_to_key(GetFieldInfo(block="PCAP", extended_metadata=True)): repeat(
{
"TRIG_EDGE": EnumFieldInfo(
@@ -737,6 +754,10 @@ def standard_responses(table_field_info, table_data_1, table_data_2):
GetChanges is polled at 10Hz if a different command isn't made.
"""
return {
+ command_to_key(Get(field="*IDN")): repeat(
+ "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \
+ rootfs: PandA 3.1a1-1-g22fdd94"
+ ),
command_to_key(GetFieldInfo(block="PCAP", extended_metadata=True)): repeat(
{
"TRIG_EDGE": EnumFieldInfo(
diff --git a/tests/test-bobfiles/SYSTEM.bob b/tests/test-bobfiles/SYSTEM.bob
new file mode 100644
index 00000000..f6f58514
--- /dev/null
+++ b/tests/test-bobfiles/SYSTEM.bob
@@ -0,0 +1,128 @@
+
+ SYSTEM
+ 0
+ 0
+ 506
+ 166
+ 4
+ 4
+
+ Title
+ TITLE
+ SYSTEM
+ 0
+ 0
+ 506
+ 25
+
+
+
+
+
+
+
+
+ true
+ 1
+
+
+ VERSIONS
+ 5
+ 30
+ 496
+ 131
+ true
+
+ Label
+ Ioc Version
+ 0
+ 0
+ 250
+ 20
+ $(text)
+
+
+ TextUpdate
+ TEST_PREFIX:SYSTEM:IOC_VERSION
+ 255
+ 0
+ 205
+ 20
+
+
+
+
+ 1
+ 6
+
+
+ Label
+ Panda Sw Version
+ 0
+ 25
+ 250
+ 20
+ $(text)
+
+
+ TextUpdate
+ TEST_PREFIX:SYSTEM:PANDA_SW_VERSION
+ 255
+ 25
+ 205
+ 20
+
+
+
+
+ 1
+ 6
+
+
+ Label
+ Fpga Version
+ 0
+ 50
+ 250
+ 20
+ $(text)
+
+
+ TextUpdate
+ TEST_PREFIX:SYSTEM:FPGA_VERSION
+ 255
+ 50
+ 205
+ 20
+
+
+
+
+ 1
+ 6
+
+
+ Label
+ Rootfs Version
+ 0
+ 75
+ 250
+ 20
+ $(text)
+
+
+ TextUpdate
+ TEST_PREFIX:SYSTEM:ROOTFS_VERSION
+ 255
+ 75
+ 205
+ 20
+
+
+
+
+ 1
+ 6
+
+
+
diff --git a/tests/test-bobfiles/index.bob b/tests/test-bobfiles/index.bob
index 8172b259..c7a1aab3 100644
--- a/tests/test-bobfiles/index.bob
+++ b/tests/test-bobfiles/index.bob
@@ -3,7 +3,7 @@
0
0
488
- 130
+ 155
4
4
@@ -27,7 +27,7 @@
Label
- PCAP
+ SYSTEM
23
30
250
@@ -38,7 +38,7 @@
OpenDisplay
- PCAP.bob
+ SYSTEM.bob
tab
Open Display
@@ -52,7 +52,7 @@
Label
- DATA
+ PCAP
23
55
250
@@ -63,7 +63,7 @@
OpenDisplay
- DATA.bob
+ PCAP.bob
tab
Open Display
@@ -77,7 +77,7 @@
Label
- SEQ
+ DATA
23
80
250
@@ -88,7 +88,7 @@
OpenDisplay
- SEQ.bob
+ DATA.bob
tab
Open Display
@@ -102,7 +102,7 @@
Label
- PULSE
+ SEQ
23
105
250
@@ -113,7 +113,7 @@
OpenDisplay
- PULSE.bob
+ SEQ.bob
tab
Open Display
@@ -125,4 +125,29 @@
20
$(actions)
+
+ Label
+ PULSE
+ 23
+ 130
+ 250
+ 20
+ $(text)
+
+
+ OpenDisplay
+
+
+ PULSE.bob
+ tab
+ Open Display
+
+
+ SubScreen
+ 278
+ 130
+ 205
+ 20
+ $(actions)
+
diff --git a/tests/test_connection.py b/tests/test_connection.py
index c5598080..67f643be 100644
--- a/tests/test_connection.py
+++ b/tests/test_connection.py
@@ -13,6 +13,7 @@
Arm,
ChangeGroup,
Disarm,
+ Get,
GetBlockInfo,
GetChanges,
GetFieldInfo,
@@ -47,6 +48,10 @@ async def test_no_panda_found_connection_error():
def panda_disconnect_responses(table_field_info, table_data_1, table_data_2):
# The responses return nothing, as the panda disconnects after introspection
return {
+ command_to_key(Get(field="*IDN")): repeat(
+ "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \
+ rootfs: PandA 3.1a1-1-g22fdd94"
+ ),
command_to_key(GetFieldInfo(block="PCAP", extended_metadata=True)): repeat(
{
"TRIG_EDGE": EnumFieldInfo(
diff --git a/tests/test_ioc.py b/tests/test_ioc.py
index 6508c17f..faa9e7b2 100644
--- a/tests/test_ioc.py
+++ b/tests/test_ioc.py
@@ -39,6 +39,7 @@
StringRecordLabelValidator,
_RecordUpdater,
_TimeRecordUpdater,
+ get_panda_versions,
update,
)
@@ -840,3 +841,70 @@ class MockConnectionStatus:
# unreliable number of calls to the set method.
record_info.record.set.assert_any_call(True)
record_info.record.set.assert_any_call(0)
+
+
+@pytest.mark.parametrize(
+ "sample_idn_response, expected_output, expected_log_messages",
+ [
+ (
+ "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 "
+ "07d202f8 rootfs: PandA 3.1a1-1-g22fdd94",
+ {
+ EpicsName("PANDA_SW"): "3.0-11-g6422090",
+ EpicsName("FPGA"): "3.0.0C4 86e5f0a2 07d202f8",
+ EpicsName("ROOTFS"): "PandA 3.1a1-1-g22fdd94",
+ },
+ [],
+ ),
+ (
+ "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8",
+ {
+ EpicsName("PANDA_SW"): "3.0-11-g6422090",
+ EpicsName("FPGA"): "3.0.0C4 86e5f0a2 07d202f8",
+ EpicsName("ROOTFS"): "Unknown",
+ },
+ ["Failed to get rootfs version information!"],
+ ),
+ (
+ "PandA SW: 3.0-11-g6422090 rootfs: PandA 3.1a1-1-g22fdd94",
+ {
+ EpicsName("PANDA_SW"): "3.0-11-g6422090",
+ EpicsName("FPGA"): "Unknown",
+ EpicsName("ROOTFS"): "PandA 3.1a1-1-g22fdd94",
+ },
+ ["Failed to get FPGA version information!"],
+ ),
+ (
+ "",
+ {
+ EpicsName("PANDA_SW"): "Unknown",
+ EpicsName("FPGA"): "Unknown",
+ EpicsName("ROOTFS"): "Unknown",
+ },
+ [
+ "Failed to get PandA SW version information!",
+ "Failed to get FPGA version information!",
+ "Failed to get rootfs version information!",
+ ],
+ ),
+ (
+ "FPGA: 3.0.0C4 86e5f0a2 07d202f8 "
+ "Hello World: 12345 rootfs: PandA 3.1a1-1-g22fdd94",
+ {
+ EpicsName("PANDA_SW"): "Unknown",
+ EpicsName("FPGA"): "Unknown",
+ EpicsName("ROOTFS"): "Unknown",
+ },
+ [
+ "Recieved unexpected version numbers",
+ ],
+ ),
+ ],
+)
+def test_get_version_information(
+ sample_idn_response, expected_output, expected_log_messages, caplog
+):
+ parsed_firmware_versions = get_panda_versions(sample_idn_response)
+ assert parsed_firmware_versions == expected_output
+ for log_message in expected_log_messages:
+ assert log_message in caplog.text