From f2bc012d8b9032b411bc871b9ea8c3b424c9a17b Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:45:43 -0700 Subject: [PATCH 1/2] Make the Python module name a build option so instrument builds can coexist --- README.md | 9 +++++++++ python/CMakeLists.txt | 7 +++++++ python/camera_interface_module.cpp | 10 ++++++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce112e5..bcdc844 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,15 @@ Every command `camerad` accepts is reachable: the base commands are bound as met The controller and instrument are fixed at CMake configure time, so `instrument_name()` and `controller_name()` report which build was loaded. A failed command raises `RuntimeError`. +Because one build serves one instrument, two instruments mean two builds, and they collide in a shared environment while both are called `camera_interface`. `-DCAMERAD_MODULE_NAME=` renames the module and its file together, so per-instrument builds can be installed and imported side by side: + +```bash +$ cmake -DBUILD_PYTHON_MODULE=ON -DINSTRUMENT=hispec_tracking_camera \ + -DCAMERAD_MODULE_NAME=camera_interface_tracking .. +``` + +It defaults to `camera_interface`, so a build that does not set it is unaffected. + `output_status()` is a snapshot, never a barrier: the FITS writer queues and drops frames by design because disk is slower than acquisition can be, so nothing here lets a caller stall acquisition by waiting on an output. Anything needing to be woken per frame should attach to the shared-memory segment, which posts semaphores. Commands release the GIL while they run, so a blocking `expose()` leaves the rest of the process responsive. diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 6d4e3a2..c3f5b7d 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -39,8 +39,15 @@ target_include_directories(camera_interface PRIVATE ${INTERFACE_INCLUDES} ) +set(CAMERAD_MODULE_NAME "camera_interface" CACHE STRING + "Import name of the Python module") + +# Filename and import name must agree, so both come from CAMERAD_MODULE_NAME +set_target_properties(camera_interface PROPERTIES OUTPUT_NAME ${CAMERAD_MODULE_NAME}) + target_compile_definitions(camera_interface PRIVATE CAMERAD_INSTRUMENT_NAME="${INSTRUMENT}" + CAMERAD_MODULE_NAME=${CAMERAD_MODULE_NAME} ) # Same link set as camerad, minus the server sources it does not use diff --git a/python/camera_interface_module.cpp b/python/camera_interface_module.cpp index d0ddbfd..22d6406 100644 --- a/python/camera_interface_module.cpp +++ b/python/camera_interface_module.cpp @@ -118,7 +118,11 @@ namespace { } -PYBIND11_MODULE(camera_interface, module) { +#ifndef CAMERAD_MODULE_NAME +#define CAMERAD_MODULE_NAME camera_interface +#endif + +PYBIND11_MODULE(CAMERAD_MODULE_NAME, module) { module.doc() = "Direct control of a camera-interface camera, without camerad"; module.def("instrument_name", [] { return std::string(INSTRUMENT_NAME); }, @@ -126,7 +130,9 @@ PYBIND11_MODULE(camera_interface, module) { module.def("controller_name", [] { return std::string(CONTROLLER_NAME); }, "Return the controller this module was built for"); - py::class_(module, "Camera", + // module_local keeps this out of pybind11's process-wide type registry, so + // two per-instrument builds can be imported together + py::class_(module, "Camera", py::module_local(), "One camera, configured from a camerad .cfg file.\n\n" "Construction performs the same setup camerad does at startup: read the\n" "config, initialize logging, then configure the controller, interface,\n" From 90c351169aedd8ee40b1dcc6b6ac56a360a2d736 Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:43:21 -0700 Subject: [PATCH 2/2] Add a per-instrument package for the tracking camera --- README.md | 16 ++++++++++++++-- packaging/tracking/pyproject.toml | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 packaging/tracking/pyproject.toml diff --git a/README.md b/README.md index bcdc844..5ea2934 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ Any CMake option can be passed the same way, so `--config-settings=cmake.define. pybind11 comes from `[build-system] requires`, so pip fetches it into an isolated build environment. It is never installed into the environment being built for. -The controller and instrument are fixed when the wheel is built, and the module is always named `camera_interface`, so one environment holds one instrument's build. Install into a separate environment per instrument and have the caller assert which one it loaded: +The controller and instrument are fixed when the wheel is built, so a caller should assert which build it loaded: ```python assert camera_interface.instrument_name() == "hispec_tracking_camera" @@ -168,6 +168,18 @@ assert camera_interface.instrument_name() == "hispec_tracking_camera" A compiler and the full dependency set have to be present wherever `pip install` runs, since it compiles camerad and the module from source. +### Per-instrument packages + +Installing this way twice replaces the first build, since both wheels are called `camera-interface` and both modules `camera_interface`. `packaging/` holds one directory per instrument that needs its own package, each fixing the instrument and the module name so neither is the caller's to pass: + +```bash +$ pip install ./camera-interface/packaging/tracking +``` + +That installs `camera-interface-tracking`, providing the module `camera_interface_tracking`. Such packages are independent of each other and of the plain `camera-interface` above, so any combination can share one environment. + +To add one, copy a `packaging/*/pyproject.toml` and change `name`, `INSTRUMENT` and `CAMERAD_MODULE_NAME`. + ## Python Module Built with `-DBUILD_PYTHON_MODULE=ON`, `camera_interface` lets a Python process own a camera directly, with no `camerad` process and no text protocol in between. It performs the same startup `camerad` does, then exposes the interface as methods: @@ -191,7 +203,7 @@ Every command `camerad` accepts is reachable: the base commands are bound as met The controller and instrument are fixed at CMake configure time, so `instrument_name()` and `controller_name()` report which build was loaded. A failed command raises `RuntimeError`. -Because one build serves one instrument, two instruments mean two builds, and they collide in a shared environment while both are called `camera_interface`. `-DCAMERAD_MODULE_NAME=` renames the module and its file together, so per-instrument builds can be installed and imported side by side: +`-DCAMERAD_MODULE_NAME=` renames the module and its file together, so per-instrument builds can be imported side by side. [Per-instrument packages](#per-instrument-packages) is the packaged form of the same thing: ```bash $ cmake -DBUILD_PYTHON_MODULE=ON -DINSTRUMENT=hispec_tracking_camera \ diff --git a/packaging/tracking/pyproject.toml b/packaging/tracking/pyproject.toml new file mode 100644 index 0000000..54c7e61 --- /dev/null +++ b/packaging/tracking/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["scikit-build-core>=0.10", "pybind11>=2.12"] +build-backend = "scikit_build_core.build" + +[project] +name = "camera-interface-tracking" +version = "2.0.0" +description = "camera_interface built for the HISPEC tracking camera" +requires-python = ">=3.10" + +[tool.scikit-build] +cmake.version = ">=3.15" +cmake.source-dir = "../.." +wheel.packages = [] + +cmake.define.BUILD_PYTHON_MODULE = "ON" +cmake.define.CONTROLLER = "archon" +cmake.define.INSTRUMENT = "hispec_tracking_camera" +cmake.define.CAMERAD_MODULE_NAME = "camera_interface_tracking"