From f38996e2af5bc1c3a1df54f214e9a3a7efb9db34 Mon Sep 17 00:00:00 2001 From: Matthew Pritchard Date: Wed, 17 Aug 2022 16:31:28 +0100 Subject: [PATCH 1/9] #9: Add minimal PMAC device for simple IOC startup --- examples/configs/pmac.yaml | 12 ++++ tickit_devices/pmac/__init__.py | 3 + tickit_devices/pmac/pmac.py | 109 ++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 examples/configs/pmac.yaml create mode 100644 tickit_devices/pmac/__init__.py create mode 100644 tickit_devices/pmac/pmac.py diff --git a/examples/configs/pmac.yaml b/examples/configs/pmac.yaml new file mode 100644 index 00000000..f01fbf79 --- /dev/null +++ b/examples/configs/pmac.yaml @@ -0,0 +1,12 @@ +- tickit.devices.source.Source: + name: source + inputs: {} + value: 42.0 +- tickit_devices.pmac.PMAC: + name: pmac + inputs: + flux: source:value +- tickit.devices.sink.Sink: + name: sink + inputs: + flux: pmac:flux \ No newline at end of file diff --git a/tickit_devices/pmac/__init__.py b/tickit_devices/pmac/__init__.py new file mode 100644 index 00000000..c8fbd2b3 --- /dev/null +++ b/tickit_devices/pmac/__init__.py @@ -0,0 +1,3 @@ +from tickit_devices.pmac.pmac import PMAC + +__all__ = ["PMAC"] diff --git a/tickit_devices/pmac/pmac.py b/tickit_devices/pmac/pmac.py new file mode 100644 index 00000000..d0095764 --- /dev/null +++ b/tickit_devices/pmac/pmac.py @@ -0,0 +1,109 @@ +from dataclasses import dataclass + +from tickit.adapters.composed import ComposedAdapter +from tickit.adapters.interpreters.command import CommandInterpreter +from tickit.adapters.interpreters.command.regex_command import RegexCommand +from tickit.adapters.interpreters.wrappers import ( + BeheadingInterpreter, + SplittingInterpreter, +) +from tickit.adapters.servers.tcp import TcpServer +from tickit.core.components.component import Component, ComponentConfig +from tickit.core.components.device_simulation import DeviceSimulation +from tickit.core.device import Device, DeviceUpdate +from tickit.core.typedefs import SimTime +from tickit.utils.byte_format import ByteFormat +from tickit.utils.compat.typing_compat import TypedDict + + +class PMACDevice(Device): + + Inputs: TypedDict = TypedDict("Inputs", {"flux": float}) + Outputs: TypedDict = TypedDict("Outputs", {"flux": float}) + + def __init__(self) -> None: + pass + + def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]: + print("Updating\n") + return DeviceUpdate(self.Outputs(flux=2.0), None) + + +class PMACAdapter(ComposedAdapter): + + device: PMACDevice + + def __init__( + self, + host: str = "localhost", + port: int = 1025, + ) -> None: + """A PMAC which instantiates a TcpServer with configured host and port. + + Args: + device (Device): The device which this adapter is attached to + raise_interrupt (Callable): A callback to request that the device is + updated immediately. + host (Optional[str]): The host address of the TcpServer. Defaults to + "localhost". + port (Optional[int]): The bound port of the TcpServer. Defaults to 1025. + """ + super().__init__( + TcpServer(host, port, ByteFormat(b"%b\n")), + BeheadingInterpreter( + SplittingInterpreter(CommandInterpreter(), b" ", b""), + header_size=8, + ), + ) + + @RegexCommand(rb"\r?\n?") + async def parse_just_bytes(self): + return b"\r" + + @RegexCommand(rb"(?i:CID)\r?\n?") + async def get_cid(self): + return b"603382\r" + + @RegexCommand(rb"(?i:CPU)\r?\n?") + async def get_cpu(self): + return b"DSP56321\r" + + @RegexCommand(rb"#([1-8])[pP]\r?\n?") + async def get_axis_position(self, axis_no: int): + return str(1.0).encode() + b"1\r" + + @RegexCommand(rb"#([1-8])[fF]\r?\n?") + async def get_axis_following_error(self): + # target - current + return b"0\r" + + @RegexCommand(rb"#([1-8])\?\r?\n?") + async def get_axis_status(self): + return b"880000018401\r" + + @RegexCommand(rb"[mM]([0-9]{1,4})\r?\n?") + async def m_var(self): + return b"1\r" + + @RegexCommand(rb"[iI][0-9]{1,4}\r?\n?") + async def i_var(self): + return b"2\r" + + @RegexCommand(rb"\?\?\?\r?\n?") + async def get_status(self): + return b"000000000000\r" + + +@dataclass +class PMAC(ComponentConfig): + """PMAC accessible over TCP.""" + + host: str = "localhost" + port: int = 1025 + + def __call__(self) -> Component: # noqa: D102 + return DeviceSimulation( + name=self.name, + device=PMACDevice(), + adapters=[PMACAdapter(host=self.host, port=self.port)], + ) From e0087ff2ca296ea8f68b4f90db0df09c0e7b51c5 Mon Sep 17 00:00:00 2001 From: Matthew Pritchard Date: Mon, 22 Aug 2022 12:51:12 +0100 Subject: [PATCH 2/9] 11 implement get and set commands for p and m vars --- tickit_devices/pmac/pmac.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/tickit_devices/pmac/pmac.py b/tickit_devices/pmac/pmac.py index d0095764..e900f7fa 100644 --- a/tickit_devices/pmac/pmac.py +++ b/tickit_devices/pmac/pmac.py @@ -15,6 +15,11 @@ from tickit.utils.byte_format import ByteFormat from tickit.utils.compat.typing_compat import TypedDict +M_TRAJ_VERSION = 4049 +M_TRAJ_BUFSIZE = 4037 +M_TRAJ_A_ADR = 4041 +M_TRAJ_B_ADR = 4042 + class PMACDevice(Device): @@ -22,7 +27,16 @@ class PMACDevice(Device): Outputs: TypedDict = TypedDict("Outputs", {"flux": float}) def __init__(self) -> None: - pass + self.mvars = [0.0] * 16000 + self.mvars[M_TRAJ_VERSION] = 3.0 + self.mvars[M_TRAJ_BUFSIZE] = 1000 + self.mvars[M_TRAJ_A_ADR] = 0x40000 + self.mvars[M_TRAJ_B_ADR] = 0x30000 + self.mvars[70] = 11990 + self.mvars[71] = 554 + self.mvars[72] = 2621 + self.mvars[73] = 76 + self.pvars = [0.0] * 16000 def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]: print("Updating\n") @@ -81,9 +95,23 @@ async def get_axis_following_error(self): async def get_axis_status(self): return b"880000018401\r" - @RegexCommand(rb"[mM]([0-9]{1,4})\r?\n?") - async def m_var(self): - return b"1\r" + @RegexCommand(rb"[mM]([0-9]{1,5})\r?\n?") # 1-4 or 1-5? + async def get_m_var(self, mvar: int): + return f"{self.device.mvars[mvar]}\r".encode() + + @RegexCommand(rb"[mM]([0-9]{1,5})=([0-9]*.?[0-9]*)\r?\n?") + async def set_m_var(self, mvar: int, value: float): + self.device.mvars[mvar] = value + return b"\r" + + @RegexCommand(rb"[pP]([0-9]{1,5})\r?\n?") # 1-4 or 1-5? + async def get_p_var(self, pvar: int): + return f"{self.device.pvars[pvar]}\r".encode() + + @RegexCommand(rb"[pP]([0-9]{1,5})=([0-9]*.?[0-9]*)\r?\n?") + async def set_p_var(self, pvar: int, value: float): + self.device.pvars[pvar] = value + return b"\r" @RegexCommand(rb"[iI][0-9]{1,4}\r?\n?") async def i_var(self): From e96d8c496f2625c477eb886cddfb15abc254f83d Mon Sep 17 00:00:00 2001 From: Matthew Pritchard Date: Mon, 22 Aug 2022 13:09:06 +0100 Subject: [PATCH 3/9] #11: Add docstrings to p/mvar getting/setting command functions --- tickit_devices/pmac/pmac.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tickit_devices/pmac/pmac.py b/tickit_devices/pmac/pmac.py index e900f7fa..bb9fe9fc 100644 --- a/tickit_devices/pmac/pmac.py +++ b/tickit_devices/pmac/pmac.py @@ -97,19 +97,41 @@ async def get_axis_status(self): @RegexCommand(rb"[mM]([0-9]{1,5})\r?\n?") # 1-4 or 1-5? async def get_m_var(self, mvar: int): + """Regex bytestring command that returns the value of a specific mvar. + + Args: + mvar (int): the mvar to be returned. + """ return f"{self.device.mvars[mvar]}\r".encode() @RegexCommand(rb"[mM]([0-9]{1,5})=([0-9]*.?[0-9]*)\r?\n?") async def set_m_var(self, mvar: int, value: float): + """Regex bytestring command that sets the value of a specific mvar. + + Args: + mvar (int): the mvar to be set. + value (float): the value for the mvar to be set to. + """ self.device.mvars[mvar] = value return b"\r" @RegexCommand(rb"[pP]([0-9]{1,5})\r?\n?") # 1-4 or 1-5? async def get_p_var(self, pvar: int): + """Regex bytestring command that returns the value of a specific pvar. + + Args: + pvar (int): the p var to be returned. + """ return f"{self.device.pvars[pvar]}\r".encode() @RegexCommand(rb"[pP]([0-9]{1,5})=([0-9]*.?[0-9]*)\r?\n?") async def set_p_var(self, pvar: int, value: float): + """Regex bytestring command that sets the value of a specific pvar. + + Args: + pvar (int): the pvar to be set. + value (float): the value for the pvar to be set to. + """ self.device.pvars[pvar] = value return b"\r" From 3336a9fa9f2455d914e1586e81b835a2dc1dec3d Mon Sep 17 00:00:00 2001 From: Matthew Pritchard Date: Mon, 5 Sep 2022 16:28:20 +0100 Subject: [PATCH 4/9] #9: Update PmacAdapter to use JoiningInterpreter --- tickit_devices/pmac/pmac.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tickit_devices/pmac/pmac.py b/tickit_devices/pmac/pmac.py index d0095764..35cab1f2 100644 --- a/tickit_devices/pmac/pmac.py +++ b/tickit_devices/pmac/pmac.py @@ -5,6 +5,7 @@ from tickit.adapters.interpreters.command.regex_command import RegexCommand from tickit.adapters.interpreters.wrappers import ( BeheadingInterpreter, + JoiningInterpreter, SplittingInterpreter, ) from tickit.adapters.servers.tcp import TcpServer @@ -51,7 +52,9 @@ def __init__( super().__init__( TcpServer(host, port, ByteFormat(b"%b\n")), BeheadingInterpreter( - SplittingInterpreter(CommandInterpreter(), b" ", b""), + JoiningInterpreter( + SplittingInterpreter(CommandInterpreter(), b" "), b"" + ), header_size=8, ), ) From 06d79cb4b2b34e441e590da5ff9a6a20e1acb027 Mon Sep 17 00:00:00 2001 From: Matthew Pritchard Date: Mon, 5 Sep 2022 17:47:33 +0100 Subject: [PATCH 5/9] #12: Read and write ivars --- tickit_devices/pmac/pmac.py | 66 ++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/tickit_devices/pmac/pmac.py b/tickit_devices/pmac/pmac.py index 35cab1f2..759d7106 100644 --- a/tickit_devices/pmac/pmac.py +++ b/tickit_devices/pmac/pmac.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Dict from tickit.adapters.composed import ComposedAdapter from tickit.adapters.interpreters.command import CommandInterpreter @@ -17,13 +18,38 @@ from tickit.utils.compat.typing_compat import TypedDict +class PMACAxis: + def __init__(self): + self.ivars = dict() + self.ivars[13] = 10000 + self.ivars[14] = -10000 + self.ivars[31] = 50 + self.ivars[32] = 50 + self.ivars[33] = 50 + + class PMACDevice(Device): Inputs: TypedDict = TypedDict("Inputs", {"flux": float}) Outputs: TypedDict = TypedDict("Outputs", {"flux": float}) def __init__(self) -> None: - pass + self.axes = { + 1: PMACAxis(), + 2: PMACAxis(), + 3: PMACAxis(), + 4: PMACAxis(), + 5: PMACAxis(), + 6: PMACAxis(), + 7: PMACAxis(), + 8: PMACAxis(), + } + self.system_ivars: Dict = dict() + self.system_ivars[20] = "$78400" + self.system_ivars[21] = "$0" + self.system_ivars[22] = "$0" + self.system_ivars[23] = "$0" + self.other_ivars: Dict = dict() def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]: print("Updating\n") @@ -88,9 +114,41 @@ async def get_axis_status(self): async def m_var(self): return b"1\r" - @RegexCommand(rb"[iI][0-9]{1,4}\r?\n?") - async def i_var(self): - return b"2\r" + @RegexCommand(rb"[iI]([0-9]{1,2})\r?\n?") + async def read_system_ivar(self, ivar: int): + value = self.device.system_ivars.get(ivar, None) + if value is None: + self.device.system_ivars[ivar] = 0 + return f"{self.device.system_ivars[ivar]}\r".encode() + + @RegexCommand(rb"[iI]([0-9]{1,2})=\$?(\d+(?:.\d+)?)\r?\n?") + async def write_system_ivar(self, ivar: int, value: float): + self.device.system_ivars[ivar] = value + return b"\r" + + @RegexCommand(rb"[iI]([0-9])([0-9]{2})\r?\n?") + async def read_axis_var(self, axis: int, ivar: int): + value = self.device.axes[axis].ivars.get(ivar, None) + if value is None: + self.device.axes[axis].ivars[ivar] = 0 + return f"{self.device.axes[axis].ivars[ivar]}\r".encode() + + @RegexCommand(rb"[iI]([0-9])([0-9]{2})=-?(\d+(?:.\d+)?)\r?\n?") + async def write_axis_ivar(self, axis: int, ivar: int, value: float): + self.device.axes[axis].ivars[ivar] = value + return b"\r" + + @RegexCommand(rb"[iI]([0-9]{4})\r?\n?") + async def read_other_ivar(self, ivar: int): + value = self.device.other_ivars.get(ivar, None) + if value is None: + self.device.other_ivars[ivar] = 0 + return f"{self.device.other_ivars[ivar]}\r".encode() + + @RegexCommand(rb"[iI]([0-9]{4})=-?(\d+(?:\.\d+)?)\r?\n?") + async def write_other_var(self, ivar: int, value: float): + self.device.other_ivars[ivar] = value + return b"\r" @RegexCommand(rb"\?\?\?\r?\n?") async def get_status(self): From 2771595c6b2113d501133245a962844a55cb9da8 Mon Sep 17 00:00:00 2001 From: Matthew Pritchard Date: Mon, 5 Sep 2022 17:54:08 +0100 Subject: [PATCH 6/9] #11: Replace p/mvar lists with dicts --- tickit_devices/pmac/pmac.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tickit_devices/pmac/pmac.py b/tickit_devices/pmac/pmac.py index 3baacfc8..4ac77f74 100644 --- a/tickit_devices/pmac/pmac.py +++ b/tickit_devices/pmac/pmac.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Dict from tickit.adapters.composed import ComposedAdapter from tickit.adapters.interpreters.command import CommandInterpreter @@ -28,7 +29,7 @@ class PMACDevice(Device): Outputs: TypedDict = TypedDict("Outputs", {"flux": float}) def __init__(self) -> None: - self.mvars = [0.0] * 16000 + self.mvars = dict() self.mvars[M_TRAJ_VERSION] = 3.0 self.mvars[M_TRAJ_BUFSIZE] = 1000 self.mvars[M_TRAJ_A_ADR] = 0x40000 @@ -37,7 +38,7 @@ def __init__(self) -> None: self.mvars[71] = 554 self.mvars[72] = 2621 self.mvars[73] = 76 - self.pvars = [0.0] * 16000 + self.pvars: Dict = dict() def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]: print("Updating\n") @@ -105,6 +106,9 @@ async def get_m_var(self, mvar: int): Args: mvar (int): the mvar to be returned. """ + value = self.device.mvars.get(mvar, None) + if value is None: + self.device.mvars[mvar] = 0 return f"{self.device.mvars[mvar]}\r".encode() @RegexCommand(rb"[mM]([0-9]{1,5})=([0-9]*.?[0-9]*)\r?\n?") @@ -125,6 +129,9 @@ async def get_p_var(self, pvar: int): Args: pvar (int): the p var to be returned. """ + value = self.device.pvars.get(pvar, None) + if value is None: + self.device.pvars[pvar] = 0 return f"{self.device.pvars[pvar]}\r".encode() @RegexCommand(rb"[pP]([0-9]{1,5})=([0-9]*.?[0-9]*)\r?\n?") From f2085fc412905fff51b476435fac7e1b5b5ee389 Mon Sep 17 00:00:00 2001 From: Matthew Pritchard Date: Mon, 5 Sep 2022 18:06:08 +0100 Subject: [PATCH 7/9] #12: Add doctrings for ivar set/getters --- tickit_devices/pmac/pmac.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tickit_devices/pmac/pmac.py b/tickit_devices/pmac/pmac.py index 759d7106..2fd3b5ee 100644 --- a/tickit_devices/pmac/pmac.py +++ b/tickit_devices/pmac/pmac.py @@ -116,6 +116,11 @@ async def m_var(self): @RegexCommand(rb"[iI]([0-9]{1,2})\r?\n?") async def read_system_ivar(self, ivar: int): + """Regex bytestring command that returns the value of a specific system ivar. + + Args: + ivar (int): the ivar to read. + """ value = self.device.system_ivars.get(ivar, None) if value is None: self.device.system_ivars[ivar] = 0 @@ -123,11 +128,23 @@ async def read_system_ivar(self, ivar: int): @RegexCommand(rb"[iI]([0-9]{1,2})=\$?(\d+(?:.\d+)?)\r?\n?") async def write_system_ivar(self, ivar: int, value: float): + """Regex bytestring command that sets the value of a specific system ivar. + + Args: + ivar (int): the ivar to set. + value (float): the value to set the ivar to. + """ self.device.system_ivars[ivar] = value return b"\r" @RegexCommand(rb"[iI]([0-9])([0-9]{2})\r?\n?") async def read_axis_var(self, axis: int, ivar: int): + """Regex bytestring command that returns the value of a specific axis ivar. + + Args: + axis (int): the axis to read ivars from. + ivar (int): the ivar to read. + """ value = self.device.axes[axis].ivars.get(ivar, None) if value is None: self.device.axes[axis].ivars[ivar] = 0 @@ -135,11 +152,23 @@ async def read_axis_var(self, axis: int, ivar: int): @RegexCommand(rb"[iI]([0-9])([0-9]{2})=-?(\d+(?:.\d+)?)\r?\n?") async def write_axis_ivar(self, axis: int, ivar: int, value: float): + """Regex bytestring command that sets the value of a specific axis ivar. + + Args: + axis (int): the axis to set ivars on. + ivar (int): the ivar to set. + value (float): the value to set the ivar to. + """ self.device.axes[axis].ivars[ivar] = value return b"\r" @RegexCommand(rb"[iI]([0-9]{4})\r?\n?") async def read_other_ivar(self, ivar: int): + """Regex bytestring command that returns the value of a specific ivar. + + Args: + ivar (int): the ivar to read. + """ value = self.device.other_ivars.get(ivar, None) if value is None: self.device.other_ivars[ivar] = 0 @@ -147,6 +176,12 @@ async def read_other_ivar(self, ivar: int): @RegexCommand(rb"[iI]([0-9]{4})=-?(\d+(?:\.\d+)?)\r?\n?") async def write_other_var(self, ivar: int, value: float): + """Regex bytestring command that sets the value of a specific ivar. + + Args: + ivar (int): the ivar to set. + value (float): the value to set the ivar to. + """ self.device.other_ivars[ivar] = value return b"\r" From 165142b6222cba37af461fa81d8bef6587a9c850 Mon Sep 17 00:00:00 2001 From: Matthew Pritchard Date: Thu, 8 Sep 2022 16:32:19 +0100 Subject: [PATCH 8/9] #18: Add minimal PMAC device for dls-pmac-control startup --- tickit_devices/pmac/pmac.py | 169 ++++++++++++++++++++++++++---------- 1 file changed, 121 insertions(+), 48 deletions(-) diff --git a/tickit_devices/pmac/pmac.py b/tickit_devices/pmac/pmac.py index ef8e9bfb..172c73eb 100644 --- a/tickit_devices/pmac/pmac.py +++ b/tickit_devices/pmac/pmac.py @@ -2,12 +2,11 @@ from typing import Dict from tickit.adapters.composed import ComposedAdapter -from tickit.adapters.interpreters.command import CommandInterpreter +from tickit.adapters.interpreters.command import MultiCommandInterpreter from tickit.adapters.interpreters.command.regex_command import RegexCommand from tickit.adapters.interpreters.wrappers import ( BeheadingInterpreter, JoiningInterpreter, - SplittingInterpreter, ) from tickit.adapters.servers.tcp import TcpServer from tickit.core.components.component import Component, ComponentConfig @@ -17,6 +16,7 @@ from tickit.utils.byte_format import ByteFormat from tickit.utils.compat.typing_compat import TypedDict +# Some constants copied from the old PMAC sim. M_TRAJ_VERSION = 4049 M_TRAJ_BUFSIZE = 4037 M_TRAJ_A_ADR = 4041 @@ -49,22 +49,15 @@ def __init__(self) -> None: self.mvars[72] = 2621 self.mvars[73] = 76 self.pvars: Dict = dict() - self.axes = { - 1: PMACAxis(), - 2: PMACAxis(), - 3: PMACAxis(), - 4: PMACAxis(), - 5: PMACAxis(), - 6: PMACAxis(), - 7: PMACAxis(), - 8: PMACAxis(), - } + self.axes = {i: PMACAxis() for i in range(1, 17)} self.system_ivars: Dict = dict() self.system_ivars[20] = "$78400" self.system_ivars[21] = "$0" self.system_ivars[22] = "$0" self.system_ivars[23] = "$0" self.other_ivars: Dict = dict() + self.current_axis = 1 + self.current_cs = 1 def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]: print("Updating\n") @@ -91,41 +84,39 @@ def __init__( port (Optional[int]): The bound port of the TcpServer. Defaults to 1025. """ super().__init__( - TcpServer(host, port, ByteFormat(b"%b\n")), + TcpServer(host, port, ByteFormat(b"%b\x06")), BeheadingInterpreter( - JoiningInterpreter( - SplittingInterpreter(CommandInterpreter(), b" "), b"" - ), + JoiningInterpreter(MultiCommandInterpreter(), b""), header_size=8, ), ) - @RegexCommand(rb"\r?\n?") - async def parse_just_bytes(self): + @RegexCommand(rb"\r?\n?$") + async def end_of_message(self): return b"\r" - @RegexCommand(rb"(?i:CID)\r?\n?") + @RegexCommand(rb"(?i:CID)") async def get_cid(self): + """Regex bytestring command returning the value for the PMACA's card ID number. + + Note: + 602404: "Turbo PMAC2 Clipper", + 602413: "Turbo PMAC2-VME", + 603382: "Geo Brick (3U Turbo PMAC2)", + """ return b"603382\r" - @RegexCommand(rb"(?i:CPU)\r?\n?") + @RegexCommand(rb"(?i:CPU)") async def get_cpu(self): + """Command reporting the PMAC's CPU type.""" return b"DSP56321\r" - @RegexCommand(rb"#([1-8])[pP]\r?\n?") - async def get_axis_position(self, axis_no: int): - return str(1.0).encode() + b"1\r" - - @RegexCommand(rb"#([1-8])[fF]\r?\n?") - async def get_axis_following_error(self): - # target - current - return b"0\r" + @RegexCommand(rb"(?i:VER)") + async def get_ver(self): + """Command reporting the PMAC's firmware version.""" + return b"1.947 \r" - @RegexCommand(rb"#([1-8])\?\r?\n?") - async def get_axis_status(self): - return b"880000018401\r" - - @RegexCommand(rb"[mM]([0-9]{1,5})\r?\n?") # 1-4 or 1-5? + @RegexCommand(rb"[mM]([0-9]{1,5})") async def get_m_var(self, mvar: int): """Regex bytestring command that returns the value of a specific mvar. @@ -137,7 +128,7 @@ async def get_m_var(self, mvar: int): self.device.mvars[mvar] = 0 return f"{self.device.mvars[mvar]}\r".encode() - @RegexCommand(rb"[mM]([0-9]{1,5})=([0-9]*.?[0-9]*)\r?\n?") + @RegexCommand(rb"[mM]([0-9]{1,5})=([0-9]*.?[0-9]*)") async def set_m_var(self, mvar: int, value: float): """Regex bytestring command that sets the value of a specific mvar. @@ -146,9 +137,9 @@ async def set_m_var(self, mvar: int, value: float): value (float): the value for the mvar to be set to. """ self.device.mvars[mvar] = value - return b"\r" + return b"" - @RegexCommand(rb"[pP]([0-9]{1,5})\r?\n?") # 1-4 or 1-5? + @RegexCommand(rb"[pP]([0-9]{1,5})") async def get_p_var(self, pvar: int): """Regex bytestring command that returns the value of a specific pvar. @@ -160,7 +151,7 @@ async def get_p_var(self, pvar: int): self.device.pvars[pvar] = 0 return f"{self.device.pvars[pvar]}\r".encode() - @RegexCommand(rb"[pP]([0-9]{1,5})=([0-9]*.?[0-9]*)\r?\n?") + @RegexCommand(rb"[pP]([0-9]{1,5})=([0-9]*.?[0-9]*)") async def set_p_var(self, pvar: int, value: float): """Regex bytestring command that sets the value of a specific pvar. @@ -169,9 +160,9 @@ async def set_p_var(self, pvar: int, value: float): value (float): the value for the pvar to be set to. """ self.device.pvars[pvar] = value - return b"\r" + return b"" - @RegexCommand(rb"[iI]([0-9]{1,2})\r?\n?") + @RegexCommand(rb"[iI]([0-9]{1,2})") async def read_system_ivar(self, ivar: int): """Regex bytestring command that returns the value of a specific system ivar. @@ -183,7 +174,7 @@ async def read_system_ivar(self, ivar: int): self.device.system_ivars[ivar] = 0 return f"{self.device.system_ivars[ivar]}\r".encode() - @RegexCommand(rb"[iI]([0-9]{1,2})=\$?(\d+(?:.\d+)?)\r?\n?") + @RegexCommand(rb"[iI]([0-9]{1,2})=\$?(\d+(?:.\d+)?)") async def write_system_ivar(self, ivar: int, value: float): """Regex bytestring command that sets the value of a specific system ivar. @@ -192,9 +183,9 @@ async def write_system_ivar(self, ivar: int, value: float): value (float): the value to set the ivar to. """ self.device.system_ivars[ivar] = value - return b"\r" + return b"" - @RegexCommand(rb"[iI]([0-9])([0-9]{2})\r?\n?") + @RegexCommand(rb"[iI](1*[0-9])([0-9]{2})") async def read_axis_var(self, axis: int, ivar: int): """Regex bytestring command that returns the value of a specific axis ivar. @@ -207,7 +198,7 @@ async def read_axis_var(self, axis: int, ivar: int): self.device.axes[axis].ivars[ivar] = 0 return f"{self.device.axes[axis].ivars[ivar]}\r".encode() - @RegexCommand(rb"[iI]([0-9])([0-9]{2})=-?(\d+(?:.\d+)?)\r?\n?") + @RegexCommand(rb"[iI](1*[0-9])([0-9]{2})=-?(\d+(?:.\d+)?)") async def write_axis_ivar(self, axis: int, ivar: int, value: float): """Regex bytestring command that sets the value of a specific axis ivar. @@ -217,9 +208,9 @@ async def write_axis_ivar(self, axis: int, ivar: int, value: float): value (float): the value to set the ivar to. """ self.device.axes[axis].ivars[ivar] = value - return b"\r" + return b"" - @RegexCommand(rb"[iI]([0-9]{4})\r?\n?") + @RegexCommand(rb"[iI]([0-9]{4})") async def read_other_ivar(self, ivar: int): """Regex bytestring command that returns the value of a specific ivar. @@ -231,7 +222,7 @@ async def read_other_ivar(self, ivar: int): self.device.other_ivars[ivar] = 0 return f"{self.device.other_ivars[ivar]}\r".encode() - @RegexCommand(rb"[iI]([0-9]{4})=-?(\d+(?:\.\d+)?)\r?\n?") + @RegexCommand(rb"[iI]([0-9]{4})=-?(\d+(?:\.\d+)?)") async def write_other_var(self, ivar: int, value: float): """Regex bytestring command that sets the value of a specific ivar. @@ -240,12 +231,94 @@ async def write_other_var(self, ivar: int, value: float): value (float): the value to set the ivar to. """ self.device.other_ivars[ivar] = value - return b"\r" + return b"" + + @RegexCommand(rb"#") + async def get_current_axis(self): + """Command reporting the currently addressed axis.""" + return f"{self.device.current_axis}\r".encode() + + @RegexCommand(rb"#(1*[0-9])") + async def set_current_axis(self, axis: int): + """Command setting the currently addressed axis. + + Args: + axis (int): the new current axis. + """ + self.device.current_axis = axis + return b"" + + @RegexCommand(rb"\?") + async def get_axis_status(self): + """Command getting the status of the currently addressed axis. + + Currently returns a dummy value of the right format. + """ + return b"000000000000\r" + + @RegexCommand(rb"&") + async def get_current_cs(self): + """Command reporting the currently addressed coordinate system.""" + return f"{self.device.current_cs}".encode() - @RegexCommand(rb"\?\?\?\r?\n?") + @RegexCommand(rb"&([1-8])") + async def set_current_cs(self, cs: int): + """Command setting the currently addressed coordinate system. + + Args: + axis (int): the new active coordinate system. + """ + self.device.current_cs = cs + return b"" + + @RegexCommand(rb"\?\?") + async def get_cs_status(self): + """Command reporting the status of the currently addressed coordinate system. + + Currently returns a dummy value of the right format. + """ + return b"A80020000000000000\r" + + @RegexCommand(rb"\?\?\?") async def get_status(self): + """Command reporting the status of the PMAC. + + Currently returns a dummy value of the right format. + """ return b"000000000000\r" + @RegexCommand(rb"%") + async def get_cs_feedrate_override(self): + """Command reporting the feedrate override of the current coordinate system. + + Currently returns a dummy value of the right format. + """ + return b"100\r" + + @RegexCommand(rb"P") + async def get_axis_position(self): + """Command reporting the motor position of the currently addressed axis. + + Currently returns a dummy value of the right format. + """ + return b"1.0\r" + + @RegexCommand(rb"V") + async def get_axis_velocity(self): + """Command reporting the motor velocity of the currently addressed axis. + + Currently returns a dummy value of the right format. + """ + return b"0.0\r" + + @RegexCommand(rb"F") + async def get_axis_follow_error(self): + """Command reporting the motor following error of the current axis. + + Currently returns a dummy value of the right format. + """ + return b"0.0\r" + @dataclass class PMAC(ComponentConfig): From e1b3d9d42bdd3041415d873bcdeff2658a97f5be Mon Sep 17 00:00:00 2001 From: Matthew Pritchard Date: Mon, 12 Sep 2022 13:59:48 +0100 Subject: [PATCH 9/9] #13: Implement PMAC motor jogging --- tickit_devices/pmac/pmac.py | 127 ++++++++++++++++++++++++++++++++---- 1 file changed, 116 insertions(+), 11 deletions(-) diff --git a/tickit_devices/pmac/pmac.py b/tickit_devices/pmac/pmac.py index 172c73eb..075b0f00 100644 --- a/tickit_devices/pmac/pmac.py +++ b/tickit_devices/pmac/pmac.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Dict +from typing import Dict, Optional from tickit.adapters.composed import ComposedAdapter from tickit.adapters.interpreters.command import MultiCommandInterpreter @@ -28,9 +28,47 @@ def __init__(self): self.ivars = dict() self.ivars[13] = 10000 self.ivars[14] = -10000 + self.ivars[22] = 32.0 # velocity in counts per millisecond self.ivars[31] = 50 self.ivars[32] = 50 self.ivars[33] = 50 + self.target_position = 0 + self.current_position = 0 + + @property + def velocity(self): + return self.ivars[22] + + @property + def in_position(self): + return self.target_position == self.current_position + + @property + def status(self): + status_digit = "1" if self.in_position else "0" + return "88000001840" + status_digit + + def move(self, period_ms: float): + """A helper method used to compute the new position of the axis motor. + + A helper method used to compute the new position of the axis motor given a + period over which the change occurs. Movement is performed at the rate defined + by ivar22 and comes to a "hard" stop when the desired position is reached. + + Args: + period_ms (float): The period over which the change occurs in milliseconds. + """ + if self.in_position: + return + current_pos = self.current_position + target_pos = self.target_position + print(current_pos, target_pos) + velocity = self.velocity + if current_pos < target_pos: + new_position = min(current_pos + velocity * period_ms, target_pos) + elif current_pos > target_pos: + new_position = max(current_pos - velocity * period_ms, target_pos) + self.current_position = new_position class PMACDevice(Device): @@ -56,12 +94,24 @@ def __init__(self) -> None: self.system_ivars[22] = "$0" self.system_ivars[23] = "$0" self.other_ivars: Dict = dict() - self.current_axis = 1 + self.current_axis_index = 1 self.current_cs = 1 + self.last_update_time: Optional[SimTime] = None + + @property + def current_axis(self): + return self.axes[self.current_axis_index] def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]: - print("Updating\n") - return DeviceUpdate(self.Outputs(flux=2.0), None) + if self.last_update_time is not None: + # Calculate time interval in milliseconds + time_interval = SimTime(time - self.last_update_time) / 1e6 + for axis in self.axes.values(): + axis.move(time_interval) + in_position = all(axis.in_position for axis in self.axes.values()) + self.last_update_time = None if in_position else time + call_at = None if in_position else SimTime(time + int(1e8)) + return DeviceUpdate(self.Outputs(flux=2.0), call_at) class PMACAdapter(ComposedAdapter): @@ -236,7 +286,7 @@ async def write_other_var(self, ivar: int, value: float): @RegexCommand(rb"#") async def get_current_axis(self): """Command reporting the currently addressed axis.""" - return f"{self.device.current_axis}\r".encode() + return f"{self.device.current_axis_index}\r".encode() @RegexCommand(rb"#(1*[0-9])") async def set_current_axis(self, axis: int): @@ -245,7 +295,7 @@ async def set_current_axis(self, axis: int): Args: axis (int): the new current axis. """ - self.device.current_axis = axis + self.device.current_axis_index = axis return b"" @RegexCommand(rb"\?") @@ -254,7 +304,8 @@ async def get_axis_status(self): Currently returns a dummy value of the right format. """ - return b"000000000000\r" + status = self.device.current_axis.status + return f"{status}\r".encode() @RegexCommand(rb"&") async def get_current_cs(self): @@ -266,7 +317,7 @@ async def set_current_cs(self, cs: int): """Command setting the currently addressed coordinate system. Args: - axis (int): the new active coordinate system. + cs (int): the new active coordinate system. """ self.device.current_cs = cs return b"" @@ -301,7 +352,8 @@ async def get_axis_position(self): Currently returns a dummy value of the right format. """ - return b"1.0\r" + position = self.device.current_axis.current_position + return f"{position}\r".encode() @RegexCommand(rb"V") async def get_axis_velocity(self): @@ -309,7 +361,8 @@ async def get_axis_velocity(self): Currently returns a dummy value of the right format. """ - return b"0.0\r" + velocity = self.device.current_axis.velocity + return f"{velocity}\r".encode() @RegexCommand(rb"F") async def get_axis_follow_error(self): @@ -317,7 +370,59 @@ async def get_axis_follow_error(self): Currently returns a dummy value of the right format. """ - return b"0.0\r" + follow_error = ( + self.device.current_axis.target_position + - self.device.current_axis.current_position + ) + return f"{follow_error}\r".encode() + + @RegexCommand(rb"HM", interrupt=True) + async def home(self): + """Command causing the addressed motor to perform a homing search routine.""" + self.device.current_axis.target_position = 0 + return b"" + + @RegexCommand(rb"J\^(-?\d+(?:\.\d+)?)", interrupt=True) + async def jog_relative(self, move: float): + """Command causing the current motor to jog relative to its actual position. + + Args: + move (float): The distance to move relative to the current position. + """ + current_pos = self.device.current_axis.target_position + target_pos = current_pos + move + self.device.current_axis.target_position = target_pos + return b"" + + @RegexCommand(rb"J=(-?\d+(?:\.\d+)?)", interrupt=True) + async def jog_specific(self, target: float): + """Command causing the current motor to jog to a specific position. + + Args: + target (float): The distance to move relative to the current position. + """ + self.device.current_axis.target_position = target + return b"" + + @RegexCommand(rb"J/", interrupt=True) + async def jog_stop(self): + """Command causing the current motor to stop jogging.""" + self.device.current_axis.target_position = ( + self.device.current_axis.current_position + ) + return b"" + + @RegexCommand(rb"J\+", interrupt=True) + async def jog_pos(self): + """Command to jog indefinitely in the positive direction.""" + self.device.current_axis.target_position = 1e9 + return b"" + + @RegexCommand(rb"J-", interrupt=True) + async def jog_neg(self): + """Command to jog indefinitely in the negative direction.""" + self.device.current_axis.target_position = -1e9 + return b"" @dataclass