diff --git a/pyproject.toml b/pyproject.toml index de6db933..576799c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "h5py", "softioc>=4.4.0", "pandablocks>=0.5.3", - "pvi>=0.6", + "pvi~=0.7.0", ] # Add project dependencies here, e.g. ["click", "numpy"] dynamic = ["version"] license.file = "LICENSE" diff --git a/src/pandablocks_ioc/_pvi.py b/src/pandablocks_ioc/_pvi.py index 0d7686cc..ce74d379 100644 --- a/src/pandablocks_ioc/_pvi.py +++ b/src/pandablocks_ioc/_pvi.py @@ -27,7 +27,7 @@ from softioc import builder from softioc.pythonSoftIoc import RecordWrapper -from ._types import OUT_RECORD_FUNCTIONS, EpicsName +from ._types import OUT_RECORD_FUNCTIONS, EpicsName, epics_to_pvi_name class PviGroup(Enum): @@ -64,18 +64,20 @@ def add_pvi_info( writeable: bool = record_creation_func in OUT_RECORD_FUNCTIONS useComboBox: bool = record_creation_func == builder.mbbOut + pvi_name = epics_to_pvi_name(record_name) + if record_creation_func == builder.Action: if record_name == "PCAP:ARM": component = SignalRW( - record_name, - record_name, - widget=ButtonPanel(actions=dict(Arm=1, Disarm=0)), + name=pvi_name, + pv=record_name, + widget=ButtonPanel(actions=dict(Arm="1", Disarm="0")), read_widget=LED(), ) access = "rw" else: - component = SignalX(record_name, record_name, value="") + component = SignalX(name=pvi_name, pv=record_name, value="") access = "x" elif writeable: if useComboBox: @@ -86,10 +88,10 @@ def add_pvi_info( else: widget = TextWrite(format=None) - component = SignalRW(record_name, record_name, widget) + component = SignalRW(name=pvi_name, pv=record_name, widget=widget) access = "rw" else: - component = SignalR(record_name, record_name, TextRead()) + component = SignalR(name=pvi_name, pv=record_name, widget=TextRead()) access = "r" block, field = record_name.split(":", maxsplit=1) block_name_suffixed = f"pvi.{field.lower().replace(':', '_')}.{access}" @@ -108,29 +110,56 @@ def add_pvi_info( Pvi.add_pvi_info(record_name=record_name, group=group, component=component) -_positions_table_group = Group("POSITIONS_TABLE", Grid(labelled=True), children=[]) +_positions_table_group = Group( + name="PositionsTable", layout=Grid(labelled=True), children=[] +) _positions_table_headers = ["VALUE", "UNITS", "SCALE", "OFFSET", "CAPTURE"] # TODO: Replicate this for the BITS table def add_positions_table_row( - record_name: str, - value_record_name: str, - units_record_name: str, - scale_record_name: str, - offset_record_name: str, - capture_record_name: str, + record_name: EpicsName, + value_record_name: EpicsName, + units_record_name: EpicsName, + scale_record_name: EpicsName, + offset_record_name: EpicsName, + capture_record_name: EpicsName, ) -> None: """Add a Row to the Positions table""" # TODO: Use the Components defined in _positions_columns_defs to # create the children, which will make it more obvious which # component is for which column children = [ - SignalR(value_record_name, value_record_name, TextWrite()), - SignalRW(units_record_name, units_record_name, TextWrite()), - SignalRW(scale_record_name, scale_record_name, TextWrite()), - SignalRW(offset_record_name, offset_record_name, TextWrite()), - SignalRW(capture_record_name, capture_record_name, TextWrite()), + SignalR( + name=epics_to_pvi_name(value_record_name), + label=value_record_name, + pv=value_record_name, + widget=TextRead(), + ), + SignalRW( + name=epics_to_pvi_name(units_record_name), + label=units_record_name, + pv=units_record_name, + widget=TextWrite(), + ), + SignalRW( + name=epics_to_pvi_name(scale_record_name), + label=scale_record_name, + pv=scale_record_name, + widget=TextWrite(), + ), + SignalRW( + name=epics_to_pvi_name(offset_record_name), + label=offset_record_name, + pv=offset_record_name, + widget=TextWrite(), + ), + SignalRW( + name=epics_to_pvi_name(capture_record_name), + label=capture_record_name, + pv=capture_record_name, + widget=TextWrite(), + ), ] row = Row() @@ -138,9 +167,10 @@ def add_positions_table_row( row.header = _positions_table_headers row_group = Group( - record_name, - row, - children, + name=epics_to_pvi_name(record_name), + label=record_name, + layout=row, + children=children, ) _positions_table_group.children.append(row_group) @@ -156,9 +186,9 @@ class Pvi: # to the positions table _general_device_refs = { "CAPTURE": DeviceRef( - "AllPostionCaptureParameters", - "CAPTURE", - "PandA_POSITIONS_TABLE", + name="AllPostionCaptureParameters", + pv="CAPTURE", + ui="PandA_PositionsTable", ) } @@ -205,9 +235,11 @@ def create_pvi_records(record_prefix: str): if PviGroup.NONE in v: children.extend(v.pop(PviGroup.NONE)) for group, components in v.items(): - children.append(Group(group.name, Grid(), components)) + children.append( + Group(name=group.name, layout=Grid(), children=components) + ) - device = Device(block_name, children=children) + device = Device(label=block_name, children=children) devices.append(device) # Add PVI structure. Unfortunately we need something in the database @@ -238,16 +270,25 @@ def create_pvi_records(record_prefix: str): # TODO: Properly add this to list of screens, add a PV, maybe roll into # the "PLACEHOLDER" Device? # Add Tables to a new top level screen - top_device = Device("PandA", children=[_positions_table_group]) + top_device = Device(label="PandA", children=[_positions_table_group]) devices.append(top_device) # Create top level Device, with references to all child Devices - index_device_refs = [ - DeviceRef(x, x, x.replace(":PVI", "")) for x in pvi_records - ] + index_device_refs = [] + for pvi_record in pvi_records: + record_with_no_suffix = EpicsName(pvi_record.replace(":PVI", "")) + name = epics_to_pvi_name(record_with_no_suffix) + index_device_refs.append( + DeviceRef( + name=name, + label=record_with_no_suffix, + pv=pvi_record, + ui=record_with_no_suffix, + ) + ) # # TODO: What should the label be? - device = Device("index", children=index_device_refs) + device = Device(label="index", children=index_device_refs) devices.append(device) # TODO: label widths need some tweaking - some are pretty long right now diff --git a/src/pandablocks_ioc/_tables.py b/src/pandablocks_ioc/_tables.py index e8eeff6b..76f11adf 100644 --- a/src/pandablocks_ioc/_tables.py +++ b/src/pandablocks_ioc/_tables.py @@ -25,6 +25,7 @@ RecordInfo, RecordValue, epics_to_panda_name, + epics_to_pvi_name, trim_description, ) @@ -56,7 +57,7 @@ class TableFieldRecordContainer: def make_bit_order( - table_field_records: Dict[str, TableFieldRecordContainer] + table_field_records: Dict[str, TableFieldRecordContainer], ) -> Dict[str, TableFieldRecordContainer]: return dict( sorted(table_field_records.items(), key=lambda item: item[1].field.bit_low) @@ -144,12 +145,14 @@ def __init__( ) self.all_values_dict = all_values_dict + pvi_table_name = epics_to_pvi_name(table_name) + # The PVI group to put all records into pvi_group = PviGroup.PARAMETERS Pvi.add_pvi_info( table_name, pvi_group, - SignalRW(table_name, table_name, TableWrite([])), + SignalRW(name=pvi_table_name, pv=table_name, widget=TableWrite(widgets=[])), ) # Note that the table_updater's table_fields are guaranteed sorted in bit order, @@ -216,10 +219,11 @@ def __init__( initial_value=TableModeEnum.VIEW.value, on_update=self.update_mode, ) + pvi_name = epics_to_pvi_name(mode_record_name) Pvi.add_pvi_info( mode_record_name, pvi_group, - SignalRW(mode_record_name, mode_record_name, ComboBox()), + SignalRW(name=pvi_name, pv=mode_record_name, widget=ComboBox()), ) self.mode_record_info = RecordInfo(lambda x: x, labels, False) diff --git a/src/pandablocks_ioc/_types.py b/src/pandablocks_ioc/_types.py index 7ae02c36..05d6c8bc 100644 --- a/src/pandablocks_ioc/_types.py +++ b/src/pandablocks_ioc/_types.py @@ -1,5 +1,6 @@ # Various new or derived types/classes and helper functions for the IOC module import logging +import re from dataclasses import dataclass, field from typing import Any, Awaitable, Callable, List, NewType, Optional, Union @@ -20,6 +21,8 @@ class InErrorException(Exception): EpicsName = NewType("EpicsName", str) # PandA format, i.e. "." dividers PandAName = NewType("PandAName", str) +# No dividers and PascalCase +PviName = NewType("PviName", str) def panda_to_epics_name(field_name: PandAName) -> EpicsName: @@ -34,6 +37,20 @@ def epics_to_panda_name(field_name: EpicsName) -> PandAName: return PandAName(field_name.replace(":", ".")) +def epics_to_pvi_name(field_name: EpicsName) -> PviName: + """Converts EPICS naming convention to PVI naming convention. + For example PANDA:PCAP:TRIG_EDGE -> TrigEdge.""" + relevant_section = field_name.split(":")[-1] + words = relevant_section.replace("-", "_").split("_") + capitalised_word = "".join(word.capitalize() for word in words) + + # We don't want to allow any non-alphanumeric characters. + formatted_word = re.search(r"[A-Za-z0-9]+", capitalised_word) + assert formatted_word + + return PviName(formatted_word.group()) + + def device_and_record_to_panda_name(field_name: EpicsName) -> PandAName: """Convert an EPICS naming convention (including Device prefix) to PandA convention.""" diff --git a/tests/test-bobfiles/HDF5.bob b/tests/test-bobfiles/HDF5.bob index 59627584..51d60d04 100644 --- a/tests/test-bobfiles/HDF5.bob +++ b/tests/test-bobfiles/HDF5.bob @@ -34,7 +34,7 @@ true Label - HDF5: File Path + Filepath 0 0 250 @@ -52,7 +52,7 @@ Label - HDF5: File Name + Filename 0 25 250 @@ -70,7 +70,7 @@ Label - HDF5: Num Capture + Numcapture 0 50 250 @@ -87,7 +87,7 @@ Label - HDF5: Flush Period + Flushperiod 0 75 250 @@ -104,7 +104,7 @@ Label - HDF5: Capture + Capture 0 100 250 @@ -129,7 +129,7 @@ true Label - HDF5: Status + Status 0 0 250 @@ -150,7 +150,7 @@ Label - HDF5: Capturing + Capturing 0 25 250 diff --git a/tests/test-bobfiles/PCAP.bob b/tests/test-bobfiles/PCAP.bob index 3ac71343..08454ae4 100644 --- a/tests/test-bobfiles/PCAP.bob +++ b/tests/test-bobfiles/PCAP.bob @@ -34,7 +34,7 @@ true Label - PCAP: LABEL + Label 0 0 250 @@ -52,7 +52,7 @@ Label - PCAP: ARM + Arm 0 25 250 @@ -94,7 +94,7 @@ LED - TEST_PREFIX: + TEST_PREFIX:PCAP:ARM 350 25 20 @@ -102,7 +102,7 @@ Label - PCAP: GATE + Gate 0 50 250 @@ -120,7 +120,7 @@ Label - PCAP: GATE: DELAY + Delay 0 75 250 @@ -145,7 +145,7 @@ true Label - PCAP: TRIG_ EDGE + Trig Edge 0 0 250 diff --git a/tests/test-bobfiles/PULSE.bob b/tests/test-bobfiles/PULSE.bob index fdcf3f19..f1045ecb 100644 --- a/tests/test-bobfiles/PULSE.bob +++ b/tests/test-bobfiles/PULSE.bob @@ -34,7 +34,7 @@ true Label - PULSE: DELAY + Delay 0 0 250 @@ -51,7 +51,7 @@ Label - PULSE: DELAY: UNITS + Units 0 25 250 diff --git a/tests/test-bobfiles/PandA.bob b/tests/test-bobfiles/PandA.bob index 4a8da246..7e9133dd 100644 --- a/tests/test-bobfiles/PandA.bob +++ b/tests/test-bobfiles/PandA.bob @@ -26,7 +26,7 @@ 1 - POSITIONS_ TABLE + Positions Table 5 30 36 diff --git a/tests/test-bobfiles/SEQ.bob b/tests/test-bobfiles/SEQ.bob index 823f3f84..db99613a 100644 --- a/tests/test-bobfiles/SEQ.bob +++ b/tests/test-bobfiles/SEQ.bob @@ -42,7 +42,7 @@ Label - SEQ: TABLE: MODE + Mode 0 205 250 diff --git a/tests/test-bobfiles/index.bob b/tests/test-bobfiles/index.bob index 7a6b2418..77167918 100644 --- a/tests/test-bobfiles/index.bob +++ b/tests/test-bobfiles/index.bob @@ -27,7 +27,7 @@ Label - PCAP: PVI + PCAP 23 30 250 @@ -42,7 +42,7 @@ Open Display - PCAP: PVI + PCAP 278 30 125 @@ -51,7 +51,7 @@ Label - HDF5: PVI + HDF5 23 55 250 @@ -66,7 +66,7 @@ Open Display - HDF5: PVI + HDF5 278 55 125 @@ -75,7 +75,7 @@ Label - SEQ: PVI + SEQ 23 80 250 @@ -90,7 +90,7 @@ Open Display - SEQ: PVI + SEQ 278 80 125 @@ -99,7 +99,7 @@ Label - PULSE: PVI + PULSE 23 105 250 @@ -114,7 +114,7 @@ Open Display - PULSE: PVI + PULSE 278 105 125 diff --git a/tests/test_ioc_system.py b/tests/test_ioc_system.py index 0634a005..bab4d673 100644 --- a/tests/test_ioc_system.py +++ b/tests/test_ioc_system.py @@ -385,10 +385,11 @@ async def test_create_bobfiles_deletes_existing_files_with_clear_bobfiles( non_bobfile.touch() Pvi.configure_pvi(tmp_path, True) + pv = new_random_test_prefix + ":PCAP:TRIG_EDGE" Pvi.add_pvi_info( - new_random_test_prefix + ":PCAP:TRIG_EDGE", + pv, PviGroup.PARAMETERS, - SignalX("TRIG_EDGE", "Falling"), + SignalX(name="TrigEdge", pv=pv, value="Falling"), ) Pvi.create_pvi_records(new_random_test_prefix) diff --git a/tests/test_types.py b/tests/test_types.py index c6cedb9f..232935fe 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,7 +1,10 @@ +import pytest + from pandablocks_ioc._types import ( EpicsName, PandAName, epics_to_panda_name, + epics_to_pvi_name, panda_to_epics_name, trim_description, trim_string_value, @@ -23,6 +26,21 @@ def test_panda_to_epics_and_back_name_conversion() -> None: ) == PandAName("ABC.123.456") +@pytest.mark.parametrize( + "arg_result", + [ + ("WOW:WHAT:A_THINGY", "AThingy"), + ("WOW:WHAT:A-THINGY", "AThingy"), + ("WOW:WHAT:aTHINGY", "Athingy"), + ("WOW:WHAT:A_THINGY123", "AThingy123"), + ("WOW:WHAT:A-THINGY_123", "AThingy123"), + ], +) +def test_epics_to_pvi_name(arg_result): + arg, result = arg_result + assert epics_to_pvi_name(arg) == result + + def test_string_value(): """Test trim_string_values for a few cases""" assert trim_string_value("ABC", "SomeRecordName") == "ABC"