Skip to content

Commit 16bca96

Browse files
author
Peter Braun
committed
tiled writer compat
1 parent 0ba81dc commit 16bca96

9 files changed

Lines changed: 905 additions & 236 deletions

File tree

src/secop_ophyd/GenNodeCode.py

Lines changed: 93 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,24 @@
1111
from frappy.client import get_datatype
1212
from frappy.datatypes import CommandType, DataType, EnumType, StructOf
1313
from jinja2 import Environment, PackageLoader, select_autoescape
14-
from ophyd_async.core import SignalR, SignalRW
14+
from ophyd_async.core import SignalR, SignalRW, SignalW
1515
from ophyd_async.core import StandardReadableFormat as Format
1616

1717
from secop_ophyd.SECoPDevices import (
1818
IGNORED_PROPS,
19+
ParameterMemberType,
1920
ParameterType,
2021
PropertyType,
2122
class_from_interface,
2223
)
2324
from secop_ophyd.SECoPSignal import secop_dtype_obj_from_json
2425
from secop_ophyd.util import (
26+
CompositeKind,
2527
SECoPdtype,
2628
build_command_signature,
29+
classify_datatype,
2730
command_dtype_to_annotation_str,
31+
get_composite_members,
2832
secop_enum_name_to_python,
2933
)
3034

@@ -222,6 +226,7 @@ def __init__(self, path: str | None = None, log=None):
222226
self.add_import("typing", "Annotated as A")
223227
self.add_import("ophyd_async.core", "SignalR")
224228
self.add_import("ophyd_async.core", "SignalRW")
229+
self.add_import("ophyd_async.core", "SignalW")
225230
self.add_import("ophyd_async.core", "Command")
226231
self.add_import("ophyd_async.core", "TriggerableCommand")
227232
self.add_import("ophyd_async.core", "StandardReadableFormat as Format")
@@ -230,6 +235,9 @@ def __init__(self, path: str | None = None, log=None):
230235
self.add_import("typing", "Any")
231236
self.add_import("numpy", "ndarray")
232237
self.add_import("secop_ophyd.SECoPDevices", "ParameterType as ParamT")
238+
self.add_import(
239+
"secop_ophyd.SECoPDevices", "ParameterMemberType as ParamMemberT"
240+
)
233241
self.add_import("secop_ophyd.SECoPDevices", "PropertyType as PropT")
234242
# Add necessary Device imports
235243
self.add_import("secop_ophyd.SECoPDevices", "SECoPDevice")
@@ -535,6 +543,28 @@ def from_json_describe(self, json_data: str | dict):
535543

536544
mod_parameters: list[ParameterAttribute] = []
537545

546+
def _camel(identifier: str) -> str:
547+
words = identifier.replace(" ", "_").replace("-", "_").split("_")
548+
return "".join(word.capitalize() for word in words)
549+
550+
def _enum_type_param(
551+
type_param: str | None,
552+
enum_class_name: str,
553+
members: dict,
554+
enum_descr: str,
555+
) -> str | None:
556+
"""If `type_param` is the generic 'StrictEnum', generate a
557+
concrete named enum class for it and return that class's name
558+
instead; otherwise return `type_param` unchanged."""
559+
if not (type_param and "StrictEnum" in type_param):
560+
return type_param
561+
562+
enum_cls = _build_enum_class(enum_class_name, members, enum_descr)
563+
if enum_cls:
564+
module_enum_classes.append(enum_cls)
565+
return enum_class_name
566+
return type_param
567+
538568
for param_name, param_data in parameters.items():
539569

540570
descr = self._normalize_description(param_data.get("description", ""))
@@ -546,7 +576,6 @@ def from_json_describe(self, json_data: str | dict):
546576
)
547577
else:
548578
param_descr = descr
549-
signal_base = SignalR if param_data["readonly"] else SignalRW
550579

551580
format = None
552581

@@ -567,40 +596,74 @@ def from_json_describe(self, json_data: str | dict):
567596
format = format or Format.HINTED_SIGNAL
568597

569598
# Remove "StandardReadable" prefix from format for cleaner annotation
570-
format = (
599+
format_str = (
571600
str(format).removeprefix("StandardReadable") if format else None
572601
)
573602

574-
datainfo = param_data.get("datainfo", {})
575-
576-
# infer the ophyd type from secop datatype
577-
type_param = get_type_param(param_data["datatype"])
578-
579-
# Handle StrictEnum types - generate enum class
580-
if type_param and "StrictEnum" in type_param:
581-
# Generate unique enum class name:
582-
# ModuleClass + ParamName + Enum
583-
param_name_list = (
584-
param_name.replace(" ", "_").replace("-", "_").split("_")
585-
)
603+
raw_datatype = param_data["datatype"]
604+
kind = classify_datatype(raw_datatype)
605+
606+
if kind is CompositeKind.UNSUPPORTED:
607+
# mirrors SECoPDeviceConnector.connect_real(): no signal is
608+
# generated for a struct/tuple nested inside another
609+
# struct/tuple/array. Unlike the runtime path, codegen has
610+
# no notion of "mandatory for this instance's interface
611+
# class" at generation time, so it always just skips --
612+
# the runtime path is still the one that raises for a
613+
# genuinely mandatory parameter when actually connecting.
614+
continue
586615

587-
param_name_camel = "".join(
588-
word.capitalize() for word in param_name_list
589-
)
616+
if kind is CompositeKind.DECOMPOSABLE:
617+
for member_key, member_dt in get_composite_members(raw_datatype):
618+
member_type_param: str | None
619+
if isinstance(member_dt, EnumType):
620+
# an EnumType member embedded in a struct/tuple
621+
# stays as its raw numeric code at runtime (see
622+
# SECoPBackend.init_member_from_introspection's
623+
# _is_enum_member handling), not a resolved enum
624+
# class -- so no enum class is generated here
625+
member_type_param = "int"
626+
else:
627+
member_type_param = get_type_param(member_dt)
628+
629+
mod_parameters.append(
630+
ParameterAttribute(
631+
name=f"{param_name}_{member_key}",
632+
type=SignalR.__name__,
633+
type_param=member_type_param,
634+
description=param_descr,
635+
path_annotation=str(ParameterMemberType()),
636+
format_annotation=format_str,
637+
)
638+
)
590639

591-
enum_class_name = f"{module_class}_{param_name_camel}_Enum"
640+
if not param_data["readonly"]:
641+
mod_parameters.append(
642+
ParameterAttribute(
643+
name=param_name,
644+
type=SignalW.__name__,
645+
type_param=get_type_param(raw_datatype),
646+
description=param_descr,
647+
path_annotation=str(ParameterType()),
648+
# a write-only SignalW can't carry a
649+
# StandardReadableFormat (nothing to read)
650+
format_annotation=None,
651+
)
652+
)
653+
continue
592654

593-
enum_cls = _build_enum_class(
594-
enum_class_name,
595-
datainfo.get("members", {}),
596-
f"{param_name} enum for `{module_class}`.",
597-
)
598-
if enum_cls:
599-
module_enum_classes.append(enum_cls)
655+
# ATOMIC: single signal, unchanged from previous behaviour
656+
signal_base = SignalR if param_data["readonly"] else SignalRW
657+
datainfo = param_data.get("datainfo", {})
600658

601-
# Use the specific enum class name instead of generic
602-
# StrictEnum
603-
type_param = enum_class_name
659+
# infer the ophyd type from secop datatype
660+
type_param = get_type_param(raw_datatype)
661+
type_param = _enum_type_param(
662+
type_param,
663+
f"{module_class}_{_camel(param_name)}_Enum",
664+
datainfo.get("members", {}),
665+
f"{param_name} enum for `{module_class}`.",
666+
)
604667

605668
# Default format for parameters is CONFIG_SIGNAL
606669

@@ -611,7 +674,7 @@ def from_json_describe(self, json_data: str | dict):
611674
type_param=type_param,
612675
description=param_descr,
613676
path_annotation=str(ParameterType()),
614-
format_annotation=format,
677+
format_annotation=format_str,
615678
)
616679
)
617680

0 commit comments

Comments
 (0)