diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 195255e..a0de6fb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -82,6 +82,8 @@ jobs: version-bumped: name: Version bumped runs-on: ubuntu-latest + # Every pull request: ordinary work on a -DEV base keeps the version, and + # a release-branch backport on a released base must raise it (versions.py). if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3371a34..b6a94c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,25 @@ the next version with `-DEV`). ## [Unreleased] +### Fixed +- **PI stage: `initialize` no longer finishes on an unreferenced stage.** It + ignored the return of the reference move (`PI_FRF`), so when the controller + rejected it (GCS error 5, e.g. one axis's servo off) the later move to the + centre was rejected too and the driver believed the stage was centred while + the controller read about 0. It now checks `PI_FRF`, then polls `PI_qFRF` + for up to 60 s until both axes report referenced; either failure throws + with the `PI_GetError` code and closes the connection first, so a retried + `initialize` starts clean. `referencemove`'s signature and return are + unchanged. Reported by the MicroscopeAdapt rig; not yet run on hardware + (#64). + +### Changed (release process) +- **`main` is the development branch**, carrying the next version with + `-DEV`; every pull request goes into it. The `0.3rc1` release-candidate + branch, which never released, is folded into `main` and retired, and CI's + version check runs on every pull request again. A release branch is cut only + for a safety backport (CLAUDE.md "Versioning"). + ## [0.2.4] - 2026-09-29 Safety patch for the TCube laser driver. **v0.2.3 and every earlier tag are diff --git a/CLAUDE.md b/CLAUDE.md index fe3ef39..31f4b85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -164,4 +164,25 @@ Some hardware modules are commented out in `MicroscopeControl.jl` while under de ### Versioning -This package is 0.x and not yet registered; install a pinned tag per the README's Installation Notes. Policy, following Julia's pre-1.0 convention: while the version is `0.x.y`, **`x` is the breaking component and `y` is the non-breaking one** -- `0.2.0 -> 0.3.0` declares a breaking release and `0.2.0 -> 0.2.1` a compatible one, which is also how Julia's `^0.2` compat bound reads them. So bump `x` only when working downstream code can behave differently (a signature, an export, or what a call returns or throws), and bump `y` for everything else, including bug fixes that change behaviour on a path that was already broken. `.github/workflows/TagOnMerge.yml` tags a merge to `main` only when its Project.toml version is a release `X.Y.Z` (a `-DEV` version is development and is skipped, lab decision 0033) and a passing `lab/tests` record covers that commit's tree (decision 0009); an untested tree is not tagged, and the job says why. Hardware verification is not tracked in this repo; it is recorded by the downstream rig repo that pins to a given tag. The merge gate is the local suite (see "Testing policy" above) plus `test/contract.jl`'s "Interface Contract" testset, which guards the no-ambiguous-exports and core-method invariants described above; CI confirms it on a reduced matrix. +This package is 0.x and not yet registered; install a pinned tag per the README's Installation Notes. Versions follow lab decision 0033. + +**`main` is the development branch.** Its `Project.toml` carries the next +version with `-DEV`: after `vX.Y.Z` is tagged, the next pull request sets +`X.Y.(Z+1)-DEV`. Every pull request goes into `main` and leaves that version +alone, unless it breaks an interface, in which case it raises it to +`X.(Y+1).0-DEV`. A release is one pull request that drops `-DEV` and writes +the `CHANGELOG.md` section; `.github/workflows/TagOnMerge.yml` tags its merge +`vX.Y.Z` when a passing `lab/tests` record covers that commit's tree +(decision 0009), and otherwise fails and says why. It skips every `-DEV` +merge. CI's "Version bumped" check runs on every pull request +(`.github/scripts/versions.py`): against a `-DEV` base the version may stay +or rise but never fall, and against a released base it must rise. + +A release branch exists only for a safety backport: when `main` has moved on +to a breaking version and a rig pinned to the previous series needs a fix, +cut `release-X.Y` from that series' last tag, merge the fix into it with a +pull request that raises `Z`, and merge the same fix into `main`. +`[limitation]` TagOnMerge watches `main` only, so a backport's tag is cut by +hand, after checking the same `lab/tests` coverage. + +What the numbers mean (decision 0033): before 1.0, in `0.Y.Z` **raising `Z` is any non-breaking change, new features included, and raising `Y` is an interface break** -- `0.2.4 -> 0.3.0` declares a break and `0.2.4 -> 0.2.5` a compatible release, which is also how Julia's `^0.2` compat bound reads them. A break is anything that lets working downstream code behave differently: a signature, an export, or what a call returns or throws. A bug fix that changes behaviour only on a path that was already broken is not a break. Config types are built by keyword (lab decision 0035): adding a field with a default is not a break, and a positional argument's meaning never changes (add a keyword and deprecate the old form instead). Hardware verification is not tracked in this repo; it is recorded by the downstream rig repo that pins to a given tag. The merge gate is the local suite (see "Testing policy" above) plus `test/contract.jl`'s "Interface Contract" testset, which guards the no-ambiguous-exports and core-method invariants described above; CI confirms it on a reduced matrix. diff --git a/Project.toml b/Project.toml index 1f56ba2..50f08c3 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "MicroscopeControl" uuid = "aa70d9ae-4a1e-49fd-870a-8ccfd99f4c3e" -version = "0.2.4" +version = "0.2.5-DEV" authors = ["klidke@unm.edu"] [deps] diff --git a/src/hardware_implementations/pi_stage/config_methods.jl b/src/hardware_implementations/pi_stage/config_methods.jl index a2525b7..0d98669 100644 --- a/src/hardware_implementations/pi_stage/config_methods.jl +++ b/src/hardware_implementations/pi_stage/config_methods.jl @@ -42,13 +42,24 @@ function initialize_original(stage::PIStage) #TODO: Error handling #Set servo mode to on for both axes, noting axis X is labeled "1" and axis Y is labeled "2" servo(stage, true, true) - #Reference stage - referencemove(stage) + #Reference stage. A rejected FRF (e.g. GCS error 5, servo off on one axis) used to be + # ignored: every later PI_MOV was refused too, while the driver's cached position said + # the stage was centred. Refuse to come back from initialize unreferenced. + # On failure, close the connection so a retried initialize starts clean. + try + if referencemove(stage) != 1 + error("PI_FRF refused (GCS error $(_pi_geterror(stage))); stage is not referenced") + end + _waitforreference(stage) + catch + shutdown_original(stage) + rethrow() + end #Find the max and min position of the axes getrange(stage) - - #Sleep 1 second for initialization + + #Wait for any remaining motion to finish ismoving(stage) while stage.ismoving[1] == 1 || stage.ismoving[2] == 1 ismoving(stage) @@ -71,6 +82,27 @@ function referencemove(stage::PIStage) return ismoved end +# PI_GetError returns and clears the controller's last GCS error code (0 = none). +_pi_geterror(stage::PIStage) = @ccall gcs2path.PI_GetError(stage.id::Cint)::Cint + +""" +Poll `PI_qFRF` until both axes report referenced; throw if that has not happened within +`timeout` seconds or the query itself fails. +""" +function _waitforreference(stage::PIStage; timeout::Real = 60.0) + # PI_qFRF fills `BOOL*`: one 32-bit int per axis, like PI_SVO. + referenced = zeros(Cint, 2) + deadline = time() + timeout + while true + ok = @ccall gcs2path.PI_qFRF(stage.id::Cint, "1 2"::Ptr{UInt8}, referenced::Ptr{Cint})::Cint + ok == 1 || error("PI_qFRF failed (GCS error $(_pi_geterror(stage)))") + all(!=(0), referenced) && return nothing + time() > deadline && error("PI stage not referenced after $(timeout) s: " * + "qFRF = $(Int.(referenced)), GCS error $(_pi_geterror(stage))") + sleep(0.1) + end +end + """ Function to disconnect PI Stage