Skip to content

Commit 781fa4a

Browse files
kalidkeclaude
andcommitted
Merge #73 (4618582): PI stage fixes, checked status returns and a bounded stop wait
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 parents 7acee6c + 4618582 commit 781fa4a

15 files changed

Lines changed: 625 additions & 84 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ the next version with `-DEV`).
1010

1111
## [Unreleased]
1212

13+
### Fixed
14+
- `PIStage`: `shutdown` could close another object's connection. `id` defaulted to `0`, a valid GCS id, and was never reset; it now defaults to `-1` and `shutdown` resets it.
15+
- `PIStage.initialize` reported the stage connected before it was: `connectionstatus` was set before the connect, and a failed close after a failed reference left it `true`, so a retry answered "already initialized". It is now set only after the whole sequence succeeds, and every step after the connect is inside the cleanup.
16+
- `PIStage.initialize` ignored a FALSE from reading the travel range (`PI_qTMN`/`PI_qTMX`) or setting the velocity, and came up connected with an unset range. Each is now checked; a failure closes the connection and throws. A failed range query no longer writes an uninitialized buffer into `range_x`/`range_y`.
17+
- `PIStage.initialize`'s wait for motion to stop after the reference move had no deadline and ignored `PI_IsMoving`'s return, so a failed query could spin forever. It now polls every 0.1 s, throws on a failed query, and gives up after `REFERENCE_TIMEOUT_S`.
18+
- A `PIStage.initialize` retried after a failed close reported the controller held by another process. The stage now closes its own earlier connection first, and does not reconnect if that close fails too.
19+
- The stage, Triggerscope and objective-positioner panels log a failed `initialize` instead of throwing out of the callback, through one helper, and a stage panel reads no position after an `initialize` that did not connect.
20+
21+
### Changed
22+
- `PIStage.initialize` waits for `PI_IsControllerReady` after the reference move, before polling `PI_qFRF`, as PI's samples do. Not yet run on hardware; needs a rig check on the C-867.
23+
24+
### Added
25+
- Fake-GCS2 tests for `PIStage` (`test/pi_stage_fake_sdk.jl`, `test/pi_stage.jl`): `initialize`'s ordering and cleanup and `shutdown`'s id handling, the range, velocity and motion-stop checks, the reclaim after a failed close, and the GUI guard, with no hardware.
26+
1327
## [0.2.5] - 2026-09-29
1428

1529
A non-breaking release. It brings the TCube laser's closed-loop (power) mode and

‎src/hardware_implementations/pi_stage/PI.jl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module PI
55

66
global const gcs2path = "C:\\Program Files (x86)\\Physik Instrumente (PI)\\Software Suite\\Development\\C++\\API\\PI_GCS2_DLL_x64.dll"
77

8+
include("gcs2.jl")
89
include("types.jl")
910
include("move_methods.jl")
1011
include("query_methods.jl")
Lines changed: 112 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,160 @@
11
"""
2-
Function to initialize PI Stage, right now this requires calibration using PiMikroMove to work correctly, no documentation on how to calibrate using the PI_GCS2 library
2+
Initialize the PI stage. Connects to the first PI C-867 the GCS2 library enumerates, turns both
3+
servos on, starts the reference move, waits for the controller and for both axes to report
4+
referenced, reads the travel range, waits for motion to stop, and sets `stage.velocity`.
5+
6+
`connectionstatus` becomes true only when all of that succeeded. A failure after the connect
7+
closes the connection and throws. Enumeration and connect failures log `@error` and return with
8+
`connectionstatus == false`.
9+
10+
`[limitation]` Not yet run on hardware; the wait for `PI_IsControllerReady` needs a rig check on
11+
the C-867. The stage needs calibration using PiMikroMove to work correctly; there is no
12+
documentation on how to calibrate using the PI_GCS2 library.
313
"""
4-
function initialize_original(stage::PIStage) #TODO: Error handling
14+
function initialize_original(stage::PIStage)
515
if stage.connectionstatus == true
616
@error "Stage already initialized"
717
return
818
end
919

20+
if stage.id >= 0
21+
# An earlier initialize connected and its close failed: this stage still holds the
22+
# controller, and the DLL does not enumerate a controller that is open. Close it first.
23+
@info "Closing this stage's earlier connection (id $(stage.id)) before reconnecting"
24+
shutdown_original(stage)
25+
if stage.id >= 0
26+
@error "This stage's earlier connection (id $(stage.id)) could not be closed; not reconnecting"
27+
return
28+
end
29+
end
30+
1031
# Create a buffer string
1132
bufferstring = Vector{UInt8}(undef, 1024)
1233

1334
#Find number of connected USB devices, specifically the PI C-867 controller
14-
numconnected = @ccall gcs2path.PI_EnumerateUSB(bufferstring::Ptr{UInt8}, 1024::Cint, "PI C-867"::Ptr{UInt8})::Cint
35+
numconnected = PI_EnumerateUSB(bufferstring, 1024, "PI C-867")
1536

1637
@info "Number of connected devices: " * string(numconnected)
1738

18-
#Set connection status to true
19-
if numconnected > 0
20-
stage.connectionstatus = true
21-
else
39+
if numconnected <= 0
2240
# The DLL enumerates only controllers nobody has open: a C-867 that Device Manager
2341
# still lists is held by another process (a second Julia with an initialized stage —
2442
# under any Windows user —, PIMikroMove, or an open COM port).
2543
@error "No PI C-867 found by the GCS2 library — controller absent, or held by another process"
26-
stage.connectionstatus = false
2744
return
2845
end
2946
#Connect to usb device
30-
stage.id = @ccall gcs2path.PI_ConnectUSB(bufferstring::Ptr{UInt8})::Cint
47+
stage.id = PI_ConnectUSB(bufferstring)
3148

3249
@info "Device ID: " * string(stage.id)
3350

3451
if stage.id < 0
3552
# The connect itself failed (id -1): typically another process already holds the
3653
# controller (a second Julia with an initialized stage, PIMikroMove, an open COM port).
37-
stage.connectionstatus = false
3854
@error "PI_ConnectUSB failed — the controller is probably held by another process"
3955
return
4056
end
4157

42-
#Set servo mode to on for both axes, noting axis X is labeled "1" and axis Y is labeled "2"
43-
servo(stage, true, true)
44-
45-
#Reference stage. A rejected FRF (e.g. GCS error 5, servo off on one axis) used to be
46-
# ignored: every later PI_MOV was refused too, while the driver's cached position said
47-
# the stage was centred. Refuse to come back from initialize unreferenced.
48-
# On failure, close the connection so a retried initialize starts clean.
58+
# Everything after the connect is inside the cleanup: a failure closes the connection so a
59+
# retried initialize starts clean, and connectionstatus is set only once all of it succeeded.
4960
try
61+
#Set servo mode to on for both axes, noting axis X is labeled "1" and axis Y is labeled "2"
62+
servo(stage, true, true)
63+
64+
#Reference stage. A rejected FRF (e.g. GCS error 5, servo off on one axis) used to be
65+
# ignored: every later PI_MOV was refused too, while the driver's cached position said
66+
# the stage was centred. Refuse to come back from initialize unreferenced.
5067
if referencemove(stage) != 1
5168
error("PI_FRF refused (GCS error $(_pi_geterror(stage))); stage is not referenced")
5269
end
53-
_waitforreference(stage)
70+
_waitforready(stage; timeout = REFERENCE_TIMEOUT_S[])
71+
_waitforreference(stage; timeout = REFERENCE_TIMEOUT_S[])
72+
73+
#Find the max and min position of the axes
74+
getrange(stage) == 1 ||
75+
error("PI_qTMN/PI_qTMX failed (GCS error $(_pi_geterror(stage))); travel range unknown")
76+
77+
#Wait for any remaining motion to finish
78+
_waitforstop(stage; timeout = REFERENCE_TIMEOUT_S[])
79+
80+
#Set the velocity to `stage.velocity`
81+
setvel(stage, stage.velocity) == 1 ||
82+
error("PI_VEL/PI_qVEL failed (GCS error $(_pi_geterror(stage))); velocity not set")
5483
catch
5584
shutdown_original(stage)
5685
rethrow()
5786
end
5887

59-
#Find the max and min position of the axes
60-
getrange(stage)
61-
62-
#Wait for any remaining motion to finish
63-
ismoving(stage)
64-
while stage.ismoving[1] == 1 || stage.ismoving[2] == 1
65-
ismoving(stage)
66-
end
67-
68-
#Set velocity to 1 mm/s
69-
success = setvel(stage, stage.velocity)
70-
88+
stage.connectionstatus = true
7189
@info "Stage initialized"
7290
return
7391
end
7492

7593
"""
76-
Function to calibrate PI Stage, not implemented yet as there is no documentation for this stage on calibration using the PI_GCS2 library
77-
Possibly must use PiMikroMove to calibrate, but this is not ideal, however there is a CLI
78-
94+
Start the reference move (`PI_FRF`) on both axes and return the GCS BOOL, 1 if accepted.
95+
`initialize` waits for it to finish.
7996
"""
8097
function referencemove(stage::PIStage)
81-
ismoved = @ccall gcs2path.PI_FRF(stage.id::Cint, "1 2"::Ptr{UInt8})::Cint
98+
ismoved = PI_FRF(stage.id, "1 2")
8299
return ismoved
83100
end
84101

85102
# PI_GetError returns and clears the controller's last GCS error code (0 = none).
86-
_pi_geterror(stage::PIStage) = @ccall gcs2path.PI_GetError(stage.id::Cint)::Cint
103+
_pi_geterror(stage::PIStage) = PI_GetError(stage.id)
104+
105+
"""
106+
How long each of `initialize`'s three waits may take, in seconds: for the controller to report
107+
ready, for both axes to report referenced, and for motion to stop. The budgets are separate, so
108+
`initialize` can wait up to three times this in all. A `Ref` so tests can shorten it.
109+
"""
110+
const REFERENCE_TIMEOUT_S = Ref(60.0)
111+
112+
"""
113+
Poll `PI_IsControllerReady` every 0.1 s until the controller reports ready; throw if the call
114+
fails or it is not ready within `timeout` seconds.
115+
116+
`[limitation]` Not yet run on hardware; the wait needs a rig check on the C-867.
117+
"""
118+
function _waitforready(stage::PIStage; timeout::Real = REFERENCE_TIMEOUT_S[])
119+
ready = Ref{Cint}(0)
120+
deadline = time() + timeout
121+
while true
122+
ok = PI_IsControllerReady(stage.id, ready)
123+
ok == 0 && error("PI_IsControllerReady failed (GCS error $(_pi_geterror(stage)))")
124+
ready[] != 0 && return nothing
125+
time() > deadline && error("PI controller not ready after $(timeout) s")
126+
sleep(0.1)
127+
end
128+
end
129+
130+
"""
131+
Poll `PI_IsMoving` every 0.1 s until neither axis is moving; throw if the query fails or motion
132+
has not stopped within `timeout` seconds. Query only: it sends no motion command.
133+
"""
134+
function _waitforstop(stage::PIStage; timeout::Real = REFERENCE_TIMEOUT_S[])
135+
# PI_IsMoving fills `BOOL*`, bound as UInt32 in gcs2.jl.
136+
moving = zeros(UInt32, 2)
137+
deadline = time() + timeout
138+
while true
139+
ok = PI_IsMoving(stage.id, "1 2", moving)
140+
ok == 1 || error("PI_IsMoving failed (GCS error $(_pi_geterror(stage)))")
141+
stage.ismoving = (moving[1] != 0, moving[2] != 0)
142+
any(!=(0), moving) || return nothing
143+
time() > deadline && error("PI stage still moving after $(timeout) s")
144+
sleep(0.1)
145+
end
146+
end
87147

88148
"""
89149
Poll `PI_qFRF` until both axes report referenced; throw if that has not happened within
90150
`timeout` seconds or the query itself fails.
91151
"""
92-
function _waitforreference(stage::PIStage; timeout::Real = 60.0)
152+
function _waitforreference(stage::PIStage; timeout::Real = REFERENCE_TIMEOUT_S[])
93153
# PI_qFRF fills `BOOL*`: one 32-bit int per axis, like PI_SVO.
94154
referenced = zeros(Cint, 2)
95155
deadline = time() + timeout
96156
while true
97-
ok = @ccall gcs2path.PI_qFRF(stage.id::Cint, "1 2"::Ptr{UInt8}, referenced::Ptr{Cint})::Cint
157+
ok = PI_qFRF(stage.id, "1 2", referenced)
98158
ok == 1 || error("PI_qFRF failed (GCS error $(_pi_geterror(stage)))")
99159
all(!=(0), referenced) && return nothing
100160
time() > deadline && error("PI stage not referenced after $(timeout) s: " *
@@ -108,21 +168,23 @@ end
108168
Function to disconnect PI Stage
109169
"""
110170
function shutdown_original(stage::PIStage)
111-
isconnected = @ccall gcs2path.PI_IsConnected(stage.id::Cint)::Cint
171+
isconnected = PI_IsConnected(stage.id)
112172

113173
if isconnected == 1
114-
@ccall gcs2path.PI_CloseConnection(stage.id::Cint)::Cvoid
115-
isconnected = @ccall gcs2path.PI_IsConnected(stage.id::Cint)::Cint
174+
PI_CloseConnection(stage.id)
175+
isconnected = PI_IsConnected(stage.id)
116176

117177
if isconnected == 1
118178
@error "Stage failed to disconnect"
119179
else
120180
@info "Stage disconnected"
121181
stage.connectionstatus = false
182+
stage.id = Cint(-1)
122183
end
123184
else
124185
@error "Stage already disconnected"
125186
stage.connectionstatus = false
187+
stage.id = Cint(-1)
126188
end
127189
end
128190

@@ -133,7 +195,7 @@ function servo(stage::PIStage, xtoggle::Bool, ytoggle::Bool)
133195
# PI_SVO takes `const BOOL*` = 32-bit ints, one per axis. Passing two UInt8 made the DLL
134196
# read axis 2's flag from whatever byte followed the array: servo silently OFF on Y,
135197
# every PI_MOV refused with GCS error 5 (worked by luck on Julia 1.10, failed on 1.13).
136-
istoggled = @ccall gcs2path.PI_SVO(stage.id::Cint, "1 2"::Ptr{UInt8}, Cint[xtoggle, ytoggle]::Ptr{Cint})::Cint
198+
istoggled = PI_SVO(stage.id, "1 2", Cint[xtoggle, ytoggle])
137199
stage.servostatus = (xtoggle, ytoggle)
138200

139201
if istoggled == 1
@@ -147,7 +209,7 @@ end
147209
Sets the servo state of the x axis
148210
"""
149211
function servox(stage::PIStage, xtoggle::Bool)
150-
@ccall gcs2path.PI_SVO(stage.id::Cint, "1"::Ptr{UInt8}, Cint[xtoggle]::Ptr{Cint})::Cint
212+
PI_SVO(stage.id, "1", Cint[xtoggle])
151213
stage.servostatus = (xtoggle, stage.servostatus[2])
152214
end
153215

@@ -156,25 +218,25 @@ end
156218
Sets the servo state of the y axis
157219
"""
158220
function servoy(stage::PIStage, ytoggle::Bool)
159-
@ccall gcs2path.PI_SVO(stage.id::Cint, "2"::Ptr{UInt8}, Cint[ytoggle]::Ptr{Cint})::Cint
221+
PI_SVO(stage.id, "2", Cint[ytoggle])
160222
stage.servostatus = (stage.servostatus[1], ytoggle)
161223
end
162224

163225

164226
function setvel(stage::PIStage,vel::Vector{Float64})
165227

166-
success = @ccall gcs2path.PI_VEL(stage.id::Cint, "1 2"::Ptr{UInt8}, vel::Ptr{Cdouble})::Cint
228+
setok = PI_VEL(stage.id, "1 2", vel)
167229

168-
if success == 0
230+
if setok == 0
169231
@error "Failed to set velocity"
170232
end
171-
velocity = Vector{Cdouble}(undef, 2)
172-
success = @ccall gcs2path.PI_qVEL(stage.id::Cint, "1 2"::Ptr{UInt8}, velocity::Ptr{Cdouble})::Cint
173-
174-
if success == 0
233+
velocity = zeros(Cdouble, 2)
234+
queryok = PI_qVEL(stage.id, "1 2", velocity)
235+
236+
if queryok == 0
175237
@error "Failed to query velocity"
176238
else
177239
stage.velocity = velocity
178240
end
179-
return success
241+
return setok == 1 && queryok == 1 ? Cint(1) : Cint(0)
180242
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# One wrapper per PI GCS2 DLL function: the seam test/pi_stage_fake_sdk.jl replaces.
2+
PI_EnumerateUSB(buffer, bufsize, filter) = @ccall gcs2path.PI_EnumerateUSB(buffer::Ptr{UInt8}, bufsize::Cint, filter::Ptr{UInt8})::Cint
3+
PI_ConnectUSB(description) = @ccall gcs2path.PI_ConnectUSB(description::Ptr{UInt8})::Cint
4+
PI_IsConnected(ID) = @ccall gcs2path.PI_IsConnected(ID::Cint)::Cint
5+
PI_CloseConnection(ID) = @ccall gcs2path.PI_CloseConnection(ID::Cint)::Cvoid
6+
PI_GetError(ID) = @ccall gcs2path.PI_GetError(ID::Cint)::Cint
7+
PI_IsControllerReady(ID, piControllerReady) = @ccall gcs2path.PI_IsControllerReady(ID::Cint, piControllerReady::Ptr{Cint})::Cint
8+
PI_FRF(ID, axes) = @ccall gcs2path.PI_FRF(ID::Cint, axes::Ptr{UInt8})::Cint
9+
PI_qFRF(ID, axes, referenced) = @ccall gcs2path.PI_qFRF(ID::Cint, axes::Ptr{UInt8}, referenced::Ptr{Cint})::Cint
10+
PI_SVO(ID, axes, values) = @ccall gcs2path.PI_SVO(ID::Cint, axes::Ptr{UInt8}, values::Ptr{Cint})::Cint
11+
PI_VEL(ID, axes, values) = @ccall gcs2path.PI_VEL(ID::Cint, axes::Ptr{UInt8}, values::Ptr{Cdouble})::Cint
12+
PI_qVEL(ID, axes, values) = @ccall gcs2path.PI_qVEL(ID::Cint, axes::Ptr{UInt8}, values::Ptr{Cdouble})::Cint
13+
PI_MOV(ID, axes, values) = @ccall gcs2path.PI_MOV(ID::Cint, axes::Ptr{UInt8}, values::Ptr{Cdouble})::Cint
14+
PI_HLT(ID, axes) = @ccall gcs2path.PI_HLT(ID::Cint, axes::Ptr{UInt8})::Cint
15+
PI_STP(ID) = @ccall gcs2path.PI_STP(ID::Cint)::Cint
16+
PI_qPOS(ID, axes, values) = @ccall gcs2path.PI_qPOS(ID::Cint, axes::Ptr{UInt8}, values::Ptr{Cdouble})::Cint
17+
PI_IsMoving(ID, axes, values) = @ccall gcs2path.PI_IsMoving(ID::Cint, axes::Ptr{UInt8}, values::Ptr{UInt32})::Cint
18+
PI_qTMN(ID, axes, values) = @ccall gcs2path.PI_qTMN(ID::Cint, axes::Ptr{UInt8}, values::Ptr{Cdouble})::Cint
19+
PI_qTMX(ID, axes, values) = @ccall gcs2path.PI_qTMX(ID::Cint, axes::Ptr{UInt8}, values::Ptr{Cdouble})::Cint

‎src/hardware_implementations/pi_stage/interface_methods.jl‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
2-
Function to initialize PI Stage, right now this requires calibration using PiMikroMove to work correctly, no documentation on how to calibrate using the PI_GCS2 library
2+
Initialize the PI stage; see `initialize_original` for the sequence and its failure behaviour.
33
"""
4-
function initialize(stage::PIStage) #TODO: Error handling
4+
function initialize(stage::PIStage)
55
initialize_original(stage)
66
end
77

@@ -37,6 +37,7 @@ Function to update the position range of the PI Stage
3737
"""
3838
function StageInterface.getrange(stage::PIStage)
3939
getrange(stage)
40+
return stage.range_y # preserves 0.2.5's return value
4041
end
4142

4243

‎src/hardware_implementations/pi_stage/move_methods.jl‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Function to move PI Stage to a specific position
44
"""
55
function move(stage::PIStage, x::Float64, y::Float64)
6-
ok = @ccall gcs2path.PI_MOV(stage.id::Cint, "1 2"::Ptr{UInt8}, [Cdouble(x),Cdouble(y)]::Ptr{Cdouble})::Cint
6+
ok = PI_MOV(stage.id, "1 2", [Cdouble(x),Cdouble(y)])
77
ok == 1 || @error "PI_MOV refused — stage not connected, not referenced, or servo off"
88
stage.targ_x = x
99
stage.targ_y = y
@@ -14,7 +14,7 @@ end
1414
Function to move PI Stage, and wait for completion
1515
"""
1616
function moveandwait(stage::PIStage, x::Float64, y::Float64)
17-
@ccall gcs2path.PI_MOV(stage.id::Cint, "1 2"::Ptr{UInt8}, [Cdouble(x),Cdouble(y)]::Ptr{Cdouble})::Cint
17+
PI_MOV(stage.id, "1 2", [Cdouble(x),Cdouble(y)])
1818
stage.targ_x = x
1919
stage.targ_y = y
2020
ismoving(stage)
@@ -28,23 +28,23 @@ end
2828
Function to move PI Stage X axis to a specific position
2929
"""
3030
function movex(stage::PIStage, x::Float64)
31-
@ccall gcs2path.PI_MOV(stage.id::Cint, "1"::Ptr{UInt8}, [Cdouble(x)]::Ptr{Cdouble})::Cint
31+
PI_MOV(stage.id, "1", [Cdouble(x)])
3232
stage.targ_x = x
3333
end
3434

3535
"""
3636
Function to move PI Stage Y axis to a specific position
3737
"""
3838
function movey(stage::PIStage, y::Float64)
39-
@ccall gcs2path.PI_MOV(stage.id::Cint, "2"::Ptr{UInt8}, [Cdouble(y)]::Ptr{Cdouble})::Cint
39+
PI_MOV(stage.id, "2", [Cdouble(y)])
4040
stage.targ_y = y
4141
end
4242

4343
"""
4444
Function call to smoothly stop motion of the PI Stage
4545
"""
4646
function stopmotion(stage::PIStage)
47-
isstopped = @ccall gcs2path.PI_HLT(stage.id::Cint, "1 2"::Ptr{UInt8})::Cint
47+
isstopped = PI_HLT(stage.id, "1 2")
4848
if isstopped == 1
4949
@info "Motion successfully stopped"
5050
else
@@ -56,7 +56,7 @@ end
5656
Function call to immediately stop the PI Stage
5757
"""
5858
function immediatestop(stage::PIStage)
59-
isstopped = @ccall gcs2path.PI_STP(stage.id::Cint)::Cint
59+
isstopped = PI_STP(stage.id)
6060
if isstopped == 1
6161
@info "Motion successfully stopped"
6262
else

0 commit comments

Comments
 (0)