Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 22 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
40 changes: 36 additions & 4 deletions src/hardware_implementations/pi_stage/config_methods.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
Loading