arcsecond 3.20.1__tar.gz → 3.20.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arcsecond-3.20.1 → arcsecond-3.20.2}/PKG-INFO +1 -1
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/alpaca/commands.py +55 -26
- arcsecond-3.20.2/arcsecond/alpaca/dome_probe.py +89 -0
- arcsecond-3.20.1/arcsecond/alpaca/dome_probe.py → arcsecond-3.20.2/arcsecond/alpaca/probe.py +114 -120
- arcsecond-3.20.2/arcsecond/alpaca/telescope_probe.py +180 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/pyproject.toml +1 -1
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_alpaca_probe.py +343 -9
- {arcsecond-3.20.1 → arcsecond-3.20.2}/uv.lock +1 -1
- {arcsecond-3.20.1 → arcsecond-3.20.2}/.docker/Dockerfile_postgres +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/.docker/Dockerfile_redis +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/.flake8 +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/.github/dependabot.yml +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/.github/workflows/pythonpublish.yml +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/.github/workflows/tests.yml +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/.gitignore +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/LICENSE +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/Makefile +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/README.md +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/__version__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/alpaca/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/api/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/api/config.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/api/constants.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/api/endpoint.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/api/main.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/api/resources.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cli.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/auth.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/resources.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/constants.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/context.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/datafiles/context.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/datafiles/errors.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/datafiles/uploader.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/datafiles/utils.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/errors.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/logger.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/uploader.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/utils.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploader/walker.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/cloud/uploads.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/errors.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/backups.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/checks.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/constants.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/database.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/docker/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/docker/constants.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/docker/containers.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/docker/docker-compose.yml +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/docker/images.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/docker/utils.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/keygen/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/keygen/client.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/keygen/utils.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/local.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/main.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/postgres/init-db.sh +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/setup.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/utils.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/hosting/validation.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/commands.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/detection.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/proxy.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/registry.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/runtime.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/sources/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/sources/base.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/sources/filewatch.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/sources/mdns.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/sources/network.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/sources/opencv.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/imagesources/store.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/options.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/arcsecond/targets.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/examples/example_upload_files.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/requirements.txt +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/setup.cfg +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/setup.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/sonar-project.properties +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/api/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/api/test_api.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/api/test_api_endpoint.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/api/test_config.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/api/test_targets.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/cloud/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/cloud/uploader/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/cloud/uploader/datafiles/test_uploader_errors.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/cloud/uploader/datafiles/test_uploader_full_process.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/cloud/uploader/datafiles/test_uploader_init.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/cloud/uploader/datafiles/test_uploader_prepare.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/cloud/uploader/datafiles/test_uploader_upload.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/conftest.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/fixtures/file1.fits +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_cli.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_hosting_backups_restore.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_hosting_database.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_hosting_local.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_hosting_utils_secret_key.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_imagesources_commands.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_imagesources_detection.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_imagesources_mdns.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_imagesources_network.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_imagesources_proxy.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_imagesources_store.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_imagesources_webcam.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/test_targets_planning.py +0 -0
- {arcsecond-3.20.1 → arcsecond-3.20.2}/tests/utils.py +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Click command group: ``arcsecond alpaca``.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
``SupportedActions``, ``CommandString`` /
|
|
7
|
-
passthrough behaviour, and local host hints
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
``probe camera`` / ``probe focuser`` and
|
|
11
|
-
under the same group.
|
|
4
|
+
``arcsecond alpaca probe dome`` and ``arcsecond alpaca probe telescope`` are
|
|
5
|
+
read-only diagnostics that capture what surface a given Alpaca device server
|
|
6
|
+
exposes (device metadata, ``SupportedActions``, ``CommandString`` /
|
|
7
|
+
``CommandBool`` / ``CommandBlind`` passthrough behaviour, and local host hints
|
|
8
|
+
when the target is this machine), one JSON report per run.
|
|
9
|
+
|
|
10
|
+
Structure leaves room for ``probe camera`` / ``probe focuser`` and
|
|
11
|
+
``arcsecond alpaca discover`` later under the same group.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
@@ -17,11 +17,14 @@ import json
|
|
|
17
17
|
import os
|
|
18
18
|
from datetime import datetime, timezone
|
|
19
19
|
from pathlib import Path
|
|
20
|
+
from typing import Callable
|
|
20
21
|
|
|
21
22
|
import click
|
|
22
23
|
|
|
23
24
|
from ..errors import ArcsecondError
|
|
24
|
-
from .dome_probe import
|
|
25
|
+
from .dome_probe import probe_dome
|
|
26
|
+
from .probe import HOST_HINTS_REASON_LOCAL, ProbeProgress, ProbeResult
|
|
27
|
+
from .telescope_probe import probe_telescope
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
@click.group(name="alpaca", help="Diagnostics for local ASCOM Alpaca devices.")
|
|
@@ -122,20 +125,10 @@ def _format_progress(label: str, ok: bool, detail: str | None) -> str:
|
|
|
122
125
|
return f" {tag} {label}{suffix}"
|
|
123
126
|
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"Captures device metadata, SupportedActions, and the behaviour of the "
|
|
130
|
-
"legacy CommandString / CommandBool / CommandBlind passthroughs. "
|
|
131
|
-
"Useful for figuring out whether a proprietary driver (e.g. TCSGalil) "
|
|
132
|
-
"exposes any vendor-specific extension surface beyond the standard "
|
|
133
|
-
"ASCOM IDome interface. Local host hints (open ports, COM ProgIDs) "
|
|
134
|
-
"are added when HOST is this machine."
|
|
135
|
-
),
|
|
136
|
-
)
|
|
137
|
-
@_add_options(_COMMON_OPTIONS)
|
|
138
|
-
def probe_dome_cmd(
|
|
128
|
+
def _run_probe(
|
|
129
|
+
kind: str,
|
|
130
|
+
runner: Callable[..., ProbeResult],
|
|
131
|
+
*,
|
|
139
132
|
host: str,
|
|
140
133
|
port: int,
|
|
141
134
|
device_number: int,
|
|
@@ -144,10 +137,11 @@ def probe_dome_cmd(
|
|
|
144
137
|
collect_host_info: bool | None,
|
|
145
138
|
output_path: str | None,
|
|
146
139
|
) -> None:
|
|
147
|
-
|
|
140
|
+
"""Drive one probe from the terminal: banner, progress lines, report, summary."""
|
|
141
|
+
output_path = output_path or _default_output_path(kind)
|
|
148
142
|
|
|
149
143
|
click.echo(
|
|
150
|
-
click.style("Alpaca
|
|
144
|
+
click.style(f"Alpaca {kind} probe", bold=True)
|
|
151
145
|
+ f" → {protocol}://{host}:{port} (device {device_number})"
|
|
152
146
|
)
|
|
153
147
|
if allow_active:
|
|
@@ -163,7 +157,7 @@ def probe_dome_cmd(
|
|
|
163
157
|
)
|
|
164
158
|
|
|
165
159
|
try:
|
|
166
|
-
result =
|
|
160
|
+
result = runner(
|
|
167
161
|
host=host,
|
|
168
162
|
port=port,
|
|
169
163
|
device_number=device_number,
|
|
@@ -192,3 +186,38 @@ def probe_dome_cmd(
|
|
|
192
186
|
click.echo(f" SupportedActions: {supported_n} entries")
|
|
193
187
|
click.echo(f" Host hints: {_describe_host_hints(result.report)}")
|
|
194
188
|
click.echo(f" Report written to {click.style(os.fspath(output_path), fg='cyan')}")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@probe_group.command(
|
|
192
|
+
name="dome",
|
|
193
|
+
help=(
|
|
194
|
+
"Probe a local Alpaca dome device read-only and write a JSON report.\n\n"
|
|
195
|
+
"Captures device metadata, SupportedActions, and the behaviour of the "
|
|
196
|
+
"legacy CommandString / CommandBool / CommandBlind passthroughs. "
|
|
197
|
+
"Useful for figuring out whether a proprietary driver (e.g. TCSGalil) "
|
|
198
|
+
"exposes any vendor-specific extension surface beyond the standard "
|
|
199
|
+
"ASCOM IDome interface. Local host hints (open ports, COM ProgIDs) "
|
|
200
|
+
"are added when HOST is this machine."
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
@_add_options(_COMMON_OPTIONS)
|
|
204
|
+
def probe_dome_cmd(**options) -> None:
|
|
205
|
+
_run_probe("dome", probe_dome, **options)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@probe_group.command(
|
|
209
|
+
name="telescope",
|
|
210
|
+
help=(
|
|
211
|
+
"Probe a local Alpaca telescope (mount) device read-only and write a "
|
|
212
|
+
"JSON report.\n\n"
|
|
213
|
+
"Captures device metadata, optics, site, pointing and tracking state, "
|
|
214
|
+
"capabilities, CanMoveAxis and AxisRates per movable axis, "
|
|
215
|
+
"SupportedActions, and the behaviour of the legacy CommandString / "
|
|
216
|
+
"CommandBool / CommandBlind passthroughs. Never moves the mount. "
|
|
217
|
+
"Local host hints (open ports, COM ProgIDs) are added when HOST is "
|
|
218
|
+
"this machine."
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
@_add_options(_COMMON_OPTIONS)
|
|
222
|
+
def probe_telescope_cmd(**options) -> None:
|
|
223
|
+
_run_probe("telescope", probe_telescope, **options)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read-only diagnostic for an ASCOM Alpaca *dome* device.
|
|
3
|
+
|
|
4
|
+
The standard ASCOM Alpaca ``IDome`` interface only exposes a single
|
|
5
|
+
``OpenShutter`` / ``CloseShutter`` pair. Multi-shutter domes (e.g. DFM domes
|
|
6
|
+
driven by TCSGalil) coordinate their shutters internally; if the driver does
|
|
7
|
+
not advertise per-shutter custom actions via ``SupportedActions``, there is
|
|
8
|
+
no canonical client-side way to drive the upper and lower shutters
|
|
9
|
+
independently. This probe writes down exactly what surface a dome server
|
|
10
|
+
exposes, so that decision can be made on evidence.
|
|
11
|
+
|
|
12
|
+
The engine, the report layout and the safety rules live in :mod:`.probe`;
|
|
13
|
+
this module only says which dome properties to read.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Callable, Iterable
|
|
19
|
+
|
|
20
|
+
from .probe import ProbeProgress, ProbeResult, probe_device
|
|
21
|
+
from .probe import iter_probe_labels as _iter_probe_labels
|
|
22
|
+
|
|
23
|
+
# Read-only device descriptors and dome state, in the order we want them
|
|
24
|
+
# presented in the report. Names are PascalCase to match the alpyca surface.
|
|
25
|
+
_READ_ONLY_PROPERTIES: tuple[str, ...] = (
|
|
26
|
+
"Name",
|
|
27
|
+
"Description",
|
|
28
|
+
"DriverInfo",
|
|
29
|
+
"DriverVersion",
|
|
30
|
+
"InterfaceVersion",
|
|
31
|
+
"ShutterStatus",
|
|
32
|
+
"Altitude",
|
|
33
|
+
"Azimuth",
|
|
34
|
+
"AtHome",
|
|
35
|
+
"AtPark",
|
|
36
|
+
"Slewing",
|
|
37
|
+
"Slaved",
|
|
38
|
+
"CanFindHome",
|
|
39
|
+
"CanPark",
|
|
40
|
+
"CanSetAltitude",
|
|
41
|
+
"CanSetPark",
|
|
42
|
+
"CanSetShutter",
|
|
43
|
+
"CanSlave",
|
|
44
|
+
"CanSyncAzimuth",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def probe_dome(
|
|
49
|
+
host: str,
|
|
50
|
+
port: int,
|
|
51
|
+
device_number: int = 0,
|
|
52
|
+
*,
|
|
53
|
+
protocol: str = "http",
|
|
54
|
+
allow_active: bool = False,
|
|
55
|
+
collect_host_info: bool | None = None,
|
|
56
|
+
dome_factory: Callable[[str, int, str], Any] | None = None,
|
|
57
|
+
progress: ProbeProgress | None = None,
|
|
58
|
+
) -> ProbeResult:
|
|
59
|
+
"""
|
|
60
|
+
Build a JSON-serialisable diagnostic report for an Alpaca dome at
|
|
61
|
+
``protocol://host:port`` (device_number). See :func:`.probe.probe_device`
|
|
62
|
+
for the ``collect_host_info`` semantics.
|
|
63
|
+
|
|
64
|
+
``dome_factory(address, device_number, protocol)`` is only there for
|
|
65
|
+
tests — production callers leave it ``None`` and the real
|
|
66
|
+
``alpaca.dome.Dome`` is used.
|
|
67
|
+
"""
|
|
68
|
+
if dome_factory is None:
|
|
69
|
+
from alpaca.dome import Dome # local import: keep cost off cold paths
|
|
70
|
+
|
|
71
|
+
dome_factory = Dome
|
|
72
|
+
|
|
73
|
+
return probe_device(
|
|
74
|
+
"dome",
|
|
75
|
+
host,
|
|
76
|
+
port,
|
|
77
|
+
device_number,
|
|
78
|
+
properties=_READ_ONLY_PROPERTIES,
|
|
79
|
+
device_factory=dome_factory,
|
|
80
|
+
protocol=protocol,
|
|
81
|
+
allow_active=allow_active,
|
|
82
|
+
collect_host_info=collect_host_info,
|
|
83
|
+
progress=progress,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def iter_probe_labels() -> Iterable[str]:
|
|
88
|
+
"""Convenience for tests / docs: enumerate the labels emitted by a run."""
|
|
89
|
+
return _iter_probe_labels(_READ_ONLY_PROPERTIES)
|
arcsecond-3.20.1/arcsecond/alpaca/dome_probe.py → arcsecond-3.20.2/arcsecond/alpaca/probe.py
RENAMED
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
behaviour against a small set of safe candidate commands
|
|
19
|
-
* best-effort local host hints (open ports, COM ProgIDs), collected
|
|
20
|
-
automatically when the target is this machine, the only case in which
|
|
21
|
-
they describe the Alpaca server's host at all
|
|
22
|
-
|
|
23
|
-
It never issues motion-class commands. ``CommandBlind`` and active Galil DMC
|
|
24
|
-
verbs are gated behind ``allow_active=True``.
|
|
2
|
+
The read-only probe engine shared by every ``arcsecond alpaca probe <device>``
|
|
3
|
+
command.
|
|
4
|
+
|
|
5
|
+
A probe never issues motion-class commands. It reads a device's standard
|
|
6
|
+
properties, its ``SupportedActions`` list, and how the legacy
|
|
7
|
+
``CommandString`` / ``CommandBool`` / ``CommandBlind`` passthroughs behave
|
|
8
|
+
against a small set of safe candidate commands, and writes it all out as one
|
|
9
|
+
JSON report in which every outcome, including every error, is data.
|
|
10
|
+
|
|
11
|
+
``CommandBlind`` and active Galil DMC verbs are gated behind
|
|
12
|
+
``allow_active=True``. Local host hints (open ports, COM ProgIDs) are attached
|
|
13
|
+
automatically when the target is this machine, the only case in which they
|
|
14
|
+
describe the Alpaca server's host at all.
|
|
15
|
+
|
|
16
|
+
Device modules (``dome_probe``, ``telescope_probe``) own only what differs per
|
|
17
|
+
device: the property list, the alpyca class, and any extra read-only section.
|
|
25
18
|
"""
|
|
26
19
|
|
|
27
20
|
from __future__ import annotations
|
|
@@ -35,35 +28,14 @@ import sys
|
|
|
35
28
|
import traceback
|
|
36
29
|
from dataclasses import dataclass, field
|
|
37
30
|
from datetime import datetime, timezone
|
|
38
|
-
from
|
|
39
|
-
|
|
40
|
-
# Read-only device descriptors and dome state, in the order we want them
|
|
41
|
-
# presented in the report. Names are PascalCase to match the alpyca surface.
|
|
42
|
-
_READ_ONLY_PROPERTIES: tuple[str, ...] = (
|
|
43
|
-
"Name",
|
|
44
|
-
"Description",
|
|
45
|
-
"DriverInfo",
|
|
46
|
-
"DriverVersion",
|
|
47
|
-
"InterfaceVersion",
|
|
48
|
-
"ShutterStatus",
|
|
49
|
-
"Altitude",
|
|
50
|
-
"Azimuth",
|
|
51
|
-
"AtHome",
|
|
52
|
-
"AtPark",
|
|
53
|
-
"Slewing",
|
|
54
|
-
"Slaved",
|
|
55
|
-
"CanFindHome",
|
|
56
|
-
"CanPark",
|
|
57
|
-
"CanSetAltitude",
|
|
58
|
-
"CanSetPark",
|
|
59
|
-
"CanSetShutter",
|
|
60
|
-
"CanSlave",
|
|
61
|
-
"CanSyncAzimuth",
|
|
62
|
-
)
|
|
31
|
+
from enum import Enum
|
|
32
|
+
from typing import Any, Callable, Iterable, Mapping
|
|
63
33
|
|
|
64
34
|
# Safe Galil DMC read-style verbs: print/report values without commanding
|
|
65
35
|
# motion. ``MG`` is "message" (print expression), ``TE`` is "tell error",
|
|
66
|
-
# ``TP`` is "tell position".
|
|
36
|
+
# ``TP`` is "tell position". They probe whether a driver forwards
|
|
37
|
+
# CommandString at all; a driver without the member (the DFM TCSGalil ones)
|
|
38
|
+
# fails every one of them, and that is the finding.
|
|
67
39
|
_READ_ONLY_COMMANDS: tuple[str, ...] = (
|
|
68
40
|
"MG TIME",
|
|
69
41
|
"MG _BGA",
|
|
@@ -125,8 +97,6 @@ def _coerce_jsonable(value: Any) -> Any:
|
|
|
125
97
|
"""Make values from alpyca safe to dump as JSON."""
|
|
126
98
|
# IntEnum is a subclass of int, so check it *before* the primitive branch.
|
|
127
99
|
# We want {"name": "shutterClosed", "value": 1} not just 1.
|
|
128
|
-
from enum import Enum
|
|
129
|
-
|
|
130
100
|
if isinstance(value, Enum):
|
|
131
101
|
return {"name": value.name, "value": value.value}
|
|
132
102
|
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
@@ -135,10 +105,18 @@ def _coerce_jsonable(value: Any) -> Any:
|
|
|
135
105
|
return [_coerce_jsonable(item) for item in value]
|
|
136
106
|
if isinstance(value, dict):
|
|
137
107
|
return {str(k): _coerce_jsonable(v) for k, v in value.items()}
|
|
108
|
+
if isinstance(value, datetime):
|
|
109
|
+
return value.isoformat()
|
|
110
|
+
# alpyca's ``Rate`` (from AxisRates) carries a range and nothing else.
|
|
111
|
+
if hasattr(value, "Minimum") and hasattr(value, "Maximum"):
|
|
112
|
+
return {
|
|
113
|
+
"minimum": _coerce_jsonable(value.Minimum),
|
|
114
|
+
"maximum": _coerce_jsonable(value.Maximum),
|
|
115
|
+
}
|
|
138
116
|
return str(value)
|
|
139
117
|
|
|
140
118
|
|
|
141
|
-
def
|
|
119
|
+
def capture(call: Callable[[], Any]) -> dict:
|
|
142
120
|
"""Run ``call()`` and capture either its value or its exception."""
|
|
143
121
|
try:
|
|
144
122
|
raw = call()
|
|
@@ -150,7 +128,21 @@ def _capture(call: Callable[[], Any]) -> dict:
|
|
|
150
128
|
return {"ok": True, "value": _truncate(_coerce_jsonable(raw))}
|
|
151
129
|
|
|
152
130
|
|
|
131
|
+
def record(
|
|
132
|
+
outcome: dict, label: str, progress: ProbeProgress, counts: ProbeCounts
|
|
133
|
+
) -> dict:
|
|
134
|
+
"""Count one captured outcome and report it to the caller; returns it."""
|
|
135
|
+
counts.total += 1
|
|
136
|
+
if outcome["ok"]:
|
|
137
|
+
counts.ok += 1
|
|
138
|
+
progress.emit(label, True, str(outcome["value"]))
|
|
139
|
+
else:
|
|
140
|
+
progress.emit(label, False, outcome["error"])
|
|
141
|
+
return outcome
|
|
142
|
+
|
|
143
|
+
|
|
153
144
|
def _build_environment(
|
|
145
|
+
kind: str,
|
|
154
146
|
host: str,
|
|
155
147
|
port: int,
|
|
156
148
|
device_number: int,
|
|
@@ -168,6 +160,7 @@ def _build_environment(
|
|
|
168
160
|
|
|
169
161
|
return {
|
|
170
162
|
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
163
|
+
"device_type": kind,
|
|
171
164
|
"host": host,
|
|
172
165
|
"port": port,
|
|
173
166
|
"device_number": device_number,
|
|
@@ -181,29 +174,25 @@ def _build_environment(
|
|
|
181
174
|
|
|
182
175
|
|
|
183
176
|
def _probe_device_metadata(
|
|
184
|
-
|
|
177
|
+
device: Any,
|
|
178
|
+
properties: Iterable[str],
|
|
185
179
|
progress: ProbeProgress,
|
|
186
180
|
counts: ProbeCounts,
|
|
187
181
|
) -> dict:
|
|
188
182
|
section: dict = {}
|
|
189
|
-
for name in
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if outcome["ok"]:
|
|
194
|
-
counts.ok += 1
|
|
195
|
-
progress.emit(name, True, str(outcome["value"]))
|
|
196
|
-
else:
|
|
197
|
-
progress.emit(name, False, outcome["error"])
|
|
183
|
+
for name in properties:
|
|
184
|
+
section[name] = record(
|
|
185
|
+
capture(lambda n=name: getattr(device, n)), name, progress, counts
|
|
186
|
+
)
|
|
198
187
|
return section
|
|
199
188
|
|
|
200
189
|
|
|
201
190
|
def _probe_supported_actions(
|
|
202
|
-
|
|
191
|
+
device: Any,
|
|
203
192
|
progress: ProbeProgress,
|
|
204
193
|
counts: ProbeCounts,
|
|
205
194
|
) -> dict:
|
|
206
|
-
outcome =
|
|
195
|
+
outcome = capture(lambda: device.SupportedActions)
|
|
207
196
|
counts.total += 1
|
|
208
197
|
if outcome["ok"]:
|
|
209
198
|
counts.ok += 1
|
|
@@ -230,7 +219,7 @@ def _send_command(
|
|
|
230
219
|
|
|
231
220
|
|
|
232
221
|
def _probe_command_passthrough(
|
|
233
|
-
|
|
222
|
+
device: Any,
|
|
234
223
|
*,
|
|
235
224
|
allow_active: bool,
|
|
236
225
|
progress: ProbeProgress,
|
|
@@ -247,7 +236,7 @@ def _probe_command_passthrough(
|
|
|
247
236
|
commands = commands + _ACTIVE_COMMANDS
|
|
248
237
|
|
|
249
238
|
for cmd in commands:
|
|
250
|
-
entry = _send_command(
|
|
239
|
+
entry = _send_command(device.CommandString, cmd, False)
|
|
251
240
|
section["command_string"].append(entry)
|
|
252
241
|
counts.total += 1
|
|
253
242
|
counts.ok += 1 if entry["ok"] else 0
|
|
@@ -257,7 +246,7 @@ def _probe_command_passthrough(
|
|
|
257
246
|
entry.get("error") or str(entry.get("response", ""))[:80],
|
|
258
247
|
)
|
|
259
248
|
|
|
260
|
-
entry = _send_command(
|
|
249
|
+
entry = _send_command(device.CommandBool, cmd, False)
|
|
261
250
|
section["command_bool"].append(entry)
|
|
262
251
|
counts.total += 1
|
|
263
252
|
counts.ok += 1 if entry["ok"] else 0
|
|
@@ -270,7 +259,7 @@ def _probe_command_passthrough(
|
|
|
270
259
|
if allow_active:
|
|
271
260
|
# CommandBlind is fire-and-forget — only attempt under --allow-active.
|
|
272
261
|
for cmd in commands:
|
|
273
|
-
entry = _send_command(
|
|
262
|
+
entry = _send_command(device.CommandBlind, cmd, False)
|
|
274
263
|
section["command_blind"].append(entry)
|
|
275
264
|
counts.total += 1
|
|
276
265
|
counts.ok += 1 if entry["ok"] else 0
|
|
@@ -421,104 +410,109 @@ def _collect_host_hints() -> dict:
|
|
|
421
410
|
return _collect_posix_host_hints()
|
|
422
411
|
|
|
423
412
|
|
|
424
|
-
def
|
|
413
|
+
def _attach_host_hints(
|
|
414
|
+
report: dict, host: str, target_is_local: bool, collect_host_info: bool | None
|
|
415
|
+
) -> None:
|
|
416
|
+
if collect_host_info is None:
|
|
417
|
+
collect_host_info = target_is_local
|
|
418
|
+
reason = HOST_HINTS_REASON_LOCAL
|
|
419
|
+
else:
|
|
420
|
+
reason = HOST_HINTS_REASON_REQUESTED
|
|
421
|
+
|
|
422
|
+
if collect_host_info:
|
|
423
|
+
report["host_hints"] = {"collected_because": reason, **_collect_host_hints()}
|
|
424
|
+
elif reason == HOST_HINTS_REASON_REQUESTED:
|
|
425
|
+
report["host_hints_skipped_reason"] = "Disabled by the caller."
|
|
426
|
+
else:
|
|
427
|
+
report["host_hints_skipped_reason"] = (
|
|
428
|
+
f"{host} is not this machine, so the hints would describe the wrong "
|
|
429
|
+
"host; pass --collect-host-info to force them."
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
ExtraProbe = Callable[[Any, ProbeProgress, ProbeCounts], Any]
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def probe_device(
|
|
437
|
+
kind: str,
|
|
425
438
|
host: str,
|
|
426
439
|
port: int,
|
|
427
440
|
device_number: int = 0,
|
|
428
441
|
*,
|
|
442
|
+
properties: tuple[str, ...],
|
|
443
|
+
device_factory: Callable[[str, int, str], Any],
|
|
429
444
|
protocol: str = "http",
|
|
430
445
|
allow_active: bool = False,
|
|
431
446
|
collect_host_info: bool | None = None,
|
|
432
|
-
|
|
447
|
+
extra_probes: Mapping[str, ExtraProbe] | None = None,
|
|
433
448
|
progress: ProbeProgress | None = None,
|
|
434
449
|
) -> ProbeResult:
|
|
435
450
|
"""
|
|
436
|
-
Build a JSON-serialisable diagnostic report for
|
|
437
|
-
``protocol://host:port`` (device_number).
|
|
451
|
+
Build a JSON-serialisable diagnostic report for the Alpaca ``kind`` device
|
|
452
|
+
at ``protocol://host:port`` (device_number).
|
|
453
|
+
|
|
454
|
+
``properties`` are read in order into ``device_metadata``. Each
|
|
455
|
+
``extra_probes`` entry adds a section under its key, right after the
|
|
456
|
+
metadata; it receives the device, the progress sink and the counts.
|
|
438
457
|
|
|
439
458
|
``collect_host_info`` left at ``None`` attaches the local host hints when
|
|
440
459
|
``host`` is this machine and skips them otherwise, recording which in the
|
|
441
460
|
report. ``True`` forces them for a remote target (they still describe the
|
|
442
461
|
probing machine, not the target); ``False`` skips them for a local one.
|
|
443
462
|
|
|
444
|
-
``
|
|
445
|
-
|
|
446
|
-
``alpaca.dome.Dome`` is used.
|
|
463
|
+
``device_factory(address, device_number, protocol)`` is the alpyca class
|
|
464
|
+
in production and a stand-in in tests.
|
|
447
465
|
"""
|
|
448
466
|
progress = progress or ProbeProgress()
|
|
449
467
|
counts = ProbeCounts()
|
|
450
468
|
address = f"{host}:{port}"
|
|
451
469
|
|
|
452
|
-
|
|
453
|
-
from alpaca.dome import Dome # local import: keep cost off cold paths
|
|
454
|
-
|
|
455
|
-
dome_factory = Dome
|
|
456
|
-
|
|
457
|
-
# Build dome client. A failure here is fatal (host unreachable, or the
|
|
470
|
+
# Build the client. A failure here is fatal (host unreachable, or the
|
|
458
471
|
# alpyca constructor itself blew up) so we surface it to the caller.
|
|
459
472
|
try:
|
|
460
|
-
|
|
473
|
+
device = device_factory(address, device_number, protocol)
|
|
461
474
|
except Exception as exc: # noqa: BLE001
|
|
462
475
|
raise RuntimeError(
|
|
463
|
-
f"Could not construct Alpaca
|
|
476
|
+
f"Could not construct Alpaca {kind.capitalize()} client for "
|
|
464
477
|
f"{protocol}://{address} (device {device_number}): "
|
|
465
478
|
f"{type(exc).__name__}: {exc}\n" + traceback.format_exc()
|
|
466
479
|
) from exc
|
|
467
480
|
|
|
468
|
-
connected_outcome =
|
|
469
|
-
|
|
470
|
-
counts.ok += 1 if connected_outcome["ok"] else 0
|
|
471
|
-
progress.emit(
|
|
472
|
-
"Connected",
|
|
473
|
-
connected_outcome["ok"],
|
|
474
|
-
connected_outcome.get("error") or str(connected_outcome.get("value")),
|
|
481
|
+
connected_outcome = record(
|
|
482
|
+
capture(lambda: device.Connected), "Connected", progress, counts
|
|
475
483
|
)
|
|
476
484
|
|
|
477
485
|
target_is_local = is_local_target(host)
|
|
478
486
|
environment = _build_environment(
|
|
479
|
-
host, port, device_number, protocol, connected_outcome
|
|
487
|
+
kind, host, port, device_number, protocol, connected_outcome
|
|
480
488
|
)
|
|
481
489
|
environment["target_is_local"] = target_is_local
|
|
482
490
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
491
|
+
report: dict = {
|
|
492
|
+
"environment": environment,
|
|
493
|
+
"device_metadata": _probe_device_metadata(device, properties, progress, counts),
|
|
494
|
+
}
|
|
495
|
+
for key, run in (extra_probes or {}).items():
|
|
496
|
+
report[key] = run(device, progress, counts)
|
|
497
|
+
report["supported_actions"] = _probe_supported_actions(device, progress, counts)
|
|
498
|
+
report["command_passthrough"] = _probe_command_passthrough(
|
|
499
|
+
device,
|
|
487
500
|
allow_active=allow_active,
|
|
488
501
|
progress=progress,
|
|
489
502
|
counts=counts,
|
|
490
503
|
)
|
|
491
504
|
|
|
492
|
-
report
|
|
493
|
-
"environment": environment,
|
|
494
|
-
"device_metadata": device_metadata,
|
|
495
|
-
"supported_actions": supported_actions,
|
|
496
|
-
"command_passthrough": command_passthrough,
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
if collect_host_info is None:
|
|
500
|
-
collect_host_info = target_is_local
|
|
501
|
-
reason = HOST_HINTS_REASON_LOCAL
|
|
502
|
-
else:
|
|
503
|
-
reason = HOST_HINTS_REASON_REQUESTED
|
|
504
|
-
|
|
505
|
-
if collect_host_info:
|
|
506
|
-
report["host_hints"] = {"collected_because": reason, **_collect_host_hints()}
|
|
507
|
-
elif reason == HOST_HINTS_REASON_REQUESTED:
|
|
508
|
-
report["host_hints_skipped_reason"] = "Disabled by the caller."
|
|
509
|
-
else:
|
|
510
|
-
report["host_hints_skipped_reason"] = (
|
|
511
|
-
f"{host} is not this machine, so the hints would describe the wrong "
|
|
512
|
-
"host; pass --collect-host-info to force them."
|
|
513
|
-
)
|
|
514
|
-
|
|
505
|
+
_attach_host_hints(report, host, target_is_local, collect_host_info)
|
|
515
506
|
return ProbeResult(report=report, counts=counts)
|
|
516
507
|
|
|
517
508
|
|
|
518
|
-
def iter_probe_labels(
|
|
509
|
+
def iter_probe_labels(
|
|
510
|
+
properties: Iterable[str], extra_labels: Iterable[str] = ()
|
|
511
|
+
) -> Iterable[str]:
|
|
519
512
|
"""Convenience for tests / docs: enumerate the labels emitted by a run."""
|
|
520
513
|
yield "Connected"
|
|
521
|
-
yield from
|
|
514
|
+
yield from properties
|
|
515
|
+
yield from extra_labels
|
|
522
516
|
yield "SupportedActions"
|
|
523
517
|
for cmd in _READ_ONLY_COMMANDS:
|
|
524
518
|
yield f"CommandString({cmd!r})"
|