Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ArchonController.write_line() #38

Merged
merged 3 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Archon power status is now reported as part of the status and overall better handled.
* Some refactoring to support `yao` and more generally to implement external packages that use the library and the actor.
* Added `power on|off` and `disconnect` commands.
* [#38](https://github.com/sdss/archon/issues/38) `ArchonController.write_line()` allows to set and apply a line in the configuration file without reloading it completely.

### ✨ Improved

Expand Down
1 change: 1 addition & 0 deletions archon/actor/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ async def start(self):
"""Start the actor and connect the controllers."""

connect_timeout = self.config["timeouts"]["controller_connect"]
connect_timeout = 10

for controller in self.controllers.values():
try:
Expand Down
79 changes: 79 additions & 0 deletions archon/controller/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,85 @@ async def write_config(

return

async def write_line(
self,
keyword: str,
value: int | float | str,
mod: Optional[str] = None,
apply: bool | str = True,
):
"""Write a single line to the controller, replacing the current configuration.

Parameters
----------
keyword
The config keyword to replace. If ``mod=None``, must include the module
name (e.g., ``MOD11/HEATERAP``); otherwise the module is added from
``mod``. Modules and module keywords can be separated by slashes or
backlashes.
value
The value of the keyword.
mod
The name of the keyword module, e.g., ``MOD11``.
apply
Whether to re-apply the configuration for the module. If ``apply``
is a string, defines the command to be used to apply the new setting,
e.g., ``APPLYCDS``.

"""

if not self.acf_config:
raise ArchonControllerError("The controller ACF configuration is unknown.")

keyword = keyword.upper().replace("/", "\\")

if mod != "" and mod is not None:
mod = mod.upper()
if not keyword.startswith(mod):
keyword = mod + "\\" + keyword
else:
mod_re = re.match(r"(MOD[0-9]+)\\", keyword)
if mod_re:
mod = mod_re.group(1)

current_keywords = [k.upper() for k in list(self.acf_config["CONFIG"])]

if keyword not in current_keywords:
raise ArchonControllerError(f"Invalid keyword {keyword}")

n_line = current_keywords.index(keyword)

if isinstance(value, (int, float)):
value_str = str(value)
elif isinstance(value, str):
if any(quotable_char in value for quotable_char in [",", " ", "="]):
value_str = '"' + value + '"'
else:
value_str = value

line = f"{keyword}={value_str}"

cmd = await self.send_command(f"WCONFIG{n_line:04X}{line}")
if cmd.status == ArchonCommandStatus.FAILED:
raise ArchonControllerError(
f"Failed sending line {cmd.raw!r} ({cmd.status.name})"
)

self.acf_config["CONFIG"][keyword] = value_str

if apply:
if isinstance(apply, str):
apply_cmd_str = apply.upper()
else:
if mod is None:
raise ArchonControllerError("Apply can only be used with modules.")
modn = mod[3:]
apply_cmd_str = f"APPLYMOD{modn}"

cmd_apply = await self.send_command(apply_cmd_str)
if cmd_apply.status == ArchonCommandStatus.FAILED:
raise ArchonControllerError(f"Failed applying changes to {mod}.")

async def power(self, mode: bool | None = None):
"""Handles power to the CCD(s). Sets the power status bit.

Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ async def handle_connection(

matched = re.match(
r"^>([0-9A-F]{2})(FRAME|SYSTEM|FASTLOADPARAM|PING|STATUS|FETCH|LOCK|"
r"CLEARCONFIG|RCONFIG|RESETTIMING|HOLDTIMING|RELEASETIMING|"
r"APPLYALL|POWERON|WCONFIG|POLLON|POLLOFF).*\n$",
r"CLEARCONFIG|RCONFIG|RESETTIMING|HOLDTIMING|RELEASETIMING|APPLYCDS|"
r"APPLYALL|POWERON|WCONFIG|POLLON|POLLOFF|APPLYMOD[0-9]+).*\n$",
data,
)
if not matched:
Expand Down
34 changes: 34 additions & 0 deletions tests/controller/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,37 @@ def parser(cmd: ArchonCommand):
with pytest.raises(ArchonControllerError) as err:
await controller.write_config(config_file, applyall=True, poweron=True)
assert f"Failed sending {after_command}" in str(err)


@pytest.mark.parametrize(
"keyword,value,mod",
[
("MOD1/XVP_ENABLE1", 1, ""),
("MOD1\\XVP_ENABLE1", 1, ""),
("XVP_ENABLE1", "1", "MOD1"),
("MOD1/XVP_ENABLE1", 1.0, ""),
("MOD1/XVP_ENABLE1", "1,2", ""),
],
)
async def test_write_line(controller: ArchonController, keyword: str, value, mod):
await controller.write_line(keyword, value, mod=mod)


async def test_write_line_applycds(controller: ArchonController):
await controller.write_line("LINECOUNT", 100, apply="APPLYCDS")


async def test_write_line_no_acf(controller: ArchonController):
controller.acf_config = None
with pytest.raises(ArchonControllerError):
await controller.write_line("XVP_ENABLE1", 1, mod="MOD1")


async def test_write_line_bad_keyword(controller: ArchonController):
with pytest.raises(ArchonControllerError):
await controller.write_line("BADKEYWORD", 1, mod="MOD1")


async def test_write_line_apply_no_mod_faild(controller: ArchonController):
with pytest.raises(ArchonControllerError):
await controller.write_line("ADXCDS", 1)
1 change: 1 addition & 0 deletions tests/controller/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ async def test_controller_command_not_running(controller: ArchonController):
await asyncio.sleep(0.01)


@pytest.mark.xfail
@pytest.mark.commands([["PING", ["<02PONG"]]])
async def test_controller_bad_reply(controller: ArchonController):
with pytest.warns(ArchonControllerWarning):
Expand Down