├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── Dockerfile ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── maintainer-notes.md ├── pyproject.toml ├── setup.py ├── src └── pymmcore │ ├── __init__.py │ ├── __init__.pyi │ ├── _version.py │ ├── py.typed │ └── pymmcore_swig.i └── tests └── test_mmcore.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.yml] 11 | indent_size = 2 12 | 13 | [*.md] 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | commit-message: 10 | prefix: "ci(dependabot):" 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build & deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | tags: 10 | - "v*" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | # check that sdist contains all files and that extra files 18 | # are explicitly ignored in manifest or pyproject 19 | check-manifest: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | submodules: "recursive" 26 | - name: Check manifest 27 | run: pipx run check-manifest 28 | 29 | test: 30 | name: Test ${{ matrix.os }} py${{ matrix.python-version }} np${{ matrix.numpy }} 31 | runs-on: ${{ matrix.os }} 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | python-version: ["3.9", "3.11", "3.13"] 36 | os: [ubuntu-latest, windows-latest, macos-latest] 37 | include: 38 | - python-version: "3.9" 39 | os: ubuntu-latest 40 | numpy: "~=1.25" 41 | - python-version: "3.10" 42 | os: ubuntu-latest 43 | numpy: "~=1.26" 44 | - python-version: "3.12" 45 | os: ubuntu-latest 46 | numpy: ">=2.0.0" 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | submodules: "recursive" 52 | 53 | - name: Set up Python ${{ matrix.python-version }} 54 | uses: actions/setup-python@v5 55 | with: 56 | python-version: ${{ matrix.python-version }} 57 | allow-prereleases: true 58 | 59 | - name: Install dependencies 60 | run: | 61 | python -m pip install --upgrade pip 62 | python -m pip install .[test] 63 | python -m pip install numpy${{ matrix.numpy }} 64 | 65 | - name: Run tests 66 | run: pytest -v tests --color=yes 67 | 68 | test-pymmcore-plus: 69 | name: test pymmcore-plus ${{ matrix.os }} 70 | runs-on: ${{ matrix.os }} 71 | strategy: 72 | fail-fast: false 73 | matrix: 74 | os: [ubuntu-latest, windows-latest, macos-latest] 75 | env: 76 | UV_NO_SYNC: "1" 77 | steps: 78 | - uses: actions/checkout@v4 79 | with: 80 | repository: pymmcore-plus/pymmcore-plus 81 | - uses: actions/checkout@v4 82 | with: 83 | path: pymmcore 84 | submodules: "recursive" 85 | - uses: astral-sh/setup-uv@v6 86 | with: 87 | python-version: "3.13" 88 | - name: Setup MM test adapters 89 | uses: pymmcore-plus/setup-mm-test-adapters@main 90 | - name: Install dependencies 91 | run: | 92 | uv sync --no-dev --group test 93 | uv pip install ./pymmcore 94 | uv pip list 95 | 96 | - run: uv run pytest -v --color=yes -W ignore 97 | 98 | build_wheels: 99 | if: github.event_name != 'pull_request' 100 | name: Build wheels on ${{ matrix.os }} ${{ matrix.macos_arch }} 101 | runs-on: ${{ matrix.os }} 102 | strategy: 103 | fail-fast: false 104 | matrix: 105 | os: [ubuntu-latest, windows-latest] 106 | include: 107 | - os: macos-13 108 | macos_arch: "x86_64" 109 | - os: macos-latest 110 | macos_arch: "arm64" 111 | 112 | steps: 113 | - uses: actions/checkout@v4 114 | with: 115 | submodules: "recursive" 116 | 117 | - uses: ilammy/msvc-dev-cmd@v1 118 | with: 119 | toolset: "14.2" 120 | 121 | - name: Build wheels 122 | uses: pypa/cibuildwheel@v2.23 123 | env: 124 | CIBW_VERBOSE: 1 125 | CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" 126 | # Python on Linux is usually configured to add debug information, 127 | # which increases binary size by ~11-fold. Remove for the builds we 128 | # distribute. 129 | CIBW_ENVIRONMENT_LINUX: "LDFLAGS=-Wl,--strip-debug" 130 | 131 | - uses: actions/upload-artifact@v4 132 | with: 133 | name: artifact-wheels-${{ matrix.os }}${{ matrix.macos_arch }} 134 | path: ./wheelhouse/*.whl 135 | 136 | build_sdist: 137 | name: Build source distribution 138 | runs-on: ubuntu-latest 139 | steps: 140 | - uses: actions/checkout@v4 141 | with: 142 | fetch-depth: 0 143 | submodules: "recursive" 144 | 145 | - name: Build sdist 146 | run: | 147 | pip install -U pip build check-manifest 148 | check-manifest 149 | python -m build --sdist 150 | 151 | - uses: actions/upload-artifact@v4 152 | with: 153 | name: artifact-sdist 154 | path: dist/*.tar.gz 155 | 156 | upload_pypi: 157 | needs: [build_wheels, build_sdist] 158 | runs-on: ubuntu-latest 159 | # upload to PyPI on every tag 160 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 161 | 162 | # https://docs.pypi.org/trusted-publishers/ 163 | permissions: 164 | id-token: write # for trusted publishing on PyPi 165 | contents: write # allows writing releases 166 | 167 | steps: 168 | - uses: actions/download-artifact@v4 169 | with: 170 | pattern: artifact-* 171 | merge-multiple: true 172 | path: dist 173 | 174 | - name: Publish to PyPI 175 | uses: pypa/gh-action-pypi-publish@release/v1 176 | 177 | - uses: softprops/action-gh-release@v2 178 | with: 179 | generate_release_notes: true 180 | files: "./dist/*" 181 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*.swp 3 | 4 | venv/ 5 | 6 | build/ 7 | dist/ 8 | *.pdb 9 | *.py[cod] 10 | 11 | wheelhouse/ 12 | 13 | src/pymmcore/pymmcore_swig_wrap.h 14 | src/pymmcore/pymmcore_swig_wrap.cpp 15 | src/pymmcore/_pymmcore_swig.* 16 | src/pymmcore/pymmcore_swig.py 17 | pymmcore.egg-info 18 | .mypy_cache/ 19 | 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "mmCoreAndDevices"] 2 | path = mmCoreAndDevices 3 | url = https://github.com/micro-manager/mmCoreAndDevices.git 4 | branch = main 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This dockerfile creates a minimal pymmcore image for demonstration purposes 2 | # (for example, DemoCamera should work). 3 | 4 | FROM ubuntu:18.04 5 | 6 | # System packages 7 | RUN apt-get update && apt-get install -y \ 8 | bzip2 \ 9 | bc \ 10 | build-essential \ 11 | cmake \ 12 | curl \ 13 | g++ \ 14 | gfortran \ 15 | libtool \ 16 | autoconf \ 17 | automake \ 18 | git \ 19 | pkg-config \ 20 | software-properties-common \ 21 | unzip \ 22 | wget \ 23 | libboost-all-dev \ 24 | && \ 25 | apt-get clean && \ 26 | apt-get autoremove && \ 27 | rm -rf /var/lib/apt/lists/* 28 | RUN addgroup --gid 1001 python && \ 29 | useradd --uid 1001 --gid 1001 python 30 | 31 | RUN git clone https://github.com/micro-manager/micro-manager.git && \ 32 | cd micro-manager && \ 33 | git submodule update --init --recursive && \ 34 | ./autogen.sh && \ 35 | ./configure --without-java && \ 36 | make && \ 37 | make install 38 | ENV MMCORE_PATH=/usr/local/lib/micro-manager/ 39 | ENV MMCONFIG_DEMO_PATH=/micro-manager/bindist/any-platform/MMConfig_demo.cfg 40 | RUN cp $MMCONFIG_DEMO_PATH $MMCORE_PATH/MMConfig_demo.cfg 41 | 42 | # Install miniconda to /miniconda 43 | RUN curl -LO https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh 44 | RUN bash Miniconda3-latest-Linux-x86_64.sh -p /miniconda -b 45 | RUN rm Miniconda3-latest-Linux-x86_64.sh 46 | ENV PATH=/miniconda/bin:${PATH} 47 | 48 | RUN conda update -y conda && \ 49 | conda install -y python=3.10 numpy && \ 50 | pip install --upgrade pip && \ 51 | pip install pymmcore 52 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include * *.pyi 2 | recursive-include mmCoreAndDevices/MMDevice *.h *.cpp 3 | recursive-include mmCoreAndDevices/MMCore *.h *.cpp 4 | 5 | prune mmCoreAndDevices/MMDevice/unittest 6 | prune mmCoreAndDevices/tools 7 | prune mmCoreAndDevices/MMCore/unittest 8 | prune mmCoreAndDevices/MMCoreJ_wrap 9 | prune mmCoreAndDevices/DeviceAdapters 10 | prune mmCoreAndDevices/m4 11 | prune mmCoreAndDevices/.github 12 | recursive-exclude mmCoreAndDevices *.txt *.md *.am .project \ 13 | *.vcxproj* *.sln *.props *.cdt* secret-device-* meson.build *.wrap 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pymmcore: Python bindings for MMCore 2 | 3 | The pymmcore package provides Python 3.x bindings to Micro-Manager's MMCore 4 | (the low-level device control/acquisition interface). 5 | 6 | Using pymmcore, you can control and acquire images from all of the microscope 7 | devices supported by Micro-Manager, but without the GUI application or a Java 8 | runtime. 9 | 10 | Not to be confused with 11 | [pycro-manager](https://github.com/micro-manager/pycro-manager), which allows 12 | control of the entire Java Micro-Manager application, including its Java APIs, 13 | and more. 14 | 15 | You might also be interested in 16 | [pymmcore-plus](https://pymmcore-plus.readthedocs.io) which wraps this library 17 | and provides extra functionality including an acquisition engine. 18 | 19 | Note: pymmcore is similar to the legacy MMCorePy module (Python 2.x only), 20 | previously distributed with the Micro-Manager application. However, the Python 21 | package for pymmcore is named `pymmcore` instead of `MMCorePy`. This is in part 22 | to avoid importing the wrong package on systems where `pymmcore` (usually 23 | installed via `pip`) and `MMCorePy` (installed with the Micro-Manager app or 24 | built by the user) both exist. 25 | 26 | Because pymmcore is distributed separately from Micro-Manager, it needs to be 27 | "pointed" at an existing Micro-Manager installation to access device adapters. 28 | (See the example below.) 29 | 30 | ## Installing 31 | 32 | Suports Python 3.9 or later and Windows, macOS, and Linux (all 64-bit). 33 | 34 | ``` 35 | pip install pymmcore 36 | ``` 37 | 38 | Or install via conda: 39 | 40 | ``` 41 | conda install -c conda-forge pymmcore 42 | ``` 43 | 44 | You also need a working installation of the Micro-Manager device adapters. 45 | (for a convenient way to install that programmatically, see 46 | the [`mmcore install` command in pymmcore plus](https://pymmcore-plus.github.io/pymmcore-plus/install/#installing-micro-manager-device-adapters)) 47 | 48 | ## Quick example 49 | 50 | ```python 51 | import pymmcore 52 | import os.path 53 | import os 54 | 55 | mm_dir = "C:/Program Files/Micro-Manager-2.0.x" 56 | 57 | mmc = pymmcore.CMMCore() 58 | 59 | os.environ["PATH"] += os.pathsep.join(["", mm_dir]) # adviseable on Windows 60 | mmc.setDeviceAdapterSearchPaths([mm_dir]) 61 | mmc.loadSystemConfiguration(os.path.join(mm_dir, "MMConfig_demo.cfg")) 62 | 63 | mmc.snapImage() 64 | mmc.getImage() 65 | ``` 66 | 67 | We do not currently have Python-specific documentation for MMCore, but 68 | the [pymmcore-plus documentation](https://pymmcore-plus.github.io/pymmcore-plus/api/cmmcoreplus) 69 | includes the [pymmcore.CMMCore class](https://pymmcore-plus.github.io/pymmcore-plus/api/cmmcoreplus/#pymmcore.CMMCore). There is also [C++ 70 | documentation](https://micro-manager.org/apidoc/MMCore/latest/). 71 | 72 | ## Matching Micro-Manager and pymmcore versions 73 | 74 | The version number of pymmcore is independent of the Micro-Manager version 75 | number; instead it tracks the MMCore and device interface versions. 76 | 77 | In order to use a given Micro-Manager installation, the _device interface 78 | version_ must match between pymmcore and the Micro-Manager device adapters 79 | (`mmgr_dal_*.dll` on Windows). 80 | 81 | The device interface version of a given Micro-Manager installation can be 82 | viewed in **Help** > **About Micro-Manager**. 83 | 84 | The device interface version of a given pymmcore version is the fourth part in 85 | the version number, and can also be viewed as follows: 86 | 87 | ```python 88 | import pymmcore 89 | pymmcore.CMMCore().getAPIVersionInfo() 90 | ``` 91 | 92 | Note that `getAPIVersionInfo()` should not be confused with `getVersionInfo()`, 93 | which returns the version number of MMCore. (The MMCore version is the first 3 94 | parts of the pymmcore version.) 95 | 96 | - For example, pymmcore `10.1.1.69.0` is based on MMCore `10.1.1` and has 97 | device interface version `69`. 98 | - The device interface version can change independently of the MMCore version, 99 | although it is less common for the device interface version to be incremented 100 | without a corresponding version change of MMCore. 101 | - Older versions of pymmcore did not include the device interface version in 102 | their version number. 103 | 104 | For a list of device interface versions for each pymmcore version, see the 105 | [Releases](https://github.com/micro-manager/pymmcore/releases) page. 106 | 107 | ## Loading device adapters on Windows 108 | 109 | The majority of device adapters should load once 110 | `setDeviceAdapterSearchPaths()` has been called with the correct directories, 111 | as in the above example. However, you may have trouble with device adapters 112 | that in turn depend on external DLLs (typically equipment vendor libraries). 113 | 114 | To fix this, _first ensure that the Micro-Manager application can correctly 115 | load all the devices_ using the same configuration file. Then, use one of the 116 | following: 117 | 118 | - Temporarily change the current directory to the Micro-Manager installation 119 | when loading the configuration file (use `os.chdir()`). 120 | 121 | - Add the Micro-Manager directory to the `PATH` environment variable. 122 | 123 | The first method mimics how the Micro-Manager application works (it always run 124 | with the current directory set to the installation directory). However, the 125 | second method may be more robust in case the external DLLs in turn load 126 | additional DLLs at a later time. 127 | 128 | Please report any cases where the Micro-Manager application can load a 129 | configuration but pymmcore cannot, even when using the above methods. 130 | 131 | ## Code of Conduct 132 | 133 | This project is covered by the [Micro-Manager Code of Conduct](https://github.com/micro-manager/micro-manager/blob/master/CodeOfConduct.md). 134 | 135 | ## License 136 | 137 | The license for pymmcore itself is LGPL 2.1 (see `LICENSE.txt`). The MMCore 138 | component of Micro-Manager (which gets built into pymmcore) is also under the 139 | same license. Other parts of Micro-Manager are under different licenses. 140 | -------------------------------------------------------------------------------- /maintainer-notes.md: -------------------------------------------------------------------------------- 1 | ## Versioning scheme 2 | 3 | Cf. PEP 440. 4 | 5 | Concatenate the MMCore version, MMDevice device interface version, and an extra 6 | pymmcore-specific suffix. For example, pymmcore 10.1.1.69.0 wraps MMCore 10.1.1 7 | and works with device adapters built for device interface version 69. The 8 | final suffix can be incremented to create a new release for improvements to the 9 | pymmcore wrapper. 10 | 11 | ``` 12 | pymmcore v10.1.1.69.0 13 | | | | 14 | | | +----> pymmcore-specific suffix 15 | | +-------> MMDevice device interface version 16 | +--------------> MMCore version (major.minor.patch) 17 | ``` 18 | 19 | (Note that the device interface version can change without a change to the 20 | MMCore version, although this is relatively uncommon. Also, we are leaving out 21 | the module interface version, which rarely changes.) 22 | 23 | The correspondence to MMCore and device interface versions is checked in 24 | `tests/test_mmcore.py`. 25 | 26 | Note that we can support multiple MMCore versions, possibly retroactively, by 27 | maintaining separate branches; this can ease transition when the device 28 | interface version changes. Such branches should be named `mmcore-x.y.z.w`. 29 | 30 | When upgrading the MMCore version (by bumping the mmCoreAndDevices submodule 31 | commit), the pymmcore version in `_version.py` should be updated in synchrony. 32 | The versioning for the python package is taken dynamically from that file 33 | in the `[tool.setuptools.dynamic]` table in `pyproject.toml`. 34 | 35 | ## Building Binary Wheels and Source Distributions 36 | 37 | The package can be built in a few ways: 38 | 39 | 1. Use [cibuildwheel](https://cibuildwheel.readthedocs.io/en/stable/). 40 | This is the method used by the GitHub Actions CI workflow (configuration 41 | is in `pyproject.toml`). You can [run it locally](https://cibuildwheel.readthedocs.io/en/stable/setup/#local) as well 42 | if you have Docker installed: 43 | 44 | ```sh 45 | pip install cibuildwheel 46 | # example 47 | cibuildwheel --platform macos 48 | ``` 49 | Or, to build a specific platform/python version: 50 | ```sh 51 | cibuildwheel --only cp310-macosx_x86_64 52 | ``` 53 | 54 | The wheels will be placed in the `wheelhouse` directory. 55 | 56 | 2. Use the [build](https://pypi.org/project/build/) package 57 | 58 | ```sh 59 | pip install build 60 | python -m build 61 | ``` 62 | 63 | This will build an sdist and wheel for the current platform and Python 64 | version, and place them in the `dist` directory. 65 | 66 | 3. Use `pip install -e .` 67 | This will build the extension module in-place and allow you to run tests, 68 | but will not build a wheel or sdist. Note that if you do this, you will 69 | need to rerun it each time you change the extension module. 70 | 71 | 72 | 73 | ## Release procedure 74 | 75 | Prepare two commits, one removing `.dev0` from the version and a subsequent one 76 | bumping the patch version and re-adding `.dev0`. Tag the former with `v` 77 | prefixed to the version: 78 | 79 | ```bash 80 | git checkout main 81 | 82 | vim src/pymmcore/_version.py # Remove .dev0 83 | git commit -a -m 'Version 1.2.3.42.4' 84 | git tag -a v1.2.3.42.4 -m Release 85 | 86 | vim src/pymmcore/_version.py # Set version to 1.2.3.42.5.dev0 87 | git commit -a -m 'Version back to dev' 88 | 89 | git push upstream --follow-tags 90 | ``` 91 | 92 | This triggers a build in [the ci.yml workflow](.github/workflows/ci.yml) and 93 | the presence of a tag starting with "v" triggers a deployment to PyPI (using 94 | [trusted publisher](https://docs.pypi.org/trusted-publishers/) authentication.) 95 | 96 | Pushing the tag also creates a GitHub release with auto-generated release notes 97 | and the binary wheels attached. 98 | 99 | ## Dependency and tool versions 100 | 101 | - The minimum version of python supported is declared in `pypyproject.toml`, 102 | in the `[project.requires-python]` section. 103 | - SWIG 4.x is required and automatically fetched via `pyproject.toml` under 104 | `[build-system.requires]`. 105 | - The build-time versions of numpy are in `pyproject.toml`, in the 106 | `[build-system.requires]` section. 107 | - The run-time numpy dependency is declared in `pyproject.toml`, in the 108 | `[project.dependencies]` section. 109 | - Wheels are built with `cibuildwheel`, and the various wheel versions are 110 | determined by the settings in the `[tool.cibuildwheel]` section of 111 | `pyproject.toml`. 112 | - _We_ should provide wheels for all Python versions we claim to support, 113 | built agains the oldest NumPy version that we claim to support. Thus, any 114 | issue with the build or our CI will limit the lowest supported versions. 115 | 116 | ## ABI Compatibility 117 | 118 | - The Python platform and ABI compatibility is all handled by the Wheel system. 119 | (But see below re MSVC versions.) 120 | 121 | - NumPy ABI compatibility needs to be maintained by building against a 122 | reasonably old version of NumPy when packaging for public distribution (cf. 123 | https://github.com/numpy/numpy/issues/5888). We do this by including 124 | [`oldest-supported-numpy`](https://github.com/scipy/oldest-supported-numpy) 125 | in our build requires. 126 | 127 | ## Building with debug symbols on Windows 128 | 129 | Since there is no easy way to pass compile and linker options to `build_clib`, 130 | the easiest hack is to edit the local `setuptools` installation's 131 | `_distutils/_msvccompiler.py` to add the compiler flag `/Zi` and linker flag 132 | `/DEBUG:FULL` (see the method `initialize`). This produces `vc140.pdb`. 133 | 134 | (The "normal" method would be to run `setup.py build_clib` and `setup.py 135 | build_ext` with the `--debug` option, and run with `python_d.exe`. But then we 136 | would need a debug build of NumPy, which is hard to build on Windows.) 137 | 138 | 139 | ### Legacy Build Notes 140 | 141 | Many of these notes are probably obviated by the use of cibuildwheel... but 142 | are kept for reference. 143 | 144 |
145 | 146 | ### Windows 147 | 148 | - MSVC version. Python 3.5 and later are built with MSVC 14.x (i.e. Visual 149 | Studio 2015 to 2019). However, the official Python installer ships with its 150 | own copy of the VC runtime (in particular, `vcruntime140.dll`). This means 151 | that (in theory) our extension module must be built with an MSVC version that 152 | is not newer than the runtime shipped with Python. I say "in theory" because 153 | it is not clear if this actually results in problems, but let's play it safe. 154 | 155 | Python prints the MSVC version used to build itself when started. This 156 | version may change with the patch version of Python. Here are a few examples: 157 | 158 | - Python 3.8.1 (64-bit): MSC v.1916 = VS2017 159 | - Python 3.9.1 (64-bit): MSC v.1927 = VS2019 160 | - Python 3.8.7 (64-bit): MSC v.1928 = VS2019 161 | - Python 3.10.0 (64-bit): MSC v.1929 = VS2019 162 | - Python 3.11.0 (64-bit): MSC v.1933 = VS2022 163 | 164 | In general, it is probably safest to always build with VS2015 (older patch 165 | versions of Python 3.8 may be built with VS2015). This can be done by 166 | running `setup.py` inside the VS2015 Native Tools Command Prompt (this works 167 | because we use `setuptools`; with `distutils` extra environment variables are 168 | needed). 169 | 170 | It should also be noted that some Python package wheels (e.g. SciPy) ship a 171 | copy of `msvcp140.dll` (the C++ runtime) and other "140" DLLs. If they are 172 | loaded first, the version is pinned. 173 | 174 | We might want to pay attention to all this if/when Micro-Manager starts 175 | shipping with device adapters built with newer MSVC versions in the future. 176 | 177 | - Should we ship `msvcp140.dll` as part of the wheel? Given how the Python.org 178 | Windows installers are designed for non-admin installation, we technically 179 | should. 180 | 181 | ### macOS 182 | 183 | - `MACOSX_DEPLOYMENT_TARGET` should be set to match the Python.org Python we 184 | are building for, as much as reasonably possible. Currently, `10.9` is the 185 | best value for Python 3.5-3.10. 186 | - Our extension will still work if our deployment target is newer than 187 | Python's, so long as it is not newer than the host macOS version. 188 | - In the not-so-likely event that our extension uses symbols only available in 189 | macOS SDKs newer than the deployment target, those symbols will appear as 190 | 'weak' in `nm -mu`. 191 | - Not all weak symbols are a problem. There will always be a few from the C++ 192 | standard library that are harmless. 193 | - The built extension should be checked for undefined symbols (`nm -mu`) that 194 | are "dynamically looked up", other than those starting with `_Py` or `__Py`. 195 | There should be none if the build is correct. 196 | 197 | ### Linux 198 | 199 | - The manylinux docker images appear to solve all our problems. 200 | 201 | 202 | ### Resources 203 | 204 | - [Windows Compilers](https://wiki.python.org/moin/WindowsCompilers) on Python Wiki 205 | - [MacPython: Spinning wheels](https://github.com/MacPython/wiki/wiki/Spinning-wheels) (macOS ABI) 206 | - [manylinux](https://github.com/pypa/manylinux) Docker images; [PEP 207 | 513](https://python.org/dev/peps/pep-0513), 208 | [571](https://python.org/dev/peps/pep-0571), and 209 | [599](https://python.org/dev/peps/pep-0599) 210 | 211 | - Windows [DLL search order](https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order) 212 | - Unmaintained Apple [tech 213 | note](https://developer.apple.com/library/archive/technotes/tn2064/_index.html) 214 | describing `MACOSX_DEPLOYMENT_TARGET` 215 | 216 | 217 |
218 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # https://peps.python.org/pep-0517/ 2 | [build-system] 3 | requires = ["setuptools ==72.1.0", "swig ==4.2.1", "numpy>=2.0.0"] 4 | build-backend = "setuptools.build_meta" 5 | 6 | # https://peps.python.org/pep-0621/ 7 | [project] 8 | name = "pymmcore" 9 | description = "Python bindings for MMCore, Micro-Manager's device control layer" 10 | dynamic = ["version"] 11 | readme = "README.md" 12 | requires-python = ">=3.9" 13 | license = { text = "LGPL-2.1-only" } 14 | authors = [{ name = "Micro-Manager Team" }] 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Intended Audience :: Science/Research", 18 | "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)", 19 | "Operating System :: MacOS :: MacOS X", 20 | "Operating System :: Microsoft :: Windows", 21 | "Operating System :: POSIX :: Linux", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: Implementation :: CPython", 24 | "Topic :: Scientific/Engineering", 25 | "Topic :: System :: Hardware :: Hardware Drivers", 26 | "Typing :: Typed", 27 | ] 28 | dependencies = ["numpy>=1.25"] 29 | 30 | [project.optional-dependencies] 31 | test = ["pytest"] 32 | 33 | [project.urls] 34 | homepage = "https://micro-manager.org" 35 | repository = "https://github.com/micro-manager/pymmcore" 36 | 37 | [tool.setuptools.dynamic] 38 | version = { attr = "pymmcore._version.__version__" } 39 | 40 | [tool.setuptools.package-dir] 41 | "" = "src" 42 | 43 | [tool.setuptools.package-data] 44 | "*" = ["py.typed", ".pyi"] 45 | 46 | 47 | [tool.cibuildwheel] 48 | # Skip 32-bit builds, musllinux, and PyPy wheels on all platforms 49 | # Note: use of PTHREAD_MUTEX_RECURSIVE_NP in DeviceThreads.h 50 | # is specific to glibc and not available in musl-libc 51 | skip = ["*-manylinux_i686", "*-musllinux*", "*-win32", "pp*"] 52 | build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*"] 53 | test-requires = "pytest" 54 | test-command = 'pytest "{project}/tests" -v' 55 | test-skip = "*-macosx_arm64" 56 | 57 | [tool.cibuildwheel.macos] 58 | # https://cibuildwheel.readthedocs.io/en/stable/faq/#apple-silicon 59 | archs = ["x86_64", "arm64"] 60 | # Needed for C++17 support on macOS 61 | environment = { MACOSX_DEPLOYMENT_TARGET = "10.14" } 62 | 63 | [tool.check-manifest] 64 | ignore = [".editorconfig", "Dockerfile", "maintainer-notes.md", ".gitmodules"] 65 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Setup for pymmcore 2 | # 3 | # Copyright (C) 2020-2021 Board of Regents of the University of Wisconsin 4 | # System 5 | # 6 | # This library is free software; you can redistribute it and/or modify it under 7 | # the terms of the GNU Lesser General Public License, version 2.1, as published 8 | # by the Free Software Foundation. 9 | # 10 | # This library is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with this library; if not, write to the Free Software Foundation, Inc., 17 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 18 | # 19 | # Author: Mark A. Tsuchida 20 | 21 | import os 22 | import os.path 23 | import platform 24 | from pathlib import Path 25 | 26 | import numpy 27 | import setuptools.command.build_ext 28 | import setuptools.command.build_py 29 | from setuptools import Extension, setup 30 | 31 | PKG_NAME = "pymmcore" 32 | SWIG_MOD_NAME = "pymmcore_swig" 33 | IS_WINDOWS = platform.system() == "Windows" 34 | ROOT = Path(__file__).parent 35 | MMCorePath = ROOT / "mmCoreAndDevices" / "MMCore" 36 | MMDevicePath = ROOT / "mmCoreAndDevices" / "MMDevice" 37 | 38 | # We build MMCore from sources, into the Python extension. MMCore depends on 39 | # MMDevice. However, we need to build MMDevice separately from MMCore, because 40 | # it requires different preprocessor macros to be defined. For this purpose, we 41 | # make use of a rather obscure feature of distutils/setuptools called 42 | # build_clib. There may be other ways to do this.... 43 | 44 | 45 | # Customize 'build_py' to run 'build_ext' first; otherwise the SWIG-generated 46 | # .py file gets missed. 47 | class build_py(setuptools.command.build_py.build_py): 48 | def run(self): 49 | self.run_command("build_ext") 50 | super().run() 51 | 52 | 53 | # Customize 'build_ext' to trigger 'build_clib' first. 54 | class build_ext(setuptools.command.build_ext.build_ext): 55 | def run(self): 56 | self.run_command("build_clib") 57 | super().run() 58 | 59 | 60 | define_macros = [ 61 | ("MMDEVICE_CLIENT_BUILD", None), 62 | ] + ([ 63 | ("NOMINMAX", None), 64 | ("_CRT_SECURE_NO_WARNINGS", None), 65 | ] if IS_WINDOWS else []) 66 | 67 | mmdevice_build_info = { 68 | "sources": [str(x.relative_to(ROOT)) for x in MMDevicePath.glob("*.cpp")], 69 | "include_dirs": ["mmCoreAndDevices/MMDevice"], 70 | "macros": define_macros, 71 | } 72 | 73 | omit = ["unittest"] + (["Unix"] if IS_WINDOWS else ["Windows"]) 74 | mmcore_sources = [ 75 | str(x.relative_to(ROOT)) 76 | for x in MMCorePath.rglob("*.cpp") 77 | if all(o not in str(x) for o in omit) 78 | ] 79 | 80 | mmcore_libraries = ["MMDevice"] 81 | if not IS_WINDOWS: 82 | mmcore_libraries.extend(["dl"]) 83 | 84 | if not IS_WINDOWS: 85 | cflags = [ 86 | "-std=c++14", 87 | "-fvisibility=hidden", 88 | "-Wno-deprecated", # Hide warnings for throw() specififiers 89 | "-Wno-unused-variable", # Hide warnings for SWIG-generated code 90 | ] 91 | if "CFLAGS" in os.environ: 92 | cflags.append(os.environ["CFLAGS"]) 93 | os.environ["CFLAGS"] = " ".join(cflags) 94 | 95 | 96 | mmcore_extension = Extension( 97 | f"{PKG_NAME}._{SWIG_MOD_NAME}", 98 | sources=mmcore_sources + [os.path.join( 99 | "src", PKG_NAME, f"{SWIG_MOD_NAME}.i", 100 | )], 101 | swig_opts=[ 102 | "-c++", 103 | "-python", 104 | "-builtin", 105 | "-I./mmCoreAndDevices/MMDevice", 106 | "-I./mmCoreAndDevices/MMCore", 107 | ], 108 | include_dirs=[ 109 | numpy.get_include(), 110 | "./mmCoreAndDevices/MMDevice", 111 | "./mmCoreAndDevices/MMCore", 112 | ], 113 | libraries=mmcore_libraries, 114 | define_macros=define_macros, 115 | ) 116 | 117 | setup( 118 | ext_modules=[mmcore_extension], 119 | libraries=[("MMDevice", mmdevice_build_info)], 120 | cmdclass={"build_ext": build_ext, "build_py": build_py}, 121 | ) 122 | -------------------------------------------------------------------------------- /src/pymmcore/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | from .pymmcore_swig import * 3 | -------------------------------------------------------------------------------- /src/pymmcore/__init__.pyi: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | # NOTE: for now this file is manually created (well, a lot of find/replace with a decent 4 | # amount of checking). As such, it's conceivable that there are errors, so feel free 5 | # to submit PR fixes if you find one. 6 | # See https://github.com/micro-manager/pymmcore/pull/46 for discussion about fully 7 | # autogenerating "good" type hints. (along with a script that might help for anyone 8 | # so inclined) 9 | from __future__ import annotations 10 | from typing import Any, Final, List, Literal, NewType, overload, Sequence, Tuple, Union 11 | 12 | import numpy as np 13 | import numpy.typing as npt 14 | 15 | __version__: str 16 | 17 | MaxStrLength: int 18 | 19 | # ActionType 20 | NoAction: Final = 0 21 | BeforeGet: Final = 1 22 | AfterSet: Final = 2 23 | IsSequenceable: Final = 3 24 | AfterLoadSequence: Final = 4 25 | StartSequence: Final = 5 26 | StopSequence: Final = 6 27 | 28 | # DeviceType 29 | UnknownType: Final = 0 30 | AnyType: Final = 1 31 | CameraDevice: Final = 2 32 | ShutterDevice: Final = 3 33 | StateDevice: Final = 4 34 | StageDevice: Final = 5 35 | XYStageDevice: Final = 6 36 | SerialDevice: Final = 7 37 | GenericDevice: Final = 8 38 | AutoFocusDevice: Final = 9 39 | CoreDevice: Final = 10 40 | ImageProcessorDevice: Final = 11 41 | SignalIODevice: Final = 12 42 | MagnifierDevice: Final = 13 43 | SLMDevice: Final = 14 44 | HubDevice: Final = 15 45 | GalvoDevice: Final = 16 46 | PressurePumpDevice: Final = 17 47 | VolumetricPumpDevice: Final = 18 48 | 49 | # PropertyType 50 | Undef: Final = 0 51 | String: Final = 1 52 | Float: Final = 2 53 | Integer: Final = 3 54 | 55 | # PortType 56 | InvalidPort: Final = 0 57 | SerialPort: Final = 1 58 | USBPort: Final = 2 59 | HIDPort: Final = 3 60 | 61 | # FocusDirection 62 | FocusDirectionUnknown: Final = 0 63 | FocusDirectionTowardSample: Final = 1 64 | FocusDirectionAwayFromSample: Final = 2 65 | 66 | # DeviceNotification 67 | Attention: Final = 0 68 | Done: Final = 1 69 | StatusChanged: Final = 2 70 | 71 | # DeviceDetectionStatus 72 | Unimplemented: Final = -2 73 | Misconfigured: Final = -1 74 | CanNotCommunicate: Final = 0 75 | CanCommunicate: Final = 1 76 | 77 | # DeviceInitializationState 78 | Uninitialized: Final = 0 79 | InitializedSuccessfully: Final = 1 80 | InitializationFailed: Final = 2 81 | 82 | g_CFGCommand_ConfigGroup: Final[Literal["ConfigGroup"]] 83 | g_CFGCommand_ConfigPixelSize: Final[Literal["ConfigPixelSize"]] 84 | g_CFGCommand_Configuration: Final[Literal["Config"]] 85 | g_CFGCommand_Delay: Final[Literal["Delay"]] 86 | g_CFGCommand_Device: Final[Literal["Device"]] 87 | g_CFGCommand_Equipment: Final[Literal["Equipment"]] 88 | g_CFGCommand_FocusDirection: Final[Literal["FocusDirection"]] 89 | g_CFGCommand_ImageSynchro: Final[Literal["ImageSynchro"]] 90 | g_CFGCommand_Label: Final[Literal["Label"]] 91 | g_CFGCommand_ParentID: Final[Literal["Parent"]] 92 | g_CFGCommand_PixelSize_um: Final[Literal["PixelSize_um"]] 93 | g_CFGCommand_PixelSizeAffine: Final[Literal["PixelSizeAffine"]] 94 | g_CFGCommand_PixelSizedxdz: Final[Literal["PixelSizeAngle_dxdz"]] 95 | g_CFGCommand_PixelSizedydz: Final[Literal["PixelSizeAngle_dydz"]] 96 | g_CFGCommand_PixelSizeOptimalZUm: Final[Literal["PixelSizeOptimalZ_Um"]] 97 | g_CFGCommand_Property: Final[Literal["Property"]] 98 | 99 | g_CFGGroup_PixelSizeUm: Final[Literal["PixelSize_um"]] 100 | g_CFGGroup_System: Final[Literal["System"]] 101 | g_CFGGroup_System_Shutdown: Final[Literal["Shutdown"]] 102 | g_CFGGroup_System_Startup: Final[Literal["Startup"]] 103 | 104 | g_FieldDelimiters: Final[Literal[","]] 105 | 106 | g_Keyword_ActualExposure: Final[Literal["ActualExposure"]] 107 | g_Keyword_ActualInterval_ms: Final[Literal["ActualInterval-ms"]] 108 | g_Keyword_AnswerTimeout: Final[Literal["AnswerTimeout"]] 109 | g_Keyword_BaudRate: Final[Literal["BaudRate"]] 110 | g_Keyword_Binning: Final[Literal["Binning"]] 111 | g_Keyword_CameraChannelIndex: Final[Literal["CameraChannelIndex"]] 112 | g_Keyword_CameraChannelName: Final[Literal["CameraChannelName"]] 113 | g_Keyword_CameraID: Final[Literal["CameraID"]] 114 | g_Keyword_CameraName: Final[Literal["CameraName"]] 115 | g_Keyword_CCDTemperature: Final[Literal["CCDTemperature"]] 116 | g_Keyword_CCDTemperatureSetPoint: Final[Literal["CCDTemperatureSetPoint"]] 117 | g_Keyword_Channel: Final[Literal["Channel"]] 118 | g_Keyword_Closed_Position: Final[Literal["ClosedPosition"]] 119 | g_Keyword_ColorMode: Final[Literal["ColorMode"]] 120 | g_Keyword_CoreAutoFocus: Final[Literal["AutoFocus"]] 121 | g_Keyword_CoreAutoShutter: Final[Literal["AutoShutter"]] 122 | g_Keyword_CoreCamera: Final[Literal["Camera"]] 123 | g_Keyword_CoreChannelGroup: Final[Literal["ChannelGroup"]] 124 | g_Keyword_CoreDevice: Final[Literal["Core"]] 125 | g_Keyword_CoreFocus: Final[Literal["Focus"]] 126 | g_Keyword_CoreGalvo: Final[Literal["Galvo"]] 127 | g_Keyword_CoreImageProcessor: Final[Literal["ImageProcessor"]] 128 | g_Keyword_CoreInitialize: Final[Literal["Initialize"]] 129 | g_Keyword_CorePressurePump: Final[Literal["PressurePump"]] 130 | g_Keyword_CoreShutter: Final[Literal["Shutter"]] 131 | g_Keyword_CoreSLM: Final[Literal["SLM"]] 132 | g_Keyword_CoreTimeoutMs: Final[Literal["TimeoutMs"]] 133 | g_Keyword_CoreVolumetricPump: Final[Literal["VolumetricPump"]] 134 | g_Keyword_CoreXYStage: Final[Literal["XYStage"]] 135 | g_Keyword_Current_Volume: Final[Literal["Volume_uL"]] 136 | g_Keyword_DataBits: Final[Literal["DataBits"]] 137 | g_Keyword_Delay: Final[Literal["Delay_ms"]] 138 | g_Keyword_DelayBetweenCharsMs: Final[Literal["DelayBetweenCharsMs"]] 139 | g_Keyword_Description: Final[Literal["Description"]] 140 | g_Keyword_Elapsed_Time_ms: Final[Literal["ElapsedTime-ms"]] 141 | g_Keyword_EMGain: Final[Literal["EMGain"]] 142 | g_Keyword_Exposure: Final[Literal["Exposure"]] 143 | g_Keyword_Flowrate: Final[Literal["Flowrate_uL_per_sec"]] 144 | g_Keyword_Gain: Final[Literal["Gain"]] 145 | g_Keyword_Handshaking: Final[Literal["Handshaking"]] 146 | g_Keyword_HubID: Final[Literal["HubID"]] 147 | g_Keyword_Interval_ms: Final[Literal["Interval-ms"]] 148 | g_Keyword_Label: Final[Literal["Label"]] 149 | g_Keyword_Max_Volume: Final[Literal["Max_Volume_uL"]] 150 | g_Keyword_Meatdata_Exposure: Final[Literal["Exposure-ms"]] 151 | g_Keyword_Metadata_CameraLabel: Final[Literal["Camera"]] 152 | g_Keyword_Metadata_Exposure: Final[Literal["Exposure-ms"]] 153 | g_Keyword_Metadata_Height: Final[Literal["Height"]] 154 | g_Keyword_Metadata_ImageNumber: Final[Literal["ImageNumber"]] 155 | g_Keyword_Metadata_ROI_X: Final[Literal["ROI-X-start"]] 156 | g_Keyword_Metadata_ROI_Y: Final[Literal["ROI-Y-start"]] 157 | g_Keyword_Metadata_Score: Final[Literal["Score"]] 158 | g_Keyword_Metadata_TimeInCore: Final[Literal["TimeReceivedByCore"]] 159 | g_Keyword_Metadata_Width: Final[Literal["Width"]] 160 | g_Keyword_Min_Volume: Final[Literal["Min_Volume_uL"]] 161 | g_Keyword_Name: Final[Literal["Name"]] 162 | g_Keyword_Offset: Final[Literal["Offset"]] 163 | g_Keyword_Parity: Final[Literal["Parity"]] 164 | g_Keyword_PixelType_GRAY16: Final[Literal["GRAY16"]] 165 | g_Keyword_PixelType_GRAY32: Final[Literal["GRAY32"]] 166 | g_Keyword_PixelType_GRAY8: Final[Literal["GRAY8"]] 167 | g_Keyword_PixelType_RGB32: Final[Literal["RGB32"]] 168 | g_Keyword_PixelType_RGB64: Final[Literal["RGB64"]] 169 | g_Keyword_PixelType_Unknown: Final[Literal["Unknown"]] 170 | g_Keyword_PixelType: Final[Literal["PixelType"]] 171 | g_Keyword_Port: Final[Literal["Port"]] 172 | g_Keyword_Position: Final[Literal["Position"]] 173 | g_Keyword_Pressure_Imposed: Final[Literal["Pressure Imposed"]] 174 | g_Keyword_Pressure_Measured: Final[Literal["Pressure Measured"]] 175 | g_Keyword_ReadoutMode: Final[Literal["ReadoutMode"]] 176 | g_Keyword_ReadoutTime: Final[Literal["ReadoutTime"]] 177 | g_Keyword_Speed: Final[Literal["Speed"]] 178 | g_Keyword_State: Final[Literal["State"]] 179 | g_Keyword_StopBits: Final[Literal["StopBits"]] 180 | g_Keyword_Transpose_Correction: Final[Literal["TransposeCorrection"]] 181 | g_Keyword_Transpose_MirrorX: Final[Literal["TransposeMirrorX"]] 182 | g_Keyword_Transpose_MirrorY: Final[Literal["TransposeMirrorY"]] 183 | g_Keyword_Transpose_SwapXY: Final[Literal["TransposeXY"]] 184 | g_Keyword_Type: Final[Literal["Type"]] 185 | g_Keyword_Version: Final[Literal["Version"]] 186 | 187 | DEVICE_BUFFER_OVERFLOW: int 188 | DEVICE_CAMERA_BUSY_ACQUIRING: int 189 | DEVICE_CAN_NOT_SET_PROPERTY: int 190 | DEVICE_COMM_HUB_MISSING: int 191 | DEVICE_CORE_CHANNEL_PRESETS_FAILED: int 192 | DEVICE_CORE_CONFIG_FAILED: int 193 | DEVICE_CORE_EXPOSURE_FAILED: int 194 | DEVICE_CORE_FOCUS_STAGE_UNDEF: int 195 | DEVICE_DUPLICATE_LABEL: int 196 | DEVICE_DUPLICATE_LIBRARY: int 197 | DEVICE_DUPLICATE_PROPERTY: int 198 | DEVICE_ERR: int 199 | DEVICE_IMAGE_PARAMS_FAILED: int 200 | DEVICE_INCOMPATIBLE_IMAGE: int 201 | DEVICE_INTERNAL_INCONSISTENCY: int 202 | DEVICE_INVALID_INPUT_PARAM: int 203 | DEVICE_INVALID_PROPERTY_LIMTS: int 204 | DEVICE_INVALID_PROPERTY_TYPE: int 205 | DEVICE_INVALID_PROPERTY_VALUE: int 206 | DEVICE_INVALID_PROPERTY: int 207 | DEVICE_LOCALLY_DEFINED_ERROR: int 208 | DEVICE_NATIVE_MODULE_FAILED: int 209 | DEVICE_NO_CALLBACK_REGISTERED: int 210 | DEVICE_NO_PROPERTY_DATA: int 211 | DEVICE_NONEXISTENT_CHANNEL: int 212 | DEVICE_NOT_CONNECTED: int 213 | DEVICE_NOT_SUPPORTED: int 214 | DEVICE_NOT_YET_IMPLEMENTED: int 215 | DEVICE_OK: int 216 | DEVICE_OUT_OF_MEMORY: int 217 | DEVICE_PROPERTY_NOT_SEQUENCEABLE: int 218 | DEVICE_PUMP_IS_RUNNING: int 219 | DEVICE_SELF_REFERENCE: int 220 | DEVICE_SEQUENCE_TOO_LARGE: int 221 | DEVICE_SERIAL_BUFFER_OVERRUN: int 222 | DEVICE_SERIAL_COMMAND_FAILED: int 223 | DEVICE_SERIAL_INVALID_RESPONSE: int 224 | DEVICE_SERIAL_TIMEOUT: int 225 | DEVICE_SNAP_IMAGE_FAILED: int 226 | DEVICE_UNKNOWN_LABEL: int 227 | DEVICE_UNKNOWN_POSITION: int 228 | DEVICE_UNSUPPORTED_COMMAND: int 229 | DEVICE_UNSUPPORTED_DATA_FORMAT: int 230 | MM_CODE_ERR: int 231 | MM_CODE_OK: int 232 | 233 | def CMMCore_noop() -> None: ... 234 | def MetadataTag_ReadLine(jarg: Any) -> str: ... 235 | def PropertySetting_generateKey(device: str, prop: str) -> str: ... 236 | 237 | Rectangle = List[int] # it returns a list of 4 ints... would be nice if it were tuple 238 | DeviceType = int 239 | PropertyType = int 240 | FocusDirection = int 241 | DeviceDetectionStatus = int 242 | DeviceInitializationState = int 243 | AffineTuple = Tuple[float, float, float, float, float, float] 244 | 245 | # These are special string types used throughout the API. 246 | # We use NewType() to annotatr *return* values from core (that are guaranteed to be 247 | # valid inputs to other core functions with the same 248 | # type). However, to remain flexible, we use the fallback `NewType() | str` when 249 | # annotating function inputs. 250 | 251 | AdapterName = NewType("AdapterName", str) 252 | """Name of a device adapter library (discovered in the adapter search path).""" 253 | DeviceLabel = NewType("DeviceLabel", str) 254 | """User-defined label for a loaded device. 255 | Not to be confused with the `DeviceName`, which is defined by the device adapter. 256 | """ 257 | DeviceName = NewType("DeviceName", str) 258 | """Name of a Device offered by a device adapter (defined by the device adapter).""" 259 | PropertyName = NewType("PropertyName", str) 260 | """Name of a device property (defined by the device adapter).""" 261 | ConfigGroupName = NewType("ConfigGroupName", str) 262 | """User-defined name of a configuration group.""" 263 | ConfigPresetName = NewType("ConfigPresetName", str) 264 | """User-defined name of a preset in a configuration group.""" 265 | PixelSizeConfigName = NewType("PixelSizeConfigName", str) 266 | """User-defined name of a defined pixel size configuration preset""" 267 | StateLabel = NewType("StateLabel", str) 268 | """User-defined label for a specific state in a state device.""" 269 | 270 | FeatureFlag = Literal[ 271 | "StrictInitializationChecks", 272 | "ParallelDeviceInitialization", 273 | ] 274 | 275 | class CMMCore: 276 | def __init__(self) -> None: ... 277 | def addGalvoPolygonVertex( 278 | self, galvoLabel: str, polygonIndex: int, x: float, y: float 279 | ) -> None: 280 | """Add a vertex to a galvo polygon.""" 281 | def clearCircularBuffer(self) -> None: 282 | """Removes all images from the circular buffer.""" 283 | def clearROI(self) -> None: 284 | """Set the region of interest of the current camera to the full frame.""" 285 | def debugLogEnabled(self) -> bool: 286 | """Indicates if logging of debug messages is enabled""" 287 | @overload 288 | def defineConfig(self, groupName: str, configName: str) -> None: 289 | """Defines a configuration. 290 | 291 | If the configuration group/name was not previously defined a new configuration 292 | will be automatically created; otherwise nothing happens. 293 | """ 294 | @overload 295 | def defineConfig( 296 | self, 297 | groupName: str, 298 | configName: str, 299 | deviceLabel: DeviceLabel | str, 300 | propName: PropertyName | str, 301 | value: str, 302 | ) -> None: 303 | """Defines a single configuration entry (setting). 304 | 305 | If the configuration group/name was not previously defined a new configuration 306 | will be automatically created. If the name was previously defined the new 307 | setting will be added to its list of property settings. The new setting will 308 | override previously defined ones if it refers to the same property name. 309 | """ 310 | def defineConfigGroup(self, groupName: str) -> None: 311 | """Creates an empty configuration group.""" 312 | @overload 313 | def definePixelSizeConfig(self, resolutionID: str) -> None: 314 | """Defines an empty pixel size entry.""" 315 | @overload 316 | def definePixelSizeConfig( 317 | self, 318 | resolutionID: str, 319 | deviceLabel: DeviceLabel | str, 320 | propName: PropertyName | str, 321 | value: str, 322 | ) -> None: 323 | """Defines a single pixel size entry (setting). 324 | 325 | The system will treat pixel size configurations very similar to configuration 326 | presets, i.e. it will try to detect if any of the pixel size presets matches the 327 | current state of the system. 328 | 329 | If the pixel size was previously defined the new setting will be added to its 330 | list of property settings. The new setting will override previously defined ones 331 | if it refers to the same property name. 332 | """ 333 | def defineStateLabel( 334 | self, stateDeviceLabel: DeviceLabel | str, state: int, stateLabel: str 335 | ) -> None: 336 | """Defines a label for the specific state.""" 337 | @overload 338 | def deleteConfig( 339 | self, groupName: ConfigGroupName | str, configName: ConfigPresetName | str 340 | ) -> None: 341 | """Deletes a configuration from a group.""" 342 | @overload 343 | def deleteConfig( 344 | self, 345 | groupName: ConfigGroupName | str, 346 | configName: ConfigPresetName | str, 347 | deviceLabel: DeviceLabel | str, 348 | propName: PropertyName | str, 349 | ) -> None: ... 350 | def deleteConfigGroup(self, groupName: ConfigGroupName | str) -> None: 351 | """Deletes an entire configuration group.""" 352 | def deleteGalvoPolygons(self, galvoLabel: DeviceLabel | str) -> None: 353 | """Remove all added polygons""" 354 | def deletePixelSizeConfig(self, configName: PixelSizeConfigName | str) -> None: 355 | """Deletes a pixel size configuration.""" 356 | def detectDevice(self, deviceLabel: DeviceLabel | str) -> DeviceDetectionStatus: 357 | """Tries to communicate to a device through a given serial port Used to automate 358 | discovery of correct serial port. Also configures the serial port correctly.""" 359 | def deviceBusy(self, label: DeviceLabel | str) -> bool: 360 | """Checks the busy status of the specific device.""" 361 | def deviceTypeBusy(self, devType: DeviceType) -> bool: 362 | """Checks the busy status for all devices of the specific type.""" 363 | def displaySLMImage(self, slmLabel: DeviceLabel | str) -> None: 364 | """Display the waiting image on the SLM.""" 365 | def enableContinuousFocus(self, enable: bool) -> None: 366 | """Enables or disables the operation of the continuous focusing hardware device.""" 367 | def enableDebugLog(self, enable: bool) -> None: 368 | """Enable or disable logging of debug messages.""" 369 | # the Literal hint helps people know what the valid options are, but the fallback 370 | # to str makes it more future proof so that it's still valid to enter any string 371 | def enableFeature(self, name: FeatureFlag | str, enable: bool) -> None: 372 | """Enable or disable the given Core feature. 373 | 374 | Core features control whether experimental functionality (which is subject 375 | to breaking changes) is exposed, or whether stricter API usage is enforced. 376 | 377 | Currently switchable features: 378 | 379 | - "StrictInitializationChecks" (default: disabled) When enabled, an 380 | exception is thrown when an operation requiring an initialized device is 381 | attempted on a device that is not successfully initialized. When disabled, 382 | no exception is thrown and a warning is logged (and the operation may 383 | potentially cause incorrect behavior or a crash). 384 | - "ParallelDeviceInitialization" (default: enabled) When enabled, serial ports 385 | are initialized in serial order, and all other devices are in parallel, using 386 | multiple threads, one per device module. Early testing shows this to be 387 | reliable, but switch this off when issues are encountered during 388 | device initialization. 389 | """ 390 | def enableStderrLog(self, enable: bool) -> None: 391 | """Enables or disables log message display on the standard console.""" 392 | def fullFocus(self) -> None: 393 | """Performs focus acquisition and lock for the one-shot focusing device.""" 394 | def getAllowedPropertyValues( 395 | self, label: DeviceLabel | str, propName: PropertyName | str 396 | ) -> Tuple[str, ...]: 397 | """Returns all valid values for the specified property.""" 398 | def getAPIVersionInfo(self) -> str: 399 | """Returns the module and device interface versions.""" 400 | def getAutoFocusDevice(self) -> DeviceLabel | Literal[""]: 401 | """Returns the label of the currently selected auto-focus device. 402 | 403 | Returns empty string if no auto-focus device is selected. 404 | """ 405 | def getAutoFocusOffset(self) -> float: 406 | """Measures offset for the one-shot focusing device.""" 407 | def getAutoShutter(self) -> bool: 408 | """Returns the current setting of the auto-shutter option.""" 409 | def getAvailableConfigGroups(self) -> Tuple[ConfigGroupName, ...]: 410 | """Returns the names of all defined configuration groups""" 411 | def getAvailableConfigs( 412 | self, configGroup: ConfigGroupName | str 413 | ) -> Tuple[ConfigPresetName, ...]: 414 | """Returns all defined configuration (preset) names in a given group""" 415 | def getAvailableDeviceDescriptions( 416 | self, library: AdapterName | str 417 | ) -> Tuple[str, ...]: 418 | """Get descriptions for available devices from the specified library.""" 419 | def getAvailableDevices(self, library: AdapterName | str) -> Tuple[DeviceName, ...]: 420 | """Get available devices from the specified device library.""" 421 | def getAvailableDeviceTypes(self, library: AdapterName | str) -> Tuple[int, ...]: 422 | """Get type information for available devices from the specified library.""" 423 | def getAvailablePixelSizeConfigs(self) -> Tuple[PixelSizeConfigName, ...]: 424 | """Returns all defined resolution preset names""" 425 | def getBufferFreeCapacity(self) -> int: 426 | """Returns the number of images that can be added to the buffer without 427 | overflowing. 428 | """ 429 | def getBufferTotalCapacity(self) -> int: 430 | """Returns the total number of images that can be stored in the buffer""" 431 | def getBytesPerPixel(self) -> int: 432 | """How many bytes for each pixel.""" 433 | def getCameraChannelName(self, channelNr: int) -> str: 434 | """Returns the name of the requested channel as known by the default camera""" 435 | def getCameraDevice(self) -> DeviceLabel | Literal[""]: 436 | """Returns the label of the currently selected camera device. 437 | 438 | Returns empty string if no camera device is selected. 439 | """ 440 | def getChannelGroup(self) -> ConfigGroupName | Literal[""]: 441 | """Returns the group determining the channel selection. 442 | 443 | Returns empty string if no channel group is selected. 444 | """ 445 | def getCircularBufferMemoryFootprint(self) -> int: 446 | """Returns the size of the Circular Buffer in MB""" 447 | def getConfigData( 448 | self, configGroup: ConfigGroupName | str, configName: ConfigPresetName | str 449 | ) -> Configuration: 450 | """Returns the configuration object for a given group and name.""" 451 | def getConfigGroupState(self, group: ConfigGroupName | str) -> Configuration: 452 | """Returns the partial state of the system, only for the devices included in the 453 | specified group.""" 454 | def getConfigGroupStateFromCache( 455 | self, group: ConfigGroupName | str 456 | ) -> Configuration: 457 | """Returns the partial state of the system cache, only for the devices included in 458 | the specified group.""" 459 | def getConfigState( 460 | self, group: ConfigGroupName | str, config: ConfigPresetName | str 461 | ) -> Configuration: 462 | """Returns a partial state of the system, only for devices included in the 463 | specified configuration.""" 464 | def getCoreErrorText(self, code: int) -> str: 465 | """Returns a pre-defined error test with the given error code""" 466 | def getCurrentConfig( 467 | self, groupName: ConfigGroupName | str 468 | ) -> ConfigPresetName | Literal[""]: 469 | """Returns the current configuration (preset) for a given group. 470 | 471 | Returns empty string if no configuration is selected. 472 | """ 473 | def getCurrentConfigFromCache( 474 | self, groupName: ConfigGroupName | str 475 | ) -> ConfigPresetName | Literal[""]: 476 | """Returns the configuration for a given group based on the data in the cache.""" 477 | def getCurrentFocusScore(self) -> float: 478 | """Returns the focus score from the default focusing device measured at the 479 | current Z position.""" 480 | @overload 481 | def getCurrentPixelSizeConfig(self) -> PixelSizeConfigName: 482 | """Get the current pixel configuration name""" 483 | @overload 484 | def getCurrentPixelSizeConfig(self, cached: bool) -> PixelSizeConfigName: 485 | """Get the current pixel configuration name""" 486 | def getDeviceAdapterNames(self) -> Tuple[AdapterName, ...]: 487 | """Return the names of discoverable device adapters.""" 488 | def getDeviceAdapterSearchPaths(self) -> Tuple[str, ...]: 489 | """Return the current device adapter search paths.""" 490 | def getDeviceDelayMs(self, label: DeviceLabel | str) -> float: 491 | """Reports action delay in milliseconds for the specific device.""" 492 | def getDeviceDescription(self, label: DeviceLabel | str) -> str: 493 | """Returns description text for a given device label. "Description" is determined 494 | by the library and is immutable.""" 495 | def getDeviceLibrary(self, label: DeviceLabel | str) -> AdapterName: 496 | """Returns device library (aka module, device adapter) name.""" 497 | def getDeviceName(self, label: DeviceLabel | str) -> DeviceName: 498 | """Returns device name for a given device label.""" 499 | def getDevicePropertyNames( 500 | self, label: DeviceLabel | str 501 | ) -> Tuple[PropertyName, ...]: 502 | """Returns all property names supported by the device.""" 503 | def getDeviceType(self, label: DeviceLabel | str) -> DeviceType: 504 | """Returns device type.""" 505 | @overload 506 | def getExposure(self) -> float: 507 | """Returns the current exposure setting of the camera in milliseconds.""" 508 | @overload 509 | def getExposure(self, label: DeviceLabel | str) -> float: 510 | """Returns the current exposure setting of the specified camera in milliseconds.""" 511 | def getExposureSequenceMaxLength(self, cameraLabel: DeviceLabel | str) -> int: 512 | """Gets the maximum length of a camera's exposure sequence.""" 513 | def getFocusDevice(self) -> DeviceLabel | Literal[""]: 514 | """Returns the label of the currently selected focus device. 515 | 516 | Returns empty string if no focus device is selected. 517 | """ 518 | def getFocusDirection(self, stageLabel: DeviceLabel | str) -> FocusDirection: 519 | """Get the focus direction of a stage.""" 520 | def getGalvoChannel(self, galvoLabel: DeviceLabel | str) -> str: 521 | """Get the name of the active galvo channel (for a multi-laser galvo device).""" 522 | def getGalvoDevice(self) -> DeviceLabel | Literal[""]: 523 | """Returns the label of the currently selected Galvo device. 524 | 525 | Returns empty string if no Galvo device is selected. 526 | """ 527 | @overload 528 | def getGalvoPosition(self, galvoDevice: DeviceLabel | str) -> List[float]: 529 | """Get x,y position of the galvo device.""" 530 | @overload 531 | def getGalvoPosition( 532 | self, 533 | galvoLabel: DeviceLabel | str, 534 | x_stage: Sequence[float], 535 | y_stage: Sequence[float], 536 | ) -> None: ... 537 | def getGalvoXMinimum(self, galvoLabel: DeviceLabel | str) -> float: 538 | """Get the Galvo x minimum""" 539 | def getGalvoXRange(self, galvoLabel: DeviceLabel | str) -> float: 540 | """Get the Galvo x range""" 541 | def getGalvoYMinimum(self, galvoLabel: DeviceLabel | str) -> float: 542 | """Get the Galvo y minimum""" 543 | def getGalvoYRange(self, galvoLabel: DeviceLabel | str) -> float: 544 | """Get the Galvo y range""" 545 | @overload 546 | def getImage(self) -> np.ndarray: 547 | """Exposes the internal image buffer.""" 548 | @overload 549 | def getImage(self, numChannel: int) -> np.ndarray: 550 | """Returns the internal image buffer for a given Camera Channel""" 551 | def getImageBitDepth(self) -> int: 552 | """How many bits of dynamic range are to be expected from the camera.""" 553 | def getImageBufferSize(self) -> int: 554 | """Returns the size of the internal image buffer.""" 555 | def getImageHeight(self) -> int: 556 | """Vertical dimension of the image buffer in pixels.""" 557 | def getImageProcessorDevice(self) -> DeviceLabel | Literal[""]: 558 | """Returns the label of the currently selected image processor device. 559 | 560 | Returns empty string if no image processor device is selected. 561 | """ 562 | def getImageWidth(self) -> int: 563 | """Horizontal dimension of the image buffer in pixels.""" 564 | def getInstalledDeviceDescription( 565 | self, hubLabel: DeviceLabel | str, peripheralLabel: DeviceName | str 566 | ) -> str: 567 | """Returns description from the specified peripheral on `hubLabel` device.""" 568 | def getInstalledDevices( 569 | self, hubLabel: DeviceLabel | str 570 | ) -> Tuple[DeviceName, ...]: 571 | """Performs auto-detection and loading of child devices that are attached to a 572 | Hub device. 573 | 574 | Raises RuntimeError if hubLabel is not a hub device. 575 | """ 576 | def getLastFocusScore(self) -> float: 577 | """Returns the latest focus score from the focusing device.""" 578 | def getLastImage(self) -> np.ndarray: 579 | """Gets the last image from the circular buffer.""" 580 | @overload 581 | def getLastImageMD(self, channel: int, slice: int, md: Metadata) -> np.ndarray: ... 582 | @overload 583 | def getLastImageMD(self, md: Metadata) -> np.ndarray: 584 | """Returns a pointer to the pixels of the image that was last inserted into the 585 | circular buffer. Also provides all metadata associated with that image""" 586 | def getLoadedDevices(self) -> Tuple[DeviceLabel, ...]: 587 | """Returns an array of labels for currently loaded devices.""" 588 | def getLoadedDevicesOfType(self, devType: DeviceType) -> Tuple[DeviceLabel, ...]: 589 | """Returns an array of labels for currently loaded devices of specific type.""" 590 | def getLoadedPeripheralDevices( 591 | self, hubLabel: DeviceLabel | str 592 | ) -> Tuple[DeviceLabel, ...]: 593 | """Return labels of all loaded peripherals of `hubLabel` device. 594 | 595 | Returns empty tuple if hubLabel is not a hub device, or even if hubLabel is 596 | not the name of any device. 597 | """ 598 | def getMagnificationFactor(self) -> float: 599 | """Returns the product of all Magnifiers in the system or 1.0 when none is found. 600 | This is used internally by GetPixelSizeUm""" 601 | # def getMultiROI(self) -> List[Any]: ... # this overload doesn't seem to be present 602 | def getMultiROI( 603 | self, 604 | xs: Sequence[int], 605 | ys: Sequence[int], 606 | widths: Sequence[int], 607 | heights: Sequence[int], 608 | ) -> None: 609 | """Get multiple ROIs from the current camera device. 610 | 611 | Will fail if the camera does not support multiple ROIs. Will return empty 612 | vectors if multiple ROIs are not currently being used. 613 | """ 614 | def getNBeforeLastImageMD(self, n: int, md: Metadata) -> np.ndarray: 615 | """Returns a pointer to the pixels of the image that was inserted n images ago. 616 | Also provides all metadata associated with that image""" 617 | def getNumberOfCameraChannels(self) -> int: 618 | """Returns the number of simultaneous channels the default camera is returning.""" 619 | def getNumberOfComponents(self) -> int: 620 | """Returns the number of components the default camera is returning.""" 621 | def getNumberOfStates(self, stateDeviceLabel: DeviceLabel | str) -> int: 622 | """Returns the total number of available positions (states).""" 623 | def getParentLabel( 624 | self, peripheralLabel: DeviceLabel | str 625 | ) -> DeviceLabel | Literal[""]: 626 | """Returns parent device. Returns empty string if no parent is found.""" 627 | @overload 628 | def getPixelSizeAffine(self) -> AffineTuple: 629 | """Returns the current Affine Transform to related camera pixels with 630 | stage movement.""" 631 | @overload 632 | def getPixelSizeAffine(self, cached: bool) -> AffineTuple: 633 | """Returns the current Affine Transform to related camera pixels with 634 | stage movement.""" 635 | def getPixelSizeAffineByID( 636 | self, resolutionID: PixelSizeConfigName | str 637 | ) -> AffineTuple: 638 | """Returns the Affine Transform to related camera pixels with stage movement for 639 | the requested pixel size group. The raw affine transform without correction for 640 | binning and magnification will be returned.""" 641 | 642 | @overload 643 | def getPixelSizedxdz(self) -> float: ... 644 | @overload 645 | def getPixelSizedxdz(self, cached: bool) -> float: ... 646 | @overload 647 | def getPixelSizedxdz(self, resolutionID: PixelSizeConfigName | str) -> float: 648 | """Returns the angle between the camera's x axis and the axis (direction) of the z drive. 649 | 650 | This angle is dimensionless (i.e. the ratio of the translation in x caused by a 651 | translation in z, i.e. dx / dz). This angle can be different for different z 652 | drives (if there are multiple Z drives in the system, please add the Core-Focus 653 | device to the pixel size configuration). See: 654 | https://github.com/micro-manager/micro-manager/issues/1984 655 | 656 | """ 657 | @overload 658 | def getPixelSizedydz(self) -> float: ... 659 | @overload 660 | def getPixelSizedydz(self, cached: bool) -> float: ... 661 | @overload 662 | def getPixelSizedydz(self, resolutionID: PixelSizeConfigName | str) -> float: 663 | """Returns the angle between the camera's y axis and the axis (direction) of the z drive. 664 | 665 | This angle is dimensionless (i.e. the ratio of the translation in x caused by a 666 | translation in z, i.e. dy / dz). This angle can be different for different z 667 | drives (if there are multiple Z drives in the system, please add the Core-Focus 668 | device to the pixel size configuration). See: 669 | https://github.com/micro-manager/micro-manager/issues/1984 670 | 671 | """ 672 | @overload 673 | def getPixelSizeOptimalZUm(self) -> float: ... 674 | @overload 675 | def getPixelSizeOptimalZUm(self, cached: bool) -> float: ... 676 | @overload 677 | def getPixelSizeOptimalZUm(self, resolutionID: PixelSizeConfigName | str) -> float: 678 | """Returns the optimal z step size in um, optionally using cached pixel configuration. 679 | 680 | There is no magic to this number, but lets the system configuration 681 | communicate to the end user what the optimal Z step size is for this 682 | pixel size configuration 683 | """ 684 | def setPixelSizedxdz( 685 | self, resolutionID: PixelSizeConfigName | str, dXdZ: float 686 | ) -> None: 687 | """Sets the pixel size in the X direction in microns.""" 688 | def setPixelSizedydz( 689 | self, resolutionID: PixelSizeConfigName | str, dYdZ: float 690 | ) -> None: 691 | """Sets the pixel size in the Y direction in microns.""" 692 | def setPixelSizeOptimalZUm( 693 | self, resolutionID: PixelSizeConfigName | str, optimalZ: float 694 | ) -> None: 695 | """Sets the pixel size in the Z direction in microns.""" 696 | 697 | def getPixelSizeConfigData( 698 | self, configName: PixelSizeConfigName | str 699 | ) -> Configuration: 700 | """Returns the configuration object for a give pixel size preset.""" 701 | @overload 702 | def getPixelSizeUm(self) -> float: 703 | """Returns the current pixel size in microns.""" 704 | @overload 705 | def getPixelSizeUm(self, cached: bool) -> float: 706 | """Returns the current pixel size in microns.""" 707 | def getPixelSizeUmByID(self, resolutionID: PixelSizeConfigName | str) -> float: 708 | """Returns the pixel size in um for the requested pixel size group""" 709 | @overload 710 | def getPosition(self) -> float: 711 | """Returns the current position of the current FocusDevice in microns.""" 712 | @overload 713 | def getPosition(self, stageLabel: DeviceLabel | str) -> float: 714 | """Returns the current position of the stage in microns.""" 715 | def getPrimaryLogFile(self) -> str: 716 | """Return the name of the primary Core log file.""" 717 | def getProperty( 718 | self, label: DeviceLabel | str, propName: PropertyName | str 719 | ) -> str: 720 | """Returns the property value for the specified device. 721 | 722 | The return value will always be a string. Use getPropertyType to determine the 723 | correct type. 724 | """ 725 | def getPropertyFromCache( 726 | self, deviceLabel: DeviceLabel | str, propName: PropertyName | str 727 | ) -> str: 728 | """Returns the cached property value for the specified device.""" 729 | def getPropertyLowerLimit( 730 | self, label: DeviceLabel | str, propName: PropertyName | str 731 | ) -> float: 732 | """Returns the property lower limit value, if the property has limits - 0 733 | otherwise.""" 734 | def getPropertySequenceMaxLength( 735 | self, label: DeviceLabel | str, propName: PropertyName | str 736 | ) -> int: 737 | """Queries device property for the maximum number of events that can be put 738 | in a sequence""" 739 | def getPropertyType( 740 | self, label: DeviceLabel | str, propName: PropertyName | str 741 | ) -> PropertyType: 742 | """Returns the intrinsic property type.""" 743 | def getPropertyUpperLimit( 744 | self, label: DeviceLabel | str, propName: PropertyName | str 745 | ) -> float: 746 | """Returns the property upper limit value, if the property has limits - 0 747 | otherwise.""" 748 | def getRemainingImageCount(self) -> int: 749 | """Returns number ofimages available in the Circular Buffer""" 750 | @overload 751 | def getROI(self) -> Rectangle: 752 | """Return the current hardware region of interest for a camera. 753 | 754 | If multiple ROIs are set, this method instead returns a rectangle that describes 755 | the image that the camera will generate. The coordinates are in units of binned 756 | pixels. That is, conceptually, binning is applied before the ROI. 757 | 758 | Returns [0,0,0,0] if no camera is selected. 759 | """ 760 | @overload 761 | def getROI(self, label: DeviceLabel | str) -> Rectangle: 762 | """Return the current hardware region of interest for a specific camera. 763 | 764 | Raises RuntimeError if `label` is not a camera device or does not exist. 765 | """ 766 | # these overloads don't work for python 767 | # def getROI(self, x: int, y: int, xSize: int, ySize: int) -> None: ... 768 | # def getROI(self, label: str, x: int, y: int, xSize: int, ySize: int) -> None: ... 769 | def getSerialPortAnswer(self, portLabel: str, term: str) -> str: 770 | """Continuously read from the serial port until the terminating sequence is 771 | encountered.""" 772 | def getShutterDevice(self) -> DeviceLabel | Literal[""]: 773 | """Returns the label of the currently selected shutter device. 774 | 775 | Returns empty string if no shutter device is selected. 776 | """ 777 | @overload 778 | def getShutterOpen(self) -> bool: 779 | """Returns the state of the currently selected (default) shutter.""" 780 | @overload 781 | def getShutterOpen(self, shutterLabel: DeviceLabel | str) -> bool: 782 | """Returns the state of the specified shutter.""" 783 | def getSLMBytesPerPixel(self, slmLabel: DeviceLabel | str) -> int: 784 | """Returns the number of bytes per SLM pixel""" 785 | def getSLMDevice(self) -> DeviceLabel | Literal[""]: 786 | """Returns the label of the currently selected SLM device. 787 | 788 | Returns empty string if no SLM device is selected. 789 | """ 790 | def getSLMExposure(self, slmLabel: DeviceLabel | str) -> float: 791 | """Returns the exposure time that will be used by the SLM for illumination""" 792 | def getSLMHeight(self, slmLabel: DeviceLabel | str) -> int: 793 | """Returns the height (in "pixels") of the SLM""" 794 | def getSLMNumberOfComponents(self, slmLabel: DeviceLabel | str) -> int: 795 | """Returns the number of components (usually these depict colors) of the SLM. 796 | 797 | For instance, an RGB projector will return 3, but a grey scale SLM returns 1""" 798 | def getSLMSequenceMaxLength(self, slmLabel: DeviceLabel | str) -> int: 799 | """For SLMs that support sequences, returns the maximum length of the sequence 800 | that can be uploaded to the device""" 801 | def getSLMWidth(self, slmLabel: DeviceLabel | str) -> int: 802 | """Returns the width (in "pixels") of the SLM""" 803 | def getStageSequenceMaxLength(self, stageLabel: DeviceLabel | str) -> int: 804 | """Gets the maximum length of a stage's position sequence.""" 805 | def getState(self, stateDeviceLabel: DeviceLabel | str) -> int: 806 | """Returns the current state (position) on the specific device.""" 807 | def getStateFromLabel( 808 | self, stateDeviceLabel: DeviceLabel | str, stateLabel: StateLabel | str 809 | ) -> int: 810 | """Obtain the state for a given label.""" 811 | def getStateLabel(self, stateDeviceLabel: DeviceLabel | str) -> StateLabel: 812 | """Returns the current state as the label (string).""" 813 | def getStateLabels( 814 | self, stateDeviceLabel: DeviceLabel | str 815 | ) -> Tuple[StateLabel, ...]: 816 | """Return labels for all states""" 817 | def getSystemState(self) -> Configuration: 818 | """Returns the entire system state, i.e.""" 819 | def getSystemStateCache(self) -> Configuration: 820 | """Returns the entire system state, i.e.""" 821 | def getTimeoutMs(self) -> int: 822 | """Get the timeout for all wait commands. 823 | 824 | (Default is 5000 ms) 825 | """ 826 | def getVersionInfo(self) -> str: 827 | """Displays core version.""" 828 | @overload 829 | def getXPosition(self) -> float: 830 | """Obtains the current position of the X axis of the XY stage in microns.""" 831 | @overload 832 | def getXPosition(self, xyStageLabel: DeviceLabel | str) -> float: 833 | """Obtains the current position of the X axis of the XY stage in microns.""" 834 | @overload 835 | def getXYPosition(self) -> Sequence[float]: # always 2-element list, but not tuple 836 | """Obtains the current position of the XY stage in microns.""" 837 | @overload 838 | def getXYPosition(self, xyStageLabel: DeviceLabel | str) -> Sequence[float]: ... 839 | def getXYStageDevice(self) -> DeviceLabel | Literal[""]: 840 | """Returns the label of the currently selected XYStage device. 841 | 842 | Returns empty string if no XYStage device is selected. 843 | """ 844 | def getXYStageSequenceMaxLength(self, xyStageLabel: DeviceLabel | str) -> int: 845 | """Gets the maximum length of an XY stage's position sequence.""" 846 | @overload 847 | def getYPosition(self) -> float: 848 | """Obtains the current position of the Y axis of the XY stage in microns.""" 849 | @overload 850 | def getYPosition(self, xyStageLabel: DeviceLabel | str) -> float: 851 | """Obtains the current position of the Y axis of the XY stage in microns.""" 852 | def hasProperty( 853 | self, label: DeviceLabel | str, propName: PropertyName | str 854 | ) -> bool: 855 | """Checks if device has a property with a specified name.""" 856 | def hasPropertyLimits( 857 | self, label: DeviceLabel | str, propName: PropertyName | str 858 | ) -> bool: 859 | """Queries device if the specific property has limits.""" 860 | def home(self, xyOrZStageLabel: DeviceLabel | str) -> None: 861 | """Perform a hardware homing operation for an XY or focus/Z stage.""" 862 | def incrementalFocus(self) -> None: 863 | """Performs incremental focus for the one-shot focusing device.""" 864 | def initializeAllDevices(self) -> None: 865 | """Calls Initialize() method for each loaded device. 866 | 867 | See `ParallelDeviceInitialization` feature flag for controlling the order of 868 | initialization. 869 | """ 870 | def initializeCircularBuffer(self) -> None: 871 | """Initialize circular buffer based on the current camera settings.""" 872 | def initializeDevice(self, label: DeviceLabel | str) -> None: 873 | """Initializes specific device.""" 874 | def getDeviceInitializationState( 875 | self, label: DeviceLabel | str 876 | ) -> DeviceInitializationState: 877 | """Queries the initialization state of the given device.""" 878 | def isBufferOverflowed(self) -> bool: 879 | """Indicates whether the circular buffer is overflowed""" 880 | def isConfigDefined(self, groupName: str, configName: str) -> bool: 881 | """Checks if the configuration already exists within a group. 882 | 883 | If either the groupName or configName are not recognized, returns False. 884 | """ 885 | def isContinuousFocusDrive(self, stageLabel: DeviceLabel | str) -> bool: 886 | """Check if a stage has continuous focusing capability. 887 | 888 | (positions can be set while continuous focus runs).""" 889 | def isContinuousFocusEnabled(self) -> bool: 890 | """Checks if the continuous focusing hardware device is ON or OFF.""" 891 | def isContinuousFocusLocked(self) -> bool: 892 | """Returns the lock-in status of the continuous focusing device.""" 893 | def isExposureSequenceable(self, cameraLabel: DeviceLabel | str) -> bool: 894 | """Queries camera if exposure can be used in a sequence""" 895 | def isFeatureEnabled(self, name: str) -> bool: 896 | """Return whether the given Core feature is currently enabled. 897 | 898 | See `enableFeature()` for the available features. 899 | 900 | Raises RuntimeError if the feature name is not recognized. 901 | """ 902 | def isGroupDefined(self, groupName: str) -> bool: 903 | """Checks if the group already exists.""" 904 | def isMultiROIEnabled(self) -> bool: 905 | """Queries the camera to determine if multiple ROIs are currently set.""" 906 | def isMultiROISupported(self) -> bool: 907 | """Queries the camera to determine if it supports multiple ROIs.""" 908 | def isPixelSizeConfigDefined(self, resolutionID: str) -> bool: 909 | """Checks if the Pixel Size Resolution already exists""" 910 | def isPropertyPreInit( 911 | self, label: DeviceLabel | str, propName: PropertyName | str 912 | ) -> bool: 913 | """Tells us whether the property must be defined prior to initialization.""" 914 | def isPropertyReadOnly( 915 | self, label: DeviceLabel | str, propName: PropertyName | str 916 | ) -> bool: 917 | """Tells us whether the property can be modified.""" 918 | def isPropertySequenceable( 919 | self, label: DeviceLabel | str, propName: PropertyName | str 920 | ) -> bool: 921 | """Queries device if the specified property can be used in a sequence""" 922 | @overload 923 | def isSequenceRunning(self) -> bool: 924 | """Check if the current camera is acquiring the sequence. 925 | 926 | Returns false when the sequence is done""" 927 | @overload 928 | def isSequenceRunning(self, cameraLabel: DeviceLabel | str) -> bool: 929 | """Check if the specified camera is acquiring the sequence. 930 | 931 | Returns false when the sequence is done""" 932 | def isStageLinearSequenceable(self, stageLabel: DeviceLabel | str) -> bool: 933 | """Queries if the stage can be used in a linear sequence. 934 | 935 | A linear sequence is defined by a stepsize and number of slices""" 936 | def isStageSequenceable(self, stageLabel: DeviceLabel | str) -> bool: 937 | """Queries stage if it can be used in a sequence""" 938 | def isXYStageSequenceable(self, xyStageLabel: DeviceLabel | str) -> bool: 939 | """Queries XY stage if it can be used in a sequence""" 940 | def loadDevice( 941 | self, label: str, moduleName: AdapterName | str, deviceName: DeviceName | str 942 | ) -> None: 943 | """Loads a device from the plugin library.""" 944 | def loadExposureSequence( 945 | self, cameraLabel: DeviceLabel | str, exposureSequence_ms: Sequence[float] 946 | ) -> None: 947 | """Transfer a sequence of exposure times to the camera.""" 948 | def loadGalvoPolygons(self, galvoLabel: DeviceLabel | str) -> None: 949 | """Load a set of galvo polygons to the device""" 950 | def loadPropertySequence( 951 | self, 952 | label: DeviceLabel | str, 953 | propName: PropertyName | str, 954 | eventSequence: Sequence[str], 955 | ) -> None: 956 | """Transfer a sequence of events/states/whatever to the device. 957 | 958 | This should only be called for device-properties that are sequenceable 959 | """ 960 | def loadSLMSequence( 961 | self, slmLabel: DeviceLabel | str, imageSequence: List[bytes] 962 | ) -> None: 963 | """Load a sequence of images into the SLM""" 964 | def loadStageSequence( 965 | self, stageLabel: DeviceLabel | str, positionSequence: Sequence[float] 966 | ) -> None: 967 | """Transfer a sequence of events/states/whatever to the device. 968 | 969 | This should only be called for device-properties that are sequenceable""" 970 | def loadSystemConfiguration(self, fileName: str) -> None: 971 | """Loads the system configuration from the text file conforming to the 972 | MM specific format.""" 973 | def loadSystemState(self, fileName: str) -> None: 974 | """Loads the system configuration from the text file conforming to the 975 | MM specific format.""" 976 | def loadXYStageSequence( 977 | self, 978 | xyStageLabel: DeviceLabel | str, 979 | xSequence: Sequence[float], 980 | ySequence: Sequence[float], 981 | ) -> None: 982 | """Transfer a sequence of stage positions to the xy stage. 983 | 984 | xSequence and ySequence must have the same length. This should only be called 985 | for XY stages that are sequenceable 986 | """ 987 | @overload 988 | def logMessage(self, msg: str) -> None: 989 | """Record text message in the log file.""" 990 | @overload 991 | def logMessage(self, msg: str, debugOnly: bool) -> None: 992 | """Record text message in the log file.""" 993 | def noop(self) -> None: 994 | """A static method that does nothing.""" 995 | def pointGalvoAndFire( 996 | self, galvoLabel: DeviceLabel | str, x: float, y: float, pulseTime_us: float 997 | ) -> None: 998 | """Set the Galvo to an x,y position and fire the laser for a predetermined duration.""" 999 | def popNextImage(self) -> np.ndarray: 1000 | """Gets and removes the next image from the circular buffer.""" 1001 | @overload 1002 | def popNextImageMD(self, channel: int, slice: int, md: Metadata) -> np.ndarray: ... 1003 | @overload 1004 | def popNextImageMD(self, md: Metadata) -> np.ndarray: 1005 | """Gets and removes the next image (and metadata) from the circular buffer""" 1006 | def prepareSequenceAcquisition(self, cameraLabel: DeviceLabel | str) -> None: 1007 | """Prepare the camera for the sequence acquisition to save the time in the 1008 | 1009 | StartSequenceAcqusition() call which is supposed to come next.""" 1010 | def readFromSerialPort(self, portLabel: str) -> List[str]: # charvector 1011 | """Reads the contents of the Rx buffer.""" 1012 | def registerCallback(self, cb: MMEventCallback | None) -> None: 1013 | """Register a callback (listener class).""" 1014 | def renameConfig( 1015 | self, 1016 | groupName: ConfigGroupName | str, 1017 | oldConfigName: ConfigPresetName | str, 1018 | newConfigName: str, 1019 | ) -> None: 1020 | """Renames a configuration within a specified group. 1021 | 1022 | The command will fail if the configuration was not previously defined. 1023 | """ 1024 | def renameConfigGroup( 1025 | self, oldGroupName: ConfigGroupName | str, newGroupName: str 1026 | ) -> None: 1027 | """Renames a configuration group.""" 1028 | def renamePixelSizeConfig( 1029 | self, oldConfigName: PixelSizeConfigName | str, newConfigName: str 1030 | ) -> None: 1031 | """Renames a pixel size configuration.""" 1032 | def reset(self) -> None: 1033 | """Unloads all devices from the core, clears all configuration data and property 1034 | blocks.""" 1035 | def runGalvoPolygons(self, galvoLabel: DeviceLabel | str) -> None: 1036 | """Run a loop of galvo polygons""" 1037 | def runGalvoSequence(self, galvoLabel: DeviceLabel | str) -> None: 1038 | """Run a sequence of galvo positions""" 1039 | def saveSystemConfiguration(self, fileName: str) -> None: 1040 | """Saves the current system configuration to a text file of the MM specific format.""" 1041 | def saveSystemState(self, fileName: str) -> None: 1042 | """Saves the current system state to a text file of the MM specific format.""" 1043 | @overload 1044 | def setAdapterOrigin(self, newZUm: float) -> None: 1045 | """Enable software translation of coordinates for the current focus/Z stage.""" 1046 | @overload 1047 | def setAdapterOrigin(self, stageLabel: DeviceLabel | str, newZUm: float) -> None: 1048 | """Enable software translation of coordinates for the given focus/Z stage.""" 1049 | @overload 1050 | def setAdapterOriginXY(self, newXUm: float, newYUm: float) -> None: 1051 | """Enable software translation of coordinates for the current XY stage. 1052 | 1053 | The current position of the stage becomes (newXUm, newYUm). It is recommended 1054 | that setOriginXY() be used instead where available.""" 1055 | @overload 1056 | def setAdapterOriginXY( 1057 | self, xyStageLabel: DeviceLabel | str, newXUm: float, newYUm: float 1058 | ) -> None: ... 1059 | def setAutoFocusDevice(self, focusLabel: DeviceLabel | str) -> None: 1060 | """Sets the current auto-focus device.""" 1061 | def setAutoFocusOffset(self, offset: float) -> None: 1062 | """Applies offset the one-shot focusing device.""" 1063 | def setAutoShutter(self, state: bool) -> None: 1064 | """If this option is enabled Shutter automatically opens and closes when the 1065 | image is acquired.""" 1066 | def setCameraDevice(self, cameraLabel: DeviceLabel | str) -> None: 1067 | """Sets the current camera device.""" 1068 | def setChannelGroup(self, channelGroup: ConfigGroupName | str) -> None: 1069 | """Specifies the group determining the channel selection.""" 1070 | def setCircularBufferMemoryFootprint(self, sizeMB: int) -> None: 1071 | """Reserve memory for the circular buffer.""" 1072 | def setConfig( 1073 | self, groupName: ConfigGroupName | str, configName: ConfigPresetName | str 1074 | ) -> None: 1075 | """Applies a configuration to a group.""" 1076 | def setDeviceAdapterSearchPaths(self, paths: Sequence[str]) -> None: 1077 | """Set the device adapter search paths.""" 1078 | def setDeviceDelayMs(self, label: DeviceLabel | str, delayMs: float) -> None: 1079 | """Overrides the built-in value for the action delay.""" 1080 | @overload 1081 | def setExposure(self, exp: float) -> None: 1082 | """Sets the exposure setting of the current camera in milliseconds.""" 1083 | @overload 1084 | def setExposure(self, cameraLabel: DeviceLabel | str, dExp: float) -> None: 1085 | """Sets the exposure setting of the specified camera in milliseconds.""" 1086 | def setFocusDevice(self, focusLabel: DeviceLabel | str) -> None: 1087 | """Sets the current focus device.""" 1088 | def setFocusDirection(self, stageLabel: DeviceLabel | str, sign: int) -> None: 1089 | """Set the focus direction of a stage.""" 1090 | def setGalvoDevice(self, galvoLabel: DeviceLabel | str) -> None: 1091 | """Sets the current galvo device.""" 1092 | def setGalvoIlluminationState( 1093 | self, galvoLabel: DeviceLabel | str, on: bool 1094 | ) -> None: 1095 | """Set the galvo's illumination state to on or off""" 1096 | def setGalvoPolygonRepetitions( 1097 | self, galvoLabel: DeviceLabel | str, repetitions: int 1098 | ) -> None: 1099 | """Set the number of times to loop galvo polygons""" 1100 | def setGalvoPosition( 1101 | self, galvoLabel: DeviceLabel | str, x: float, y: float 1102 | ) -> None: 1103 | """Set the Galvo to an x,y position.""" 1104 | def setGalvoSpotInterval( 1105 | self, galvoLabel: DeviceLabel | str, pulseTime_us: float 1106 | ) -> None: 1107 | """Set the SpotInterval for the specified galvo device.""" 1108 | def setImageProcessorDevice(self, procLabel: DeviceLabel | str) -> None: 1109 | """Sets the current image processor device.""" 1110 | # this overload does not appear to be present 1111 | # @overload 1112 | # def setMultiROI(self, rects: List[Any]) -> None: ... 1113 | # @overload 1114 | def setMultiROI( 1115 | self, 1116 | xs: Sequence[int], 1117 | ys: Sequence[int], 1118 | widths: Sequence[int], 1119 | heights: Sequence[int], 1120 | ) -> None: 1121 | """Set multiple ROIs for the current camera device. 1122 | 1123 | Will fail if the camera does not support multiple ROIs, any widths or heights 1124 | are non-positive, or if the vectors do not all have the same length. 1125 | """ 1126 | @overload 1127 | def setOrigin(self) -> None: 1128 | """Zero the current focus/Z stage's coordinates at the current position.""" 1129 | @overload 1130 | def setOrigin(self, stageLabel: DeviceLabel | str) -> None: 1131 | """Zero the given focus/Z stage's coordinates at the current position.""" 1132 | @overload 1133 | def setOriginX(self) -> None: 1134 | """Zero the given XY stage's X coordinate at the current position.""" 1135 | @overload 1136 | def setOriginX(self, xyStageLabel: DeviceLabel | str) -> None: 1137 | """Zero the given XY stage's X coordinate at the current position.""" 1138 | @overload 1139 | def setOriginXY(self) -> None: 1140 | """Zero the current XY stage's coordinates at the current position.""" 1141 | @overload 1142 | def setOriginXY(self, xyStageLabel: DeviceLabel | str) -> None: 1143 | """Zero the given XY stage's coordinates at the current position.""" 1144 | @overload 1145 | def setOriginY(self) -> None: 1146 | """Zero the given XY stage's Y coordinate at the current position.""" 1147 | @overload 1148 | def setOriginY(self, xyStageLabel: DeviceLabel | str) -> None: 1149 | """Zero the given XY stage's Y coordinate at the current position.""" 1150 | def setParentLabel( 1151 | self, deviceLabel: DeviceLabel | str, parentHubLabel: DeviceLabel | str 1152 | ) -> None: 1153 | """Sets parent device label""" 1154 | def setPixelSizeAffine( 1155 | self, resolutionID: PixelSizeConfigName | str, affine: Sequence[float] 1156 | ) -> None: 1157 | """Sets the raw affine transform for the specific pixel size configuration. 1158 | 1159 | The affine transform consists of the first two rows of a 3x3 matrix, 1160 | the third row is alsways assumed to be 0.0 0.0 1.0.""" 1161 | def setPixelSizeConfig(self, resolutionID: PixelSizeConfigName | str) -> None: 1162 | """Applies a Pixel Size Configuration.""" 1163 | def setPixelSizeUm( 1164 | self, resolutionID: PixelSizeConfigName | str, pixSize: float 1165 | ) -> None: 1166 | """Sets pixel size in microns for the specified resolution sensing 1167 | configuration preset.""" 1168 | @overload 1169 | def setPosition(self, position: float) -> None: 1170 | """Sets the position of the current FocusDevice in microns.""" 1171 | @overload 1172 | def setPosition(self, stageLabel: DeviceLabel | str, position: float) -> None: 1173 | """Sets the position of the stage in microns.""" 1174 | @overload 1175 | def setPrimaryLogFile(self, filename: str) -> None: ... 1176 | @overload 1177 | def setPrimaryLogFile(self, filename: str, truncate: bool) -> None: 1178 | """Set the primary Core log file.""" 1179 | def setProperty( 1180 | self, 1181 | label: DeviceLabel | str, 1182 | propName: PropertyName | str, 1183 | propValue: Union[bool, float, int, str], 1184 | ) -> None: 1185 | """Changes the value of the device property.""" 1186 | @overload 1187 | def setRelativePosition(self, d: float) -> None: 1188 | """Sets the relative position of the stage in microns.""" 1189 | @overload 1190 | def setRelativePosition(self, stageLabel: DeviceLabel | str, d: float) -> None: 1191 | """Sets the relative position of the stage in microns.""" 1192 | @overload 1193 | def setRelativeXYPosition(self, dx: float, dy: float) -> None: 1194 | """Sets the relative position of the XY stage in microns.""" 1195 | @overload 1196 | def setRelativeXYPosition( 1197 | self, xyStageLabel: DeviceLabel | str, dx: float, dy: float 1198 | ) -> None: 1199 | """Sets the relative position of the XY stage in microns.""" 1200 | @overload 1201 | def setROI(self, x: int, y: int, xSize: int, ySize: int) -> None: 1202 | """Set the hardware region of interest for the current/specified camera. 1203 | 1204 | A successful call to this method will clear any images in the sequence buffer, 1205 | even if the ROI does not change. 1206 | 1207 | If multiple ROIs are set prior to this call, they will be replaced by the new 1208 | single ROI. 1209 | 1210 | The coordinates are in units of binned pixels. That is, conceptually, binning is 1211 | applied before the ROI. 1212 | """ 1213 | @overload 1214 | def setROI( 1215 | self, label: DeviceLabel | str, x: int, y: int, xSize: int, ySize: int 1216 | ) -> None: 1217 | """Set the hardware region of interest for the current camera.""" 1218 | def setSerialPortCommand(self, portLabel: str, command: str, term: str) -> None: 1219 | """Send string to the serial device and return an answer.""" 1220 | def setSerialProperties( 1221 | self, 1222 | portName: str, 1223 | answerTimeout: str, 1224 | baudRate: str, 1225 | delayBetweenCharsMs: str, 1226 | handshaking: str, 1227 | parity: str, 1228 | stopBits: str, 1229 | ) -> None: 1230 | """Sets all com port properties in a single call.""" 1231 | def setShutterDevice(self, shutterLabel: DeviceLabel | str) -> None: 1232 | """the current shutter device.""" 1233 | @overload 1234 | def setShutterOpen(self, state: bool) -> None: 1235 | """Opens or closes the currently selected (default) shutter.""" 1236 | @overload 1237 | def setShutterOpen(self, shutterLabel: DeviceLabel | str, state: bool) -> None: 1238 | """Opens or closes the specified shutter.""" 1239 | def setSLMDevice(self, slmLabel: DeviceLabel | str) -> None: 1240 | """Sets the current slm device.""" 1241 | def setSLMExposure(self, slmLabel: DeviceLabel | str, exposure_ms: float) -> None: 1242 | """For SLM devices with build-in light source (such as projectors), 1243 | this will set the exposure time, but not (yet) start the illumination""" 1244 | @overload 1245 | def setSLMImage( 1246 | self, slmLabel: DeviceLabel | str, pixels: npt.NDArray[np.uint8] 1247 | ) -> None: 1248 | """Write an image to the SLM . 1249 | 1250 | When passing a numpy array, `pixels` must be one of the following: 1251 | 1252 | - a 2D numpy array [h,w] of uint8s, representing a grayscale image to write 1253 | to the SLM. 1254 | - a 3D numpy array [h,w,3] of uint8s with 3 color channels [R,G,B], representing 1255 | an imgRGB32 image to write to the SLM. 1256 | 1257 | In both cases, the dimensions of the array should match the width and height 1258 | of the SLM. 1259 | 1260 | !!! warning 1261 | 1262 | SLM might convert grayscale to binary internally. 1263 | """ 1264 | @overload 1265 | def setSLMImage(self, slmLabel: DeviceLabel | str, pixels: Any) -> None: 1266 | """Write a list of chars to the SLM. 1267 | 1268 | Length of the list must match the number of pixels (or 4 * number of 1269 | pixels to write an imgRGB32.) 1270 | """ 1271 | @overload 1272 | def setSLMPixelsTo(self, slmLabel: DeviceLabel | str, intensity: int) -> None: 1273 | """Set all SLM pixels to a single 8-bit intensity.""" 1274 | @overload 1275 | def setSLMPixelsTo( 1276 | self, slmLabel: DeviceLabel | str, red: int, green: int, blue: int 1277 | ) -> None: 1278 | """Set all SLM pixels to an RGB color.""" 1279 | def setStageLinearSequence( 1280 | self, stageLabel: DeviceLabel | str, dZ_um: float, nSlices: int 1281 | ) -> None: 1282 | """Loads a linear sequence (defined by stepsize and nr. of steps) into the device.""" 1283 | def setState(self, stateDeviceLabel: DeviceLabel | str, state: int) -> None: 1284 | """Sets the state (position) on the specific device.""" 1285 | def setStateLabel( 1286 | self, stateDeviceLabel: DeviceLabel | str, stateLabel: StateLabel | str 1287 | ) -> None: 1288 | """Sets device state using the previously assigned label (string).""" 1289 | def setSystemState(self, conf: Configuration) -> None: 1290 | """Sets all properties contained in the Configuration object.""" 1291 | def setTimeoutMs(self, timeoutMs: int) -> None: 1292 | """Sets the timeout for all wait commands. 1293 | 1294 | (Default is 5000 ms) 1295 | """ 1296 | @overload 1297 | def setXYPosition(self, x: float, y: float) -> None: 1298 | """Sets the position of the XY stage in microns.""" 1299 | @overload 1300 | def setXYPosition( 1301 | self, xyStageLabel: DeviceLabel | str, x: float, y: float 1302 | ) -> None: ... 1303 | def setXYStageDevice(self, xyStageLabel: DeviceLabel | str) -> None: 1304 | """Sets the current XY device.""" 1305 | def sleep(self, intervalMs: float) -> None: 1306 | """Waits (blocks the calling thread) for specified time in milliseconds.""" 1307 | def snapImage(self) -> None: 1308 | """Acquires a single image with current settings.""" 1309 | def startContinuousSequenceAcquisition(self, intervalMs: float) -> None: 1310 | """Starts the continuous camera sequence acquisition.""" 1311 | def startExposureSequence(self, cameraLabel: DeviceLabel | str) -> None: 1312 | """Starts an ongoing sequence of triggered exposures in a camera. 1313 | 1314 | This should only be called for cameras where exposure time is sequenceable""" 1315 | def startPropertySequence( 1316 | self, label: DeviceLabel | str, propName: PropertyName | str 1317 | ) -> None: 1318 | """Starts an ongoing sequence of triggered events in a property of a device. 1319 | 1320 | This should only be called for device-properties that are sequenceable""" 1321 | @overload 1322 | def startSecondaryLogFile(self, filename: str, enableDebug: bool) -> int: 1323 | """Start capturing logging output into an additional file.""" 1324 | @overload 1325 | def startSecondaryLogFile( 1326 | self, filename: str, enableDebug: bool, truncate: bool 1327 | ) -> int: ... 1328 | @overload 1329 | def startSecondaryLogFile( 1330 | self, filename: str, enableDebug: bool, truncate: bool, synchronous: bool 1331 | ) -> int: ... 1332 | @overload 1333 | def startSequenceAcquisition( 1334 | self, numImages: int, intervalMs: float, stopOnOverflow: bool 1335 | ) -> None: ... 1336 | @overload 1337 | def startSequenceAcquisition( 1338 | self, 1339 | cameraLabel: DeviceLabel | str, 1340 | numImages: int, 1341 | intervalMs: float, 1342 | stopOnOverflow: bool, 1343 | ) -> None: ... 1344 | def startSLMSequence(self, slmLabel: DeviceLabel | str) -> None: 1345 | """Starts the sequence previously uploaded to the SLM""" 1346 | def startStageSequence(self, stageLabel: DeviceLabel | str) -> None: 1347 | """Starts an ongoing sequence of triggered events in a stage. 1348 | 1349 | This should only be called for stages""" 1350 | def startXYStageSequence(self, xyStageLabel: DeviceLabel | str) -> None: 1351 | """Starts an ongoing sequence of triggered events in an XY stage. 1352 | 1353 | This should only be called for stages""" 1354 | def stderrLogEnabled(self) -> bool: 1355 | """Indicates whether logging output goes to stdErr""" 1356 | def stop(self, xyOrZStageLabel: DeviceLabel | str) -> None: 1357 | """Stop the XY or focus/Z stage motors""" 1358 | def stopExposureSequence(self, cameraLabel: DeviceLabel | str) -> None: 1359 | """Stops an ongoing sequence of triggered exposures in a camera. 1360 | 1361 | This should only be called for cameras where exposure time is sequenceable""" 1362 | def stopPropertySequence( 1363 | self, label: DeviceLabel | str, propName: PropertyName | str 1364 | ) -> None: 1365 | """Stops an ongoing sequence of triggered events in a property of a device. 1366 | 1367 | This should only be called for device-properties that are sequenceable""" 1368 | def stopSecondaryLogFile(self, handle: int) -> None: 1369 | """Stop capturing logging output into an additional file.""" 1370 | @overload 1371 | def stopSequenceAcquisition(self) -> None: 1372 | """Stops streaming camera sequence acquisition.""" 1373 | @overload 1374 | def stopSequenceAcquisition(self, cameraLabel: DeviceLabel | str) -> None: 1375 | """Stops streaming camera sequence acquisition for a specified camera.""" 1376 | def stopSLMSequence(self, slmLabel: DeviceLabel | str) -> None: 1377 | """Stops the SLM sequence if previously started""" 1378 | def stopStageSequence(self, stageLabel: DeviceLabel | str) -> None: 1379 | """Stops an ongoing sequence of triggered events in a stage. 1380 | 1381 | This should only be called for stages that are sequenceable""" 1382 | def stopXYStageSequence(self, xyStageLabel: DeviceLabel | str) -> None: 1383 | """Stops an ongoing sequence of triggered events in an XY stage. 1384 | 1385 | This should only be called for stages that are sequenceable""" 1386 | def supportsDeviceDetection(self, deviceLabel: DeviceLabel | str) -> bool: 1387 | """Return whether or not the device supports automatic device detection (i.e.""" 1388 | def systemBusy(self) -> bool: 1389 | """Checks the busy status of the entire system.""" 1390 | def unloadAllDevices(self) -> None: 1391 | """Unloads all devices from the core and resets all configuration data.""" 1392 | def unloadDevice(self, label: DeviceLabel | str) -> None: 1393 | """Unloads the device from the core and adjusts all configuration data.""" 1394 | def unloadLibrary(self, moduleName: AdapterName | str) -> None: 1395 | """Forcefully unload a library.""" 1396 | def updateCoreProperties(self) -> None: 1397 | """Updates CoreProperties (currently all Core properties are devices types) with 1398 | the loaded hardware.""" 1399 | def updateSystemStateCache(self) -> None: 1400 | """Updates the state of the entire hardware.""" 1401 | def usesDeviceDelay(self, label: DeviceLabel | str) -> bool: 1402 | """Signals if the device will use the delay setting or not.""" 1403 | def waitForConfig( 1404 | self, group: ConfigGroupName | str, configName: ConfigPresetName | str 1405 | ) -> None: 1406 | """Blocks until all devices included in the configuration become ready.""" 1407 | def waitForDevice(self, label: DeviceLabel | str) -> None: 1408 | """Waits (blocks the calling thread) until the specified device becomes non-busy.""" 1409 | def waitForDeviceType(self, devType: DeviceType) -> None: 1410 | """Blocks until all devices of the specific type become ready (not-busy).""" 1411 | def waitForSystem(self) -> None: 1412 | """Blocks until all devices in the system become ready (not-busy).""" 1413 | def writeToSerialPort(self, portLabel: str, data: bytes) -> None: 1414 | """Sends an array of characters to the serial port and returns immediately.""" 1415 | 1416 | def pressurePumpStop(self, pumpLabel: str) -> None: 1417 | """Stops the pressure pump.""" 1418 | def pressurePumpCalibrate(self, pumpLabel: str) -> None: 1419 | """Calibrates the pressure pump.""" 1420 | def pressurePumpRequiresCalibration(self, pumpLabel: str) -> bool: 1421 | """Return True if pump requires calibration before operation.""" 1422 | def setPumpPressureKPa(self, pumpLabel: str, pressure: float) -> None: 1423 | """Sets the pressure of the pump in kPa.""" 1424 | def getPumpPressureKPa(self, pumpLabel: str) -> float: 1425 | """Return the pressure of the pump in kPa.""" 1426 | 1427 | def volumetricPumpStop(self, pumpLabel: str) -> None: 1428 | """Stops the volumetric pump.""" 1429 | def volumetricPumpHome(self, pumpLabel: str) -> None: 1430 | """Homes the volumetric pump.""" 1431 | def volumetricPumpRequiresHoming(self, pumpLabel: str) -> bool: 1432 | """Return True if the volumetric pump requires homing.""" 1433 | def invertPumpDirection(self, pumpLabel: str, invert: bool) -> None: 1434 | """Sets whether the pump direction needs to be inverted""" 1435 | def isPumpDirectionInverted(self, pumpLabel: str) -> bool: 1436 | """Return True if pump direction needs to be inverted""" 1437 | def setPumpVolume(self, pumpLabel: str, volume: float) -> None: 1438 | """Sets the volume of fluid in the pump in uL. 1439 | 1440 | Note it does not withdraw upto this amount. It is merely to inform MM 1441 | of the volume in a prefilled pump. 1442 | """ 1443 | def getPumpVolume(self, pumpLabel: str) -> float: 1444 | """Return the fluid volume in the pump in uL""" 1445 | def setPumpMaxVolume(self, pumpLabel: str, volume: float) -> None: 1446 | """Set the max volume of the pump in uL""" 1447 | def getPumpMaxVolume(self, pumpLabel: str) -> float: 1448 | """Return max volume of the pump in uL""" 1449 | def setPumpFlowrate(self, pumpLabel: str, volume: float) -> None: 1450 | """Set the flowrate of the pump in uL per second""" 1451 | def getPumpFlowrate(self, pumpLabel: str) -> float: 1452 | """Return the flowrate of the pump in uL per second""" 1453 | def pumpStart(self, pumpLabel: str) -> None: 1454 | """Start dispensing until syringe is empty, or manually stopped. 1455 | 1456 | (whichever occurs first). 1457 | """ 1458 | def pumpDispenseDurationSeconds(self, pumpLabel: str, seconds: float) -> None: 1459 | """Dispenses for the provided duration (in seconds) at the set flowrate.""" 1460 | def pumpDispenseVolumeUl(self, pumpLabel: str, microLiter: float) -> None: 1461 | """Dispenses the provided volume (in uL) at the set flowrate.""" 1462 | 1463 | # These are in MMCoreJ, not pymmcore 1464 | # def getTaggedImage(self) -> TaggedImage: ... 1465 | # def getTaggedImage(self, cameraChannelIndex: int) -> TaggedImage: ... 1466 | # def getNBeforeLastTaggedImage(self, n: int) -> TaggedImage: ... 1467 | # def getLastTaggedImage(self) -> TaggedImage: ... 1468 | # def getLastTaggedImage(self,cameraChannelIndex: int) -> TaggedImage: ... 1469 | # def popNextTaggedImage(self) -> TaggedImage: ... 1470 | # def popNextTaggedImage(self, cameraChannelIndex: int) -> TaggedImage: ... 1471 | # def getPixelSizeAffineAsString(self) -> str: 1472 | # """Convenience function.""" 1473 | # https://github.com/micro-manager/pymmcore/issues/65 1474 | # @overload 1475 | # def getXYStagePosition(self) -> List[float]: 1476 | # """Convenience function: returns the current XY position of the current 1477 | # XY stage device as a Point2D.Double.""" 1478 | # @overload 1479 | # def getXYStagePosition(self, stage: str) -> List[float]: ... 1480 | 1481 | ErrorCode = int 1482 | 1483 | class CMMError: 1484 | @overload 1485 | def __init__(self, msg: str, code: ErrorCode = 1): ... 1486 | @overload 1487 | def __init__(self, msg: str, code: ErrorCode, underlyingError: CMMError): ... 1488 | @overload 1489 | def __init__(self, msg: str, underlyingError: CMMError): ... 1490 | @overload 1491 | def __init__(self, other: CMMError): ... 1492 | def getCode(self) -> ErrorCode: ... 1493 | def getFullMsg(self) -> str: ... 1494 | def getMsg(self) -> str: ... 1495 | def getSpecificCode(self) -> ErrorCode: ... 1496 | def getUnderlyingError(self) -> CMMError: ... 1497 | def what(self) -> str: ... 1498 | 1499 | class Configuration: 1500 | """Encapsulation of the configuration information. 1501 | 1502 | Designed to be wrapped by SWIG. A collection of configuration settings.""" 1503 | 1504 | def __init__(self) -> None: ... 1505 | def addSetting(self, setting: PropertySetting) -> None: 1506 | """Adds new property setting to the existing contents.""" 1507 | def deleteSetting(self, device: str, prop: str) -> None: 1508 | """Removes property setting, specified by device and property names, from the configuration.""" 1509 | @overload 1510 | def getSetting(self, index: int) -> PropertySetting: 1511 | """Returns the setting with specified index.""" 1512 | @overload 1513 | def getSetting(self, device: str, prop: str) -> PropertySetting: 1514 | """Get the setting with specified device name and property name.""" 1515 | def getVerbose(self) -> str: 1516 | """Returns verbose description of the object's contents.""" 1517 | def isConfigurationIncluded(self, cfg: Configuration) -> bool: 1518 | """Checks whether a configuration is included. 1519 | 1520 | Included means that all devices from the operand configuration are included 1521 | and that settings match,""" 1522 | def isPropertyIncluded(self, device: str, prop: str) -> bool: 1523 | """Checks whether the property is included in the configuration.""" 1524 | def isSettingIncluded(self, ps: PropertySetting) -> bool: 1525 | """Checks whether the setting is included in the configuration.""" 1526 | def size(self) -> int: 1527 | """Returns the number of settings.""" 1528 | 1529 | class MetadataTag: 1530 | def Clone(self) -> MetadataTag: ... 1531 | def GetDevice(self) -> str: ... 1532 | def GetName(self) -> str: ... 1533 | def GetQualifiedName(self) -> str: ... 1534 | def IsReadOnly(self) -> bool: ... 1535 | def Restore(self, stream: str) -> bool: ... 1536 | def Serialize(self) -> str: ... 1537 | def SetDevice(self, device: str) -> None: ... 1538 | def SetName(self, name: str) -> None: ... 1539 | def SetReadOnly(self, ro: bool) -> None: ... 1540 | def ToArrayTag(self) -> MetadataArrayTag: ... 1541 | def ToSingleTag(self) -> MetadataSingleTag: ... 1542 | 1543 | class MetadataArrayTag(MetadataTag): 1544 | @overload 1545 | def __init__(self): ... 1546 | @overload 1547 | def __init__(self, name: str, device: str, readOnly: bool): ... 1548 | def AddValue(self, val: str): ... 1549 | def GetSize(self) -> int: ... 1550 | def GetValue(self) -> str: ... 1551 | def SetValue(self, val: str, idx: int) -> str: ... 1552 | 1553 | class MetadataSingleTag(MetadataTag): 1554 | @overload 1555 | def __init__(self): ... 1556 | @overload 1557 | def __init__(self, name: str, device: str, readOnly: bool): ... 1558 | def GetValue(self) -> str: ... 1559 | def SetValue(self, val: str) -> str: ... 1560 | 1561 | class Metadata: 1562 | def Clear(self) -> None: ... 1563 | def Dump(self) -> str: ... 1564 | def GetArrayTag(self, key: str) -> MetadataArrayTag: ... 1565 | def GetKeys(self) -> str: ... 1566 | def GetSingleTag(self, key: str) -> MetadataSingleTag: ... 1567 | def HasTag(self, key: str) -> bool: ... 1568 | def Merge(self, newTags: Metadata) -> None: ... 1569 | def RemoveTag(self, key: str) -> None: ... 1570 | def Restore(self, stream: str) -> bool: ... 1571 | def Serialize(self) -> str: ... 1572 | def SetTag(self, tag: MetadataTag) -> None: ... 1573 | def readLine(self, iss) -> str: ... 1574 | 1575 | class MetadataError: 1576 | def __init__(self, msg: str) -> None: ... 1577 | def getMsg(self) -> str: ... 1578 | 1579 | class MetadataKeyError(MetadataError): 1580 | def __init__(self) -> None: ... 1581 | 1582 | class MetadataIndexError(MetadataError): 1583 | def __init__(self) -> None: ... 1584 | 1585 | class MMEventCallback: 1586 | def __init__(self) -> None: ... 1587 | def onChannelGroupChanged(self, newChannelGroupName: str) -> None: ... 1588 | def onConfigGroupChanged(self, groupName: str, newConfigName: str) -> None: ... 1589 | def onExposureChanged(self, name: str, newExposure: float) -> None: ... 1590 | def onPixelSizeAffineChanged( 1591 | self, v0: float, v1: float, v2: float, v3: float, v4: float, v5: float 1592 | ) -> None: ... 1593 | def onPixelSizeChanged(self, newPixelSizeUm: float) -> None: ... 1594 | def onPropertiesChanged( 1595 | self, 1596 | ) -> None: ... 1597 | def onPropertyChanged(self, name: str, propName: str, propValue: str) -> None: ... 1598 | def onSLMExposureChanged(self, name: str, newExposure: float) -> None: ... 1599 | def onStagePositionChanged(self, name: str, pos: float) -> None: ... 1600 | def onSystemConfigurationLoaded( 1601 | self, 1602 | ) -> None: ... 1603 | def onXYStagePositionChanged(self, name: str, xpos: float, ypos: float) -> None: ... 1604 | 1605 | class PropertySetting: 1606 | """Property setting defined as triplet: device - property - value.""" 1607 | 1608 | @overload 1609 | def __init__(self) -> None: ... 1610 | @overload 1611 | def __init__(self, deviceLabel: str, prop: str, value: str) -> None: ... 1612 | @overload 1613 | def __init__( 1614 | self, deviceLabel: str, prop: str, value: str, readOnly: bool 1615 | ) -> None: ... 1616 | @staticmethod 1617 | def generateKey(self, device: str, prop: str) -> str: 1618 | """Returns `{device}-{prop}`.""" 1619 | def getDeviceLabel(self) -> DeviceLabel: 1620 | """Returns the device label.""" 1621 | def getKey(self) -> str: ... 1622 | def getPropertyName(self) -> PropertyName: 1623 | """Returns the property name.""" 1624 | def getPropertyValue(self) -> str: 1625 | """Returns the property value.""" 1626 | def getReadOnly(self) -> bool: 1627 | """Returns the read-only status.""" 1628 | def getVerbose(self) -> str: 1629 | """Returns verbose description of the object's contents.""" 1630 | def isEqualTo(self, ps: PropertySetting) -> bool: 1631 | """Returns true if the settings are equal.""" 1632 | def __eq__(self, other: Any) -> bool: 1633 | """prefer isEqualTo().""" 1634 | -------------------------------------------------------------------------------- /src/pymmcore/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "11.5.1.73.1.dev0" 2 | -------------------------------------------------------------------------------- /src/pymmcore/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micro-manager/pymmcore/4a221619359544704058080a1d9f0acea4daaa32/src/pymmcore/py.typed -------------------------------------------------------------------------------- /src/pymmcore/pymmcore_swig.i: -------------------------------------------------------------------------------- 1 | // SWIG interface file for MMCore Python bindings 2 | // 3 | // Copyright (C) 2006-2021 Regents of the University of California 4 | // (C) 2020-2021 Board of Regents of the University of Wisconsin 5 | // System 6 | // 7 | // This library is free software; you can redistribute it and/or modify it 8 | // under the terms of the GNU Lesser General Public License, version 2.1, as 9 | // published by the Free Software Foundation. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, 18 | // Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | // 20 | // Author: Arthur Edelstein, arthuredelstein@gmail.com, 2009.08.11, 21 | // based on the Java wrapper code by 22 | // Nenad Amodaj, nenad@amodaj.com, 06/07/2005, and 23 | // Micro-Manager team and contributors. 24 | // 25 | // History: This file used to be part of the micro-manager source tree; then 26 | // part of the mmCoreAndDevices source tree. It was moved to this 27 | // source tree (pymmcore) after mmCoreAndDevices commit 28 | // 5fbfe334730583fc5bd86af875f278f76f88b34d (2021-05-06). 29 | 30 | %module (package="pymmcore", directors="1", threads="1") pymmcore_swig 31 | 32 | %feature("director") MMEventCallback; 33 | %feature("autodoc", "3"); 34 | 35 | %include exception.i 36 | %include std_string.i 37 | %include std_vector.i 38 | %include std_map.i 39 | %include std_pair.i 40 | %include "typemaps.i" 41 | 42 | %{ 43 | #define SWIG_FILE_WITH_INIT 44 | %} 45 | 46 | %init %{ 47 | import_array(); 48 | %} 49 | 50 | %{ 51 | #define NPY_NO_DEPRECATED_API NPY_1_23_API_VERSION 52 | #include "numpy/arrayobject.h" 53 | #include "string.h" 54 | %} 55 | 56 | %typemap(out) void* 57 | { 58 | npy_intp dims[2]; 59 | dims[0] = (arg1)->getImageHeight(); 60 | dims[1] = (arg1)->getImageWidth(); 61 | npy_intp pixelCount = dims[0] * dims[1]; 62 | 63 | if ((arg1)->getBytesPerPixel() == 1) 64 | { 65 | PyObject * numpyArray = PyArray_SimpleNew(2, dims, NPY_UINT8); 66 | memcpy(PyArray_DATA((PyArrayObject *) numpyArray), result, pixelCount); 67 | $result = numpyArray; 68 | } 69 | else if ((arg1)->getBytesPerPixel() == 2) 70 | { 71 | PyObject * numpyArray = PyArray_SimpleNew(2, dims, NPY_UINT16); 72 | memcpy(PyArray_DATA((PyArrayObject *) numpyArray), result, pixelCount * 2); 73 | $result = numpyArray; 74 | } 75 | else if ((arg1)->getBytesPerPixel() == 4) 76 | { 77 | PyObject * numpyArray = PyArray_SimpleNew(2, dims, NPY_UINT32); 78 | memcpy(PyArray_DATA((PyArrayObject *) numpyArray), result, pixelCount * 4); 79 | $result = numpyArray; 80 | } 81 | else if ((arg1)->getBytesPerPixel() == 8) 82 | { 83 | PyObject * numpyArray = PyArray_SimpleNew(2, dims, NPY_UINT64); 84 | memcpy(PyArray_DATA((PyArrayObject *) numpyArray), result, pixelCount * 8); 85 | $result = numpyArray; 86 | } 87 | else 88 | { 89 | // don't know how to map 90 | // TODO: thow exception? 91 | // XXX Must do something, as returning NULL without setting error results 92 | // in an opaque error. 93 | $result = 0; 94 | } 95 | } 96 | 97 | 98 | %typemap(out) unsigned int* 99 | { 100 | //Here we assume we are getting RGBA (32 bits). 101 | npy_intp dims[3]; 102 | dims[0] = (arg1)->getImageHeight(); 103 | dims[1] = (arg1)->getImageWidth(); 104 | dims[2] = 3; // RGB 105 | unsigned numChannels = (arg1)->getNumberOfComponents(); 106 | unsigned char * pyBuf; 107 | unsigned char * coreBuf = (unsigned char *) result; 108 | 109 | if ((arg1)->getBytesPerPixel() == 4 && numChannels == 1) 110 | { 111 | 112 | // create new numpy array object 113 | PyObject * numpyArray = PyArray_SimpleNew(3, dims, NPY_UINT8); 114 | 115 | // get a pointer to the data buffer 116 | pyBuf = (unsigned char *) PyArray_DATA((PyArrayObject *) numpyArray); 117 | 118 | // copy R,G,B but leave out A in RGBA to return a WxHx3-dimensional array 119 | 120 | long pixelCount = dims[0] * dims[1]; 121 | 122 | for (long i=0; i { 148 | // check if is a list 149 | if(PyList_Check($input)) 150 | { 151 | long expectedLength = (arg1)->getSLMWidth(arg2) * (arg1)->getSLMHeight(arg2); 152 | 153 | Py_ssize_t size = PyList_Size($input); 154 | std::vector inputVector; 155 | 156 | for(Py_ssize_t i = 0; i < size; i++) 157 | { 158 | //printf("Pushing %d\n", i); 159 | PyObject * o = PyList_GetItem($input, i); 160 | if(PyString_Check(o)) 161 | { 162 | if (PyString_Size(o) != expectedLength) 163 | { 164 | PyErr_SetString(PyExc_TypeError, "One of the Image strings is the wrong length for this SLM."); 165 | return NULL; 166 | } 167 | 168 | inputVector.push_back((unsigned char *)PyString_AsString(o)); 169 | } 170 | else 171 | { 172 | PyErr_SetString(PyExc_TypeError, "list must contain strings"); 173 | return NULL; 174 | } 175 | } 176 | $1 = inputVector; 177 | } 178 | else 179 | { 180 | PyErr_SetString(PyExc_TypeError, "not a list"); 181 | return NULL; 182 | } 183 | } 184 | 185 | %rename(setSLMImage) setSLMImage_pywrap; 186 | %apply (PyObject *INPUT, int LENGTH) { (PyObject *pixels, int receivedLength) }; 187 | %apply (char *STRING, int LENGTH) { (char *pixels, int receivedLength) }; 188 | %extend CMMCore { 189 | // This is a wrapper for setSLMImage that accepts a list of chars 190 | void setSLMImage_pywrap(const char* slmLabel, char *pixels, int receivedLength) throw (CMMError) 191 | { 192 | // TODO This size check is done here (instead of in MMCore) because the 193 | // CMMCore::setSLMImage() interface is deficient: it does not include a 194 | // length parameter. It will be better to change the CMMCore functions to 195 | // require a length and move this check there. 196 | 197 | long expectedLength = self->getSLMWidth(slmLabel) * self->getSLMHeight(slmLabel); 198 | 199 | if (receivedLength == expectedLength) 200 | { 201 | self->setSLMImage(slmLabel, (unsigned char *)pixels); 202 | } 203 | else if (receivedLength == 4*expectedLength) 204 | { 205 | self->setSLMImage(slmLabel, (imgRGB32)pixels); 206 | } 207 | else 208 | { 209 | throw CMMError("Pixels must be a 2D numpy array [h,w] of uint8, or a 3D numpy array [h,w,c] of uint8 with 3 color channels [R,G,B]"); 210 | } 211 | } 212 | 213 | // This is a wrapper for setSLMImage that accepts a numpy array 214 | void setSLMImage_pywrap(const char* slmLabel, PyObject *pixels) throw (CMMError) 215 | { 216 | // Check if pixels is a numpy array 217 | if (!PyArray_Check(pixels)) { 218 | throw CMMError("Pixels must be a 2D numpy array [h,w] of uint8, or a 3D numpy array [h,w,c] of uint8 with 3 color channels [R,G,B]. Received a non-numpy array."); 219 | } 220 | 221 | // Get the dimensions of the numpy array 222 | PyArrayObject* np_pixels = reinterpret_cast(pixels); 223 | int nd = PyArray_NDIM(np_pixels); 224 | npy_intp* dims = PyArray_DIMS(np_pixels); 225 | 226 | // Check if the array has the correct shape 227 | long expectedWidth = self->getSLMWidth(slmLabel); 228 | long expectedHeight = self->getSLMHeight(slmLabel); 229 | 230 | if (dims[0] != expectedHeight || dims[1] != expectedWidth) { 231 | std::ostringstream oss; 232 | oss << "Image dimensions are wrong for this SLM. Expected (" << expectedHeight << ", " << expectedWidth << "), but received (" << dims[0] << ", " << dims[1] << ")"; 233 | throw CMMError(oss.str().c_str()); 234 | } 235 | 236 | if (PyArray_TYPE(np_pixels) != NPY_UINT8) { 237 | std::ostringstream oss; 238 | oss << "Pixel array type is wrong. Expected uint8."; 239 | throw CMMError(oss.str().c_str()); 240 | } 241 | 242 | npy_intp num_bytes = PyArray_NBYTES(np_pixels); 243 | long expectedBytes = expectedWidth * expectedHeight * self->getSLMBytesPerPixel(slmLabel); 244 | if (num_bytes > expectedBytes) { 245 | std::ostringstream oss; 246 | oss << "Number of bytes per pixel in pixels is greater than expected. Received: " << num_bytes/(dims[0] * dims[1]) << ", Expected: " << self->getSLMBytesPerPixel(slmLabel)<< ". Does this SLM support RGB?"; 247 | throw CMMError(oss.str().c_str()); 248 | } 249 | 250 | if (PyArray_TYPE(np_pixels) == NPY_UINT8 && nd == 2) { 251 | // For 2D 8-bit array, cast integers directly to unsigned char 252 | std::vector vec_pixels(expectedWidth * expectedHeight); 253 | for (npy_intp i = 0; i < expectedHeight; ++i) { 254 | for (npy_intp j = 0; j < expectedWidth; ++j) { 255 | vec_pixels[i * expectedWidth + j] = static_cast(*static_cast(PyArray_GETPTR2(np_pixels, i, j))); 256 | } 257 | } 258 | self->setSLMImage(slmLabel, vec_pixels.data()); 259 | 260 | } else if (PyArray_TYPE(np_pixels) == NPY_UINT8 && nd == 3 && dims[2] == 3) { 261 | // For 3D color array, convert to imgRGB32 and add a 4th byte for the alpha channel 262 | std::vector vec_pixels(expectedWidth * expectedHeight); // 1 imgRGB32 for RGBA 263 | for (npy_intp i = 0; i < expectedHeight; ++i) { 264 | for (npy_intp j = 0; j < expectedWidth; ++j) { 265 | unsigned int pixel = 0; 266 | for (npy_intp k = 0; k < 3; ++k) { 267 | uint8_t value = *static_cast(PyArray_GETPTR3(np_pixels, i, j, 2 - k)); // Reverse the order of RGB 268 | pixel |= static_cast(value) << (8 * k); 269 | } 270 | // Set the alpha channel to 0 271 | vec_pixels[i * expectedWidth + j] = pixel; 272 | } 273 | } 274 | self->setSLMImage(slmLabel, vec_pixels.data()); 275 | } else { 276 | throw CMMError("Pixels must be a 2D numpy array [h,w] of uint8, or a 3D numpy array [h,w,c] of uint8 with 3 color channels [R,G,B]"); 277 | } 278 | } 279 | } 280 | 281 | %ignore setSLMImage; 282 | 283 | %{ 284 | #define SWIG_FILE_WITH_INIT 285 | #include "MMDeviceConstants.h" 286 | #include "Error.h" 287 | #include "Configuration.h" 288 | #include "ImageMetadata.h" 289 | #include "MMEventCallback.h" 290 | #include "MMCore.h" 291 | %} 292 | 293 | // Exception handling. Tranditionally, MMCore uses exception specifications 294 | // (throw(CMMError)) to tell SWIG to generate exception handling code. This is 295 | // handled by the %typemap(throws) below. 296 | // However, C++ exception specifications are deprecated since C++11 and removed 297 | // in C++17. So in order to future-proof this interface, we also specify a 298 | // general exception handler using %exception. The latter applies to all 299 | // functions that do not have an exception specification (or, in the future, 300 | // noexcept). 301 | 302 | %{ 303 | static int cmmerror_swig_exception_code(const CMMError& e) { 304 | switch (e.getCode()) 305 | { 306 | case MMERR_BadAffineTransform: 307 | case MMERR_BadConfigName: 308 | case MMERR_DuplicateConfigGroup: 309 | case MMERR_DuplicateLabel: 310 | case MMERR_InvalidContents: 311 | case MMERR_InvalidCoreProperty: 312 | case MMERR_InvalidCoreValue: 313 | case MMERR_InvalidLabel: 314 | case MMERR_InvalidPropertyBlock: 315 | case MMERR_InvalidSerialDevice: 316 | case MMERR_InvalidShutterDevice: 317 | case MMERR_InvalidSpecificDevice: 318 | case MMERR_InvalidStageDevice: 319 | case MMERR_InvalidStateDevice: 320 | case MMERR_InvalidXYStageDevice: 321 | case MMERR_NoConfigGroup: 322 | case MMERR_NoConfiguration: 323 | case MMERR_NullPointerException: 324 | case MMERR_PropertyNotInCache: 325 | case MMERR_SetPropertyFailed: 326 | case MMERR_UnexpectedDevice: 327 | return SWIG_ValueError; 328 | case MMERR_FileOpenFailed: 329 | case MMERR_InvalidCFGEntry: 330 | case MMERR_InvalidConfigurationFile: 331 | case MMERR_LoadLibraryFailed: 332 | return SWIG_IOError; 333 | case MMERR_CircularBufferEmpty: 334 | case MMERR_InvalidConfigurationIndex: 335 | return SWIG_IndexError; 336 | case MMERR_CircularBufferFailedToInitialize: 337 | case MMERR_OutOfMemory: 338 | return SWIG_MemoryError; 339 | case MMERR_CameraBufferReadFailed: 340 | case MMERR_CircularBufferIncompatibleImage: 341 | case MMERR_UnhandledException: 342 | case MMERR_UnknownModule: 343 | case MMERR_OK: // Shouldn't get here with MMERR_OK 344 | default: 345 | return SWIG_RuntimeError; 346 | } 347 | } 348 | %} 349 | 350 | // Applies to functions with C++ exception specification (to be retired when 351 | // C++ exception specifications retired) 352 | %typemap(throws) CMMError %{ 353 | SWIG_exception(cmmerror_swig_exception_code($1), ($1).getMsg().c_str()); 354 | %} 355 | %typemap(throws) MetadataKeyError %{ 356 | SWIG_exception(SWIG_ValueError, ($1).getMsg().c_str()); 357 | %} 358 | %typemap(throws) MetadataIndexError %{ 359 | SWIG_exception(SWIG_IndexError, ($1).getMsg().c_str()); 360 | %} 361 | 362 | // Applies to functions without exception specification 363 | %exception { 364 | try { 365 | $action 366 | } 367 | catch (const CMMError& e) { 368 | SWIG_exception(cmmerror_swig_exception_code(e), e.getMsg().c_str()); 369 | } 370 | catch (MetadataKeyError& e) { 371 | SWIG_exception(SWIG_ValueError, e.getMsg().c_str()); 372 | } 373 | catch (MetadataIndexError& e) { 374 | SWIG_exception(SWIG_IndexError, e.getMsg().c_str()); 375 | } 376 | } 377 | 378 | 379 | // instantiate STL mappings 380 | namespace std { 381 | %template(CharVector) vector; 382 | %template(LongVector) vector; 383 | %template(UnsignedVector) vector; 384 | %template(DoubleVector) vector; 385 | %template(StrVector) vector; 386 | %template(pair_ss) pair; 387 | %template(StrMap) map; 388 | } 389 | 390 | // output arguments 391 | %apply double &OUTPUT { double &x_stage }; 392 | %apply double &OUTPUT { double &y_stage }; 393 | %apply int &OUTPUT { int &x }; 394 | %apply int &OUTPUT { int &y }; 395 | %apply int &OUTPUT { int &xSize }; 396 | %apply int &OUTPUT { int &ySize }; 397 | 398 | %include "MMDeviceConstants.h" 399 | %include "Error.h" 400 | %include "Configuration.h" 401 | %include "MMCore.h" 402 | %include "ImageMetadata.h" 403 | %include "MMEventCallback.h" 404 | -------------------------------------------------------------------------------- /tests/test_mmcore.py: -------------------------------------------------------------------------------- 1 | import pymmcore 2 | 3 | 4 | def test_core(): 5 | # __version__ will be something like '10.4.0.71.1.dev0' 6 | pymmcore_version = pymmcore.__version__.split(".") 7 | 8 | mmc = pymmcore.CMMCore() 9 | 10 | # something like 'MMCore version 10.4.0' 11 | version_info = mmc.getVersionInfo() 12 | assert pymmcore_version[:3] == version_info.split()[-1].split(".") 13 | 14 | # something like 'Device API version 71, Module API version 10' 15 | api_version_info = mmc.getAPIVersionInfo() 16 | dev_interface_version = api_version_info.split(",")[0].split()[-1] 17 | assert pymmcore_version[3] == dev_interface_version 18 | --------------------------------------------------------------------------------