From d57034ff61f7168c8d5ffe7341952a9df09df048 Mon Sep 17 00:00:00 2001 From: kalidke Date: Thu, 29 Jan 2026 13:05:05 -0700 Subject: [PATCH 01/11] Fix precompilation errors and add AbstractSystem interface - Add AbstractSystem and AbstractSystemState types to instrument.jl - Export get_state, set_state for system interface - Fix armcamera duplicate method in ThorCamCSC (remove default argument) - Fix CrystaLaser constructor: handle missing NI-DAQ gracefully - Fix VortranLaser constructor: handle missing NI-DAQ gracefully - Both laser constructors now use try-catch and validate device list Co-Authored-By: Claude Opus 4.5 --- src/MicroscopeControl.jl | 3 +- .../crysta_laser_561/types.jl | 27 +++--- .../thorcam_csc/thorcamcsc_camcontrol.jl | 2 +- .../vortran_laser_488/types.jl | 27 +++--- src/instrument.jl | 87 +++++++++++++++++++ 5 files changed, 123 insertions(+), 23 deletions(-) diff --git a/src/MicroscopeControl.jl b/src/MicroscopeControl.jl index 929c0f2..4e20e9e 100644 --- a/src/MicroscopeControl.jl +++ b/src/MicroscopeControl.jl @@ -8,7 +8,8 @@ using HDF5 # To export the abstract types and methods for all the instruments include("instrument.jl") export AbstractInstrument -export export_state, initialize, shutdown +export AbstractSystem, AbstractSystemState +export export_state, initialize, shutdown, get_state, set_state # Including the hardware interfaces and implementations and the HDF5 file saving methods include("hardware_interfaces/HardwareInterfaces.jl") diff --git a/src/hardware_implementations/crysta_laser_561/types.jl b/src/hardware_implementations/crysta_laser_561/types.jl index 5206e52..15f14b3 100644 --- a/src/hardware_implementations/crysta_laser_561/types.jl +++ b/src/hardware_implementations/crysta_laser_561/types.jl @@ -41,18 +41,23 @@ info function CrystaLaser(; unique_id::String="CrystaLaser", properties::LightSourceProperties=LightSourceProperties("mW", 0.0, false, 0.0, 100.0), - laser_color::String="561", min_voltage=0.7 ,max_voltage=2.7785 + laser_color::String="561", min_voltage=0.7, max_voltage=2.7785 ) - - J = 1 # 561 - if !isdefined(Main, :channelsAO) - daq = NIdaq() + daq = NIdaq() + channelsAO = String[] + channelsDO = String[] + + try devs = NIDAQcard.showdevices(daq) - channelsAO = NIDAQcard.showchannels(daq,"AO",devs[1]) - end - if !isdefined(Main, :channelsDO) - channelsDO = NIDAQcard.showchannels(daq,"DO",devs[1]) + if !isempty(devs) && devs[1] != "" + channelsAO = NIDAQcard.showchannels(daq, "AO", devs[1]) + channelsDO = NIDAQcard.showchannels(daq, "DO", devs[1]) + else + @warn "No NI-DAQ devices found for CrystaLaser" + end + catch e + @warn "Failed to initialize NI-DAQ for CrystaLaser: $e" end - - CrystaLaser(unique_id, properties, laser_color, daq , min_voltage, max_voltage, channelsAO, channelsDO) + + CrystaLaser(unique_id, properties, laser_color, daq, min_voltage, max_voltage, channelsAO, channelsDO) end \ No newline at end of file diff --git a/src/hardware_implementations/thorcam_csc/thorcamcsc_camcontrol.jl b/src/hardware_implementations/thorcam_csc/thorcamcsc_camcontrol.jl index 148f141..3482ac8 100644 --- a/src/hardware_implementations/thorcam_csc/thorcamcsc_camcontrol.jl +++ b/src/hardware_implementations/thorcam_csc/thorcamcsc_camcontrol.jl @@ -1,5 +1,5 @@ #Functions to control camera operations -function armcamera(camera::ThorCamCSCCamera, num_frames::Cint = Cint(2)) +function armcamera(camera::ThorCamCSCCamera, num_frames::Cint) number_of_frames_to_buffer = Cint(num_frames) is_camera_armed = @ccall "thorlabs_tsi_camera_sdk.dll".tl_camera_arm(camera.camera_handle::Ptr{Cvoid}, number_of_frames_to_buffer::Cint)::Cint if is_camera_armed != 0 diff --git a/src/hardware_implementations/vortran_laser_488/types.jl b/src/hardware_implementations/vortran_laser_488/types.jl index fd370ff..be855a2 100644 --- a/src/hardware_implementations/vortran_laser_488/types.jl +++ b/src/hardware_implementations/vortran_laser_488/types.jl @@ -29,16 +29,23 @@ function VortranLaser(; properties::LightSourceProperties=LightSourceProperties("mW", 0.0, false, 0.0, 100.0), laser_color::String="488" ) - J = 2 # 488 - if !isdefined(Main, :channelsAO) - daq = NIdaq() + daq = NIdaq() + channelsAO = String[] + channelsDO = String[] + min_voltage::Float64 = 0.0 + max_voltage::Float64 = 5.0 + + try devs = NIDAQcard.showdevices(daq) - channelsAO = NIDAQcard.showchannels(daq,"AO",devs[1]) - end - if !isdefined(Main, :channelsDO) - channelsDO = NIDAQcard.showchannels(daq,"DO",devs[1]) + if !isempty(devs) && devs[1] != "" + channelsAO = NIDAQcard.showchannels(daq, "AO", devs[1]) + channelsDO = NIDAQcard.showchannels(daq, "DO", devs[1]) + else + @warn "No NI-DAQ devices found for VortranLaser" + end + catch e + @warn "Failed to initialize NI-DAQ for VortranLaser: $e" end - min_voltage::Float64=0.0 - max_voltage::Float64=5.0 - VortranLaser(unique_id, properties, laser_color, daq , min_voltage, max_voltage, channelsAO, channelsDO) + + VortranLaser(unique_id, properties, laser_color, daq, min_voltage, max_voltage, channelsAO, channelsDO) end \ No newline at end of file diff --git a/src/instrument.jl b/src/instrument.jl index fb1f226..ae80ebd 100644 --- a/src/instrument.jl +++ b/src/instrument.jl @@ -40,4 +40,91 @@ Shuts down the instrument. """ function shutdown(instrument::AbstractInstrument) @error "Shutdown not implemented for this instrument" +end + +# ============================================================================= +# AbstractSystem - Composite of instruments +# ============================================================================= + +""" + AbstractSystemState + +Abstract type for system state. Concrete implementations should contain +the configuration parameters for a specific system. +""" +abstract type AbstractSystemState end + +""" + AbstractSystem + +Abstract type for a microscope system (composite of instruments). +Systems compose multiple instruments and manage their collective state. +""" +abstract type AbstractSystem end + +""" + get_state(sys::AbstractSystem) + +Get the current state of the system. + +# Arguments +- `sys::AbstractSystem`: The system to get state from. + +# Returns +- An `AbstractSystemState` representing the current configuration. +""" +function get_state(sys::AbstractSystem) + @error "get_state not implemented for $(typeof(sys))" +end + +""" + set_state(sys::AbstractSystem, state::AbstractSystemState) + +Set the state of the system, updating all relevant instruments. + +# Arguments +- `sys::AbstractSystem`: The system to configure. +- `state::AbstractSystemState`: The desired state. +""" +function set_state(sys::AbstractSystem, state::AbstractSystemState) + @error "set_state not implemented for $(typeof(sys))" +end + +""" + initialize(sys::AbstractSystem) + +Initialize all instruments in the system. + +# Arguments +- `sys::AbstractSystem`: The system to initialize. +""" +function initialize(sys::AbstractSystem) + @error "initialize not implemented for $(typeof(sys))" +end + +""" + shutdown(sys::AbstractSystem) + +Shutdown all instruments in the system. + +# Arguments +- `sys::AbstractSystem`: The system to shut down. +""" +function shutdown(sys::AbstractSystem) + @error "shutdown not implemented for $(typeof(sys))" +end + +""" + export_state(sys::AbstractSystem) + +Export the state of the system as a tuple of attributes, data, and children. + +# Arguments +- `sys::AbstractSystem`: The system to export state from. + +# Returns +- A tuple of (attributes::Dict, data, children::Dict). +""" +function export_state(sys::AbstractSystem) + @error "export_state not implemented for $(typeof(sys))" end \ No newline at end of file From c7a765d220d01bf5f6968389f4e3f0c1c3073c79 Mon Sep 17 00:00:00 2001 From: kalidke Date: Thu, 29 Jan 2026 13:23:05 -0700 Subject: [PATCH 02/11] Fix DaqTrLight to handle missing NI-DAQ gracefully - Constructor now validates device index before accessing - All interface methods check for empty channels before use - Warnings instead of crashes when hardware unavailable Co-Authored-By: Claude Opus 4.5 --- .../interface_methods.jl | 56 +++++++++---------- .../daq_transmission_light/types.jl | 25 ++++++--- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/src/hardware_implementations/daq_transmission_light/interface_methods.jl b/src/hardware_implementations/daq_transmission_light/interface_methods.jl index b2a4e2a..e0a3456 100644 --- a/src/hardware_implementations/daq_transmission_light/interface_methods.jl +++ b/src/hardware_implementations/daq_transmission_light/interface_methods.jl @@ -3,54 +3,52 @@ function initialize(light::DaqTrLight) end function LightSourceInterface.setpower(light::DaqTrLight, voltage::Float64) - daq = light.daq - min_voltage = light.min_voltage - max_voltage = light.max_voltage - channelsAO = light.channelsAO - channelsDO = light.channelsDO + if isempty(light.channelsAO) + @warn "No AO channels available for DaqTrLight" + return + end if voltage < light.min_voltage || voltage > light.max_voltage @error "The voltage should be between $(light.min_voltage) and $(light.max_voltage)" return end - t = NIDAQcard.createtask(daq,"AO",channelsAO[1]) - NIDAQcard.setvoltage(daq,t, voltage) - NIDAQcard.deletetask(daq,t) + t = NIDAQcard.createtask(light.daq, "AO", light.channelsAO[1]) + NIDAQcard.setvoltage(light.daq, t, voltage) + NIDAQcard.deletetask(light.daq, t) end function LightSourceInterface.light_on(light::DaqTrLight) light.properties.is_on = true - # power = 20.0 - channelsAO = light.channelsAO - channelsDO = light.channelsDO - # voltage = (power-10)/90*(light.max_voltage-light.min_voltage)+light.min_voltage - # light.properties.power = power + if isempty(light.channelsAO) + @warn "No AO channels available for DaqTrLight" + return + end voltage = 1.0 - daq = light.daq - t = NIDAQcard.createtask(daq,"AO",light.channelsAO[1]) - NIDAQcard.setvoltage(daq,t, voltage) - NIDAQcard.deletetask(daq,t) + t = NIDAQcard.createtask(light.daq, "AO", light.channelsAO[1]) + NIDAQcard.setvoltage(light.daq, t, voltage) + NIDAQcard.deletetask(light.daq, t) end function LightSourceInterface.light_off(light::DaqTrLight) light.properties.is_on = false - daq = light.daq - channelsAO = light.channelsAO - channelsDO = light.channelsDO + if isempty(light.channelsAO) + @warn "No AO channels available for DaqTrLight" + return + end voltage::Float64 = 0.0 - t = NIDAQcard.createtask(daq,"AO",light.channelsAO[1]) - NIDAQcard.setvoltage(daq,t, voltage) - NIDAQcard.deletetask(daq,t) + t = NIDAQcard.createtask(light.daq, "AO", light.channelsAO[1]) + NIDAQcard.setvoltage(light.daq, t, voltage) + NIDAQcard.deletetask(light.daq, t) end function shutdown(light::DaqTrLight) light.properties.is_on = false - daq = light.daq - channelsAO = light.channelsAO - channelsDO = light.channelsDO + if isempty(light.channelsAO) + return + end voltage::Float64 = 0.0 - t = NIDAQcard.createtask(daq,"AO",light.channelsAO[1]) - NIDAQcard.setvoltage(daq,t, voltage) - NIDAQcard.deletetask(daq,t) + t = NIDAQcard.createtask(light.daq, "AO", light.channelsAO[1]) + NIDAQcard.setvoltage(light.daq, t, voltage) + NIDAQcard.deletetask(light.daq, t) end """ diff --git a/src/hardware_implementations/daq_transmission_light/types.jl b/src/hardware_implementations/daq_transmission_light/types.jl index 8421818..05a6884 100644 --- a/src/hardware_implementations/daq_transmission_light/types.jl +++ b/src/hardware_implementations/daq_transmission_light/types.jl @@ -24,15 +24,26 @@ end function DaqTrLight(; unique_id::String="DaqTrLight", - properties::LightSourceProperties=LightSourceProperties("mW", 0.0, false, 0.0, 100.0) + properties::LightSourceProperties=LightSourceProperties("mW", 0.0, false, 0.0, 100.0), + device_index::Int=2 ) - daq = NIdaq() - devs = NIDAQcard.showdevices(daq) - channelsAO = NIDAQcard.showchannels(daq,"AO",devs[2]) - channelsDO = NIDAQcard.showchannels(daq,"DO",devs[2]) - min_voltage::Float64=0.0 - max_voltage::Float64=5.0 + channelsAO = String[] + channelsDO = String[] + min_voltage::Float64 = 0.0 + max_voltage::Float64 = 5.0 + + try + devs = NIDAQcard.showdevices(daq) + if length(devs) >= device_index && devs[device_index] != "" + channelsAO = NIDAQcard.showchannels(daq, "AO", devs[device_index]) + channelsDO = NIDAQcard.showchannels(daq, "DO", devs[device_index]) + else + @warn "NI-DAQ device index $device_index not available for DaqTrLight (found $(length(devs)) devices)" + end + catch e + @warn "Failed to initialize NI-DAQ for DaqTrLight: $e" + end DaqTrLight(unique_id, properties, daq, min_voltage, max_voltage, channelsAO, channelsDO) end \ No newline at end of file From fb2f77d4013a4f070080b78a0267e6fd39c09b94 Mon Sep 17 00:00:00 2001 From: kalidke Date: Thu, 29 Jan 2026 13:28:01 -0700 Subject: [PATCH 03/11] Add error handling to laser interface methods - VortranLaser: wrap all DAQ operations in try-catch, check channel counts - CrystaLaser: wrap all DAQ operations in try-catch, check for empty channels - Shutdown now completes gracefully even when hardware unavailable Co-Authored-By: Claude Opus 4.5 --- .../crysta_laser_561/interface_methods.jl | 78 ++++++++------- .../vortran_laser_488/interface_methods.jl | 96 +++++++++++-------- 2 files changed, 101 insertions(+), 73 deletions(-) diff --git a/src/hardware_implementations/crysta_laser_561/interface_methods.jl b/src/hardware_implementations/crysta_laser_561/interface_methods.jl index a4b690c..74144e5 100644 --- a/src/hardware_implementations/crysta_laser_561/interface_methods.jl +++ b/src/hardware_implementations/crysta_laser_561/interface_methods.jl @@ -3,9 +3,6 @@ """ function initialize(light::CrystaLaser) light.properties.is_on = false - daq = light.daq - channelsAO = light.channelsAO - channelsDO = light.channelsDO return nothing end @@ -19,18 +16,21 @@ Set the power of the laser by setting the voltage of the NIDAQ card. - `voltage::Float64`: The voltage to set the laser to. """ function LightSourceInterface.setpower(light::CrystaLaser, voltage::Float64) - daq = light.daq - min_voltage = light.min_voltage - max_voltage = light.max_voltage - channelsAO = light.channelsAO - channelsDO = light.channelsDO + if isempty(light.channelsAO) + @warn "CrystaLaser: no AO channels available for setpower" + return + end if voltage < light.min_voltage || voltage > light.max_voltage @error "The voltage should be between $(light.min_voltage) and $(light.max_voltage)" return end - t = NIDAQcard.createtask(daq,"AO",channelsAO[1]) - NIDAQcard.setvoltage(daq,t, voltage) - NIDAQcard.deletetask(daq,t) + try + t = NIDAQcard.createtask(light.daq, "AO", light.channelsAO[1]) + NIDAQcard.setvoltage(light.daq, t, voltage) + NIDAQcard.deletetask(light.daq, t) + catch e + @warn "CrystaLaser setpower failed: $e" + end end """ @@ -38,16 +38,17 @@ end """ function LightSourceInterface.light_on(light::CrystaLaser) light.properties.is_on = true - # power = 20.0 - channelsAO = light.channelsAO - channelsDO = light.channelsDO - # voltage = (power-10)/90*(light.max_voltage-light.min_voltage)+light.min_voltage - # light.properties.power = power - voltage = 1.0 - daq = light.daq - t = NIDAQcard.createtask(daq,"AO",light.channelsAO[1]) - NIDAQcard.setvoltage(daq,t, voltage) - NIDAQcard.deletetask(daq,t) + if isempty(light.channelsAO) + @warn "CrystaLaser: no AO channels available for light_on" + return + end + try + t = NIDAQcard.createtask(light.daq, "AO", light.channelsAO[1]) + NIDAQcard.setvoltage(light.daq, t, 1.0) + NIDAQcard.deletetask(light.daq, t) + catch e + @warn "CrystaLaser light_on failed: $e" + end end """ @@ -55,13 +56,17 @@ end """ function LightSourceInterface.light_off(light::CrystaLaser) light.properties.is_on = false - daq = light.daq - channelsAO = light.channelsAO - channelsDO = light.channelsDO - voltage::Float64 = 0.0 - t = NIDAQcard.createtask(daq,"AO",light.channelsAO[1]) - NIDAQcard.setvoltage(daq,t, voltage) - NIDAQcard.deletetask(daq,t) + if isempty(light.channelsAO) + @warn "CrystaLaser: no AO channels available for light_off" + return + end + try + t = NIDAQcard.createtask(light.daq, "AO", light.channelsAO[1]) + NIDAQcard.setvoltage(light.daq, t, 0.0) + NIDAQcard.deletetask(light.daq, t) + catch e + @warn "CrystaLaser light_off failed: $e" + end end """ @@ -69,13 +74,16 @@ end """ function shutdown(light::CrystaLaser) light.properties.is_on = false - daq = light.daq - channelsAO = light.channelsAO - channelsDO = light.channelsDO - voltage::Float64 = 0.0 - t = NIDAQcard.createtask(daq,"AO",light.channelsAO[1]) - NIDAQcard.setvoltage(daq,t, voltage) - NIDAQcard.deletetask(daq,t) + if isempty(light.channelsAO) + return + end + try + t = NIDAQcard.createtask(light.daq, "AO", light.channelsAO[1]) + NIDAQcard.setvoltage(light.daq, t, 0.0) + NIDAQcard.deletetask(light.daq, t) + catch e + @warn "CrystaLaser shutdown failed: $e" + end end """ diff --git a/src/hardware_implementations/vortran_laser_488/interface_methods.jl b/src/hardware_implementations/vortran_laser_488/interface_methods.jl index 4d42b9a..7da851f 100644 --- a/src/hardware_implementations/vortran_laser_488/interface_methods.jl +++ b/src/hardware_implementations/vortran_laser_488/interface_methods.jl @@ -3,12 +3,17 @@ """ function initialize(light::VortranLaser) light.properties.is_on = false - daq = light.daq - channelsAO = light.channelsAO - channelsDO = light.channelsDO - td = NIDAQcard.createtask(daq,"DO",channelsDO[12]) - NIDAQcard.setvoltage(daq,td, 0.0) - NIDAQcard.deletetask(daq,td) + if length(light.channelsDO) < 12 + @warn "VortranLaser: insufficient DO channels for initialization" + return nothing + end + try + td = NIDAQcard.createtask(light.daq, "DO", light.channelsDO[12]) + NIDAQcard.setvoltage(light.daq, td, 0.0) + NIDAQcard.deletetask(light.daq, td) + catch e + @warn "VortranLaser initialization failed: $e" + end return nothing end @@ -16,18 +21,21 @@ end setpower(light::VortranLaser, voltage::Float64) """ function LightSourceInterface.setpower(light::VortranLaser, voltage::Float64) - daq = light.daq - min_voltage = light.min_voltage - max_voltage = light.max_voltage - channelsAO = light.channelsAO - channelsDO = light.channelsDO + if length(light.channelsAO) < 2 + @warn "VortranLaser: insufficient AO channels for setpower" + return + end if voltage < light.min_voltage || voltage > light.max_voltage @error "The voltage should be between $(light.min_voltage) and $(light.max_voltage)" return end - t = NIDAQcard.createtask(daq,"AO",channelsAO[2]) - NIDAQcard.setvoltage(daq,t, voltage) - NIDAQcard.deletetask(daq,t) + try + t = NIDAQcard.createtask(light.daq, "AO", light.channelsAO[2]) + NIDAQcard.setvoltage(light.daq, t, voltage) + NIDAQcard.deletetask(light.daq, t) + catch e + @warn "VortranLaser setpower failed: $e" + end end """ @@ -35,13 +43,17 @@ end """ function LightSourceInterface.light_on(light::VortranLaser) light.properties.is_on = true - channelsAO = light.channelsAO - channelsDO = light.channelsDO - voltage = 1.0 - daq = light.daq - td = NIDAQcard.createtask(daq,"DO",channelsDO[12]) - NIDAQcard.setvoltage(daq,td, 8.0) - NIDAQcard.deletetask(daq,td) + if length(light.channelsDO) < 12 + @warn "VortranLaser: insufficient DO channels for light_on" + return + end + try + td = NIDAQcard.createtask(light.daq, "DO", light.channelsDO[12]) + NIDAQcard.setvoltage(light.daq, td, 8.0) + NIDAQcard.deletetask(light.daq, td) + catch e + @warn "VortranLaser light_on failed: $e" + end end """ @@ -49,13 +61,17 @@ end """ function LightSourceInterface.light_off(light::VortranLaser) light.properties.is_on = false - daq = light.daq - channelsAO = light.channelsAO - channelsDO = light.channelsDO - voltage::Float64 = 0.0 - td = NIDAQcard.createtask(daq,"DO",channelsDO[12]) - NIDAQcard.setvoltage(daq,td, 0.0) - NIDAQcard.deletetask(daq,td) + if length(light.channelsDO) < 12 + @warn "VortranLaser: insufficient DO channels for light_off" + return + end + try + td = NIDAQcard.createtask(light.daq, "DO", light.channelsDO[12]) + NIDAQcard.setvoltage(light.daq, td, 0.0) + NIDAQcard.deletetask(light.daq, td) + catch e + @warn "VortranLaser light_off failed: $e" + end end """ @@ -63,16 +79,20 @@ end """ function shutdown(light::VortranLaser) light.properties.is_on = false - daq = light.daq - channelsAO = light.channelsAO - channelsDO = light.channelsDO - voltage::Float64 = 0.0 - td = NIDAQcard.createtask(daq,"DO",channelsDO[12]) - NIDAQcard.setvoltage(daq,td, 0.0) - NIDAQcard.deletetask(daq,td) - t = NIDAQcard.createtask(daq,"AO",light.channelsAO[2]) - NIDAQcard.setvoltage(daq,t, voltage) - NIDAQcard.deletetask(daq,t) + try + if length(light.channelsDO) >= 12 + td = NIDAQcard.createtask(light.daq, "DO", light.channelsDO[12]) + NIDAQcard.setvoltage(light.daq, td, 0.0) + NIDAQcard.deletetask(light.daq, td) + end + if length(light.channelsAO) >= 2 + t = NIDAQcard.createtask(light.daq, "AO", light.channelsAO[2]) + NIDAQcard.setvoltage(light.daq, t, 0.0) + NIDAQcard.deletetask(light.daq, t) + end + catch e + @warn "VortranLaser shutdown failed: $e" + end end """ From 4fcbee7a5e971b546dca8c2ef1e1ae2fda6e6414 Mon Sep 17 00:00:00 2001 From: kalidke Date: Thu, 29 Jan 2026 14:04:52 -0700 Subject: [PATCH 04/11] Fix camera image permutation to (H, W, N) convention - Add permutedims after reshape in all camera getdata/getlastframe functions to convert from row-major C buffer to column-major Julia (H, W) arrays - Fix array allocations to use (H, W, N) instead of (W, H, N) - Add HDF5 metadata (dimension_order, dimension_labels, memory_layout) to make convention explicit for downstream readers - Document convention in CLAUDE.md Cameras fixed: DCAM4, SimCamera, ThorCamCSC, ThorCamDCx Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 17 +++++++++++++++++ src/h5_file_saving.jl | 7 +++++++ .../dcam4_camera/dcambuf.jl | 15 ++++----------- .../dcam4_camera/interface_methods.jl | 2 +- .../simulated_camera/interface_methods.jl | 2 +- .../thorcam_csc/capture_single_frame.jl | 3 ++- .../thorcam_csc/interface_methods.jl | 14 ++++++-------- .../thorcam_dcx/interface_methods.jl | 9 +++++---- 8 files changed, 43 insertions(+), 26 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5488b74..813930a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,6 +87,23 @@ Hardware implementations use `ccall` for vendor SDKs: - `mcl_stage/*.jl` - Mad City Labs NanoDrive - Serial devices (CrystaLaser, Vortran, Triggerscope) use `LibSerialPort` +### Camera Image Data Convention + +**Convention:** Image data is stored and displayed as column-major `(H, W, N)` arrays where `data[row, col]` = `data[y, x]`. + +**At DLL boundary (getdata):** C SDKs return row-major buffers. Must permute after reshape: +```julia +# WRONG: reshape(buffer, (W, H)) - Julia reads column-first, data is transposed +# CORRECT: permutedims(reshape(buffer, (W, H)), (2, 1)) -> (H, W) +``` + +**Display (CairoMakie heatmap):** `heatmap()` maps dim1→x, dim2→y. For `(H, W)` data: +```julia +heatmap(permutedims(data); axis=(yreversed=true,)) # W→x, H→y, origin top-left +``` + +**Saving (HDF5):** No transform needed if getdata follows convention. Save `(H, W, N)` directly. + ### Work in Progress Some hardware modules are commented out in `MicroscopeControl.jl` while under development: diff --git a/src/h5_file_saving.jl b/src/h5_file_saving.jl index a6b8d57..c99c0de 100644 --- a/src/h5_file_saving.jl +++ b/src/h5_file_saving.jl @@ -39,6 +39,13 @@ function save_group_recursive(h5parent, group_name::String, attributes::Dict, da # Save data if !isnothing(data) write(h5group, "data", data) + # Add dimension convention metadata for image data + if ndims(data) >= 2 + dset = h5group["data"] + attrs(dset)["dimension_order"] = ndims(data) == 2 ? "HW" : "HWN" + attrs(dset)["dimension_labels"] = ndims(data) == 2 ? ["height", "width"] : ["height", "width", "frames"] + attrs(dset)["memory_layout"] = "column_major_julia" + end end # Save attributes diff --git a/src/hardware_implementations/dcam4_camera/dcambuf.jl b/src/hardware_implementations/dcam4_camera/dcambuf.jl index 94f1e55..7d8df9e 100644 --- a/src/hardware_implementations/dcam4_camera/dcambuf.jl +++ b/src/hardware_implementations/dcam4_camera/dcambuf.jl @@ -169,17 +169,10 @@ function dcambuf_getframe(hdcam::Ptr{Cvoid}, iFrame::Int32) return nothing end - data = Array{ElementType}(undef, Int(height), Int(width)) - # If there was padding; but here we assume no padding and use reshape instead - # for y in 1:Int(height) - # src_start = (y-1)*stride_elements + 1 - # src_end = src_start + Int(width) - 1 - # data[y, :] .= temp_buffer[src_start:src_end] - # end - data = reshape(temp_buffer, (Int(width), Int(height))) - - return data - # return permutedims(data, (2,1)) # Transpose for display if we don't use reshape + # Reshape from row-major C buffer and permute to column-major Julia (H, W) + data = permutedims(reshape(temp_buffer, (Int(width), Int(height))), (2, 1)) + + return data end function dcambuf_getlastframe(hdcam::Ptr{Cvoid}) diff --git a/src/hardware_implementations/dcam4_camera/interface_methods.jl b/src/hardware_implementations/dcam4_camera/interface_methods.jl index e77e8ec..d5c16dc 100644 --- a/src/hardware_implementations/dcam4_camera/interface_methods.jl +++ b/src/hardware_implementations/dcam4_camera/interface_methods.jl @@ -186,7 +186,7 @@ function CameraInterface.getdata(camera::DCAM4Camera) if camera.capture_mode == SEQUENCE display("Getting sequence data") im_width, im_height = dcamprop_getsize(camera.camera_handle) - data = zeros(UInt16, im_width, im_height, camera.sequence_length) + data = zeros(UInt16, im_height, im_width, camera.sequence_length) # (H, W, N) convention for i in 1:camera.sequence_length data[:, :, i] = dcambuf_getframe(camera.camera_handle, Int32(i - 1)) end diff --git a/src/hardware_implementations/simulated_camera/interface_methods.jl b/src/hardware_implementations/simulated_camera/interface_methods.jl index 02263f0..9f54275 100644 --- a/src/hardware_implementations/simulated_camera/interface_methods.jl +++ b/src/hardware_implementations/simulated_camera/interface_methods.jl @@ -37,7 +37,7 @@ end function CameraInterface.getdata(camera::SimCamera) if camera.capture_mode == SEQUENCE - return rand(UInt16, camera.sequence_length, camera.roi.height, camera.roi.width) + return rand(UInt16, camera.roi.height, camera.roi.width, camera.sequence_length) # (H, W, N) convention elseif camera.capture_mode == SINGLE_FRAME return rand(UInt16, camera.roi.height, camera.roi.width) elseif camera.capture_mode == LIVE diff --git a/src/hardware_implementations/thorcam_csc/capture_single_frame.jl b/src/hardware_implementations/thorcam_csc/capture_single_frame.jl index 2fd065d..d9907de 100644 --- a/src/hardware_implementations/thorcam_csc/capture_single_frame.jl +++ b/src/hardware_implementations/thorcam_csc/capture_single_frame.jl @@ -48,7 +48,8 @@ function capturesingleframe(exposure_time, operation_mode, frames_per_trigger) #Display Image fig = Figure() - image_single_frame = reshape(single_image, 1440, 1080) + # Reshape and permute to (H, W) convention + image_single_frame = permutedims(reshape(single_image, 1440, 1080), (2, 1)) reinterpret(N0f16, image_single_frame) GLMakie.image(fig[1,1], image_single_frame, interpolate=false) fig diff --git a/src/hardware_implementations/thorcam_csc/interface_methods.jl b/src/hardware_implementations/thorcam_csc/interface_methods.jl index bb663ff..d294eae 100644 --- a/src/hardware_implementations/thorcam_csc/interface_methods.jl +++ b/src/hardware_implementations/thorcam_csc/interface_methods.jl @@ -1,13 +1,12 @@ function CameraInterface.getlastframe(camera::ThorCamCSCCamera) last_frame = ThorCamCSC.getlastframeornothing(camera) #Gets last frame, loop insures that frame is collected - if last_frame == Nothing - return zeros(UInt16, 1440, 1080) #returns all zeros if frame not collected - #return zeros(UInt16, 1080, 1440) - else - last_frame = reshape(last_frame, 1440, 1080) + if last_frame == Nothing + return zeros(UInt16, 1080, 1440) # (H, W) convention + else + # Reshape from row-major C buffer and permute to column-major Julia (H, W) + last_frame = permutedims(reshape(last_frame, 1440, 1080), (2, 1)) return last_frame - #return rotr90(last_frame) end end @@ -97,8 +96,7 @@ end function CameraInterface.getdata(camera::ThorCamCSCCamera) if camera.capture_mode == SEQUENCE - sequence_array = zeros(UInt16, 1440, 1080, camera.sequence_length) - #sequence_array = zeros(UInt16, 1080, 1440, camera.sequence_length) + sequence_array = zeros(UInt16, 1080, 1440, camera.sequence_length) # (H, W, N) convention for i in 1:sequence_frames single_image = CameraInterface.getlastframe(camera) sequence_array[:,:, i] = single_image diff --git a/src/hardware_implementations/thorcam_dcx/interface_methods.jl b/src/hardware_implementations/thorcam_dcx/interface_methods.jl index d442941..8b5cfb9 100644 --- a/src/hardware_implementations/thorcam_dcx/interface_methods.jl +++ b/src/hardware_implementations/thorcam_dcx/interface_methods.jl @@ -100,8 +100,9 @@ function CameraInterface.getlastframe(camera::ThorcamDCXCamera) return end - data = reshape(img_vector, (camera.bytes_pixel,Width, Height)); - data = data[1,:,:] + # Reshape and permute to column-major Julia (H, W) convention + data = reshape(img_vector, (camera.bytes_pixel, Width, Height)) + data = permutedims(data[1,:,:], (2, 1)) return data end @@ -190,12 +191,12 @@ end function CameraInterface.getdata(camera::ThorcamDCXCamera) Width = camera.roi.width Height = camera.roi.height - data = zeros(UInt8, Width, Height, camera.sequence_length) + data = zeros(UInt8, Height, Width, camera.sequence_length) # (H, W, N) convention for i in 1:camera.sequence_length img_vector = zeros(Cchar, Width * Height * camera.bytes_pixel) success = is_CopyImageMem(camera.camera_handle, camera.pImage_Mem[i][], camera.pImage_Id[i][], img_vector) img = reshape(img_vector, (camera.bytes_pixel, Width, Height)) - data[:,:,i] = img[1,:,:] + data[:,:,i] = permutedims(img[1,:,:], (2, 1)) # Permute to (H, W) end # release Memory From 6d01a85679b4f09ed757bb79cdbe7372077c18a4 Mon Sep 17 00:00:00 2001 From: kalidke Date: Thu, 29 Jan 2026 15:41:27 -0700 Subject: [PATCH 05/11] Convert DAQ implementation from NIDAQ.jl to NIDAQmx.jl Replace the deprecated NIDAQ.jl package (v0.6) with the locally developed NIDAQmx.jl package which provides better type safety, error handling, and a more complete API. Key changes: - Update Project.toml dependencies (NIDAQ -> NIDAQmx) - Update channelfunctions to use NIDAQmx API (ai_channels, ao_channels, etc.) - Update taskfunctions to use typed task constructors (AITask, AOTask, etc.) - Enhance createtask() to query device voltage ranges for compatibility - Update setvoltage/readvoltage to use write_scalar/read_scalar with auto_start - Add separate method signatures for AOTask/DOTask and AITask/DITask - Update all consumer modules (CrystaLaser, VortranLaser, TransmissionDaq) Co-Authored-By: Claude Opus 4.5 --- Project.toml | 4 +- .../crysta_laser_561/CrystaLaserControl.jl | 2 +- .../TransmissionDaqControl.jl | 2 +- .../nidaq/NIDAQcard.jl | 2 +- .../nidaq/interface_methods.jl | 215 +++++++++++++----- .../vortran_laser_488/VortranLaserControl.jl | 2 +- 6 files changed, 168 insertions(+), 59 deletions(-) diff --git a/Project.toml b/Project.toml index ec2d9ae..7b169d3 100644 --- a/Project.toml +++ b/Project.toml @@ -14,7 +14,7 @@ ImageView = "86fae568-95e7-573e-a6b2-d8a6b900c9ef" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" LibSerialPort = "a05a14c7-6e3b-5ba9-90a2-45558833e1df" -NIDAQ = "66b72792-1abf-55ab-8064-6e9051317175" +NIDAQmx = "bc903ccc-f951-4f60-9748-ff64248ad6aa" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -27,7 +27,7 @@ HDF5 = "0.17" ImageView = "0.12, 0.13" Images = "0.26" JLD2 = "0.5, 0.6" -NIDAQ = "0.6" +NIDAQmx = "1" Revise = "3" Statistics = "1" julia = "1.10" diff --git a/src/hardware_implementations/crysta_laser_561/CrystaLaserControl.jl b/src/hardware_implementations/crysta_laser_561/CrystaLaserControl.jl index bbe544b..f7421b8 100644 --- a/src/hardware_implementations/crysta_laser_561/CrystaLaserControl.jl +++ b/src/hardware_implementations/crysta_laser_561/CrystaLaserControl.jl @@ -5,7 +5,7 @@ A Module for controlling a laser through a NIDAQ card. """ module CrystaLaserControl -using NIDAQ +using NIDAQmx using ...MicroscopeControl.HardwareInterfaces.LightSourceInterface using ...MicroscopeControl.HardwareImplementations.NIDAQcard diff --git a/src/hardware_implementations/daq_transmission_light/TransmissionDaqControl.jl b/src/hardware_implementations/daq_transmission_light/TransmissionDaqControl.jl index 97242b3..eb27f93 100644 --- a/src/hardware_implementations/daq_transmission_light/TransmissionDaqControl.jl +++ b/src/hardware_implementations/daq_transmission_light/TransmissionDaqControl.jl @@ -6,7 +6,7 @@ Citation: Ali Kazemi Nasaban Shotorban & Sheng Liu, Lidke Lab, UNM """ module TransmissionDaqControl -using NIDAQ +using NIDAQmx using ...MicroscopeControl.HardwareInterfaces.LightSourceInterface using ...MicroscopeControl.HardwareImplementations.NIDAQcard diff --git a/src/hardware_implementations/nidaq/NIDAQcard.jl b/src/hardware_implementations/nidaq/NIDAQcard.jl index c5d15b0..dacd6c1 100644 --- a/src/hardware_implementations/nidaq/NIDAQcard.jl +++ b/src/hardware_implementations/nidaq/NIDAQcard.jl @@ -3,7 +3,7 @@ """ module NIDAQcard -using NIDAQ +using NIDAQmx using ...MicroscopeControl.HardwareInterfaces.DAQInterface import ...MicroscopeControl: export_state, initialize, shutdown diff --git a/src/hardware_implementations/nidaq/interface_methods.jl b/src/hardware_implementations/nidaq/interface_methods.jl index fa80037..1e1ca09 100644 --- a/src/hardware_implementations/nidaq/interface_methods.jl +++ b/src/hardware_implementations/nidaq/interface_methods.jl @@ -5,25 +5,25 @@ A dictionary defines functions of corresponding channel types. `channeltype`: "AI","AO","DI","DO","CI","CO" """ channelfunctions = Dict{String,Function}( - "AI" => analog_input_channels, - "AO" => analog_output_channels, - "DI" => digital_input_channels, - "DO" => digital_output_channels, - "CI" => counter_input_channels, - "CO" => counter_output_channels, + "AI" => NIDAQmx.ai_channels, + "AO" => NIDAQmx.ao_channels, + "DI" => NIDAQmx.di_lines, + "DO" => NIDAQmx.do_lines, + "CI" => NIDAQmx.ci_channels, + "CO" => NIDAQmx.co_channels, ) """ taskfunctions -A dictionary defines functions of corresponding task types. +A dictionary defines types of corresponding task types. `tasktype`: "AI","AO","DI","DO" """ -taskfunctions = Dict{String,Function}( - "AI" => analog_input, - "AO" => analog_output, - "DI" => digital_input, - "DO" => digital_output, +taskfunctions = Dict{String,Type}( + "AI" => NIDAQmx.AITask, + "AO" => NIDAQmx.AOTask, + "DI" => NIDAQmx.DITask, + "DO" => NIDAQmx.DOTask, ) """ @@ -35,13 +35,13 @@ List available devices. - `daq::NIdaq`: A NIdaq type. """ function DAQInterface.showdevices(daq::NIdaq) - devices() + NIDAQmx.device_names() end """ showchannels(daq::NIdaq,channeltype::String,device::String) -List available channels of a given channel type and device. +List available channels of a given channel type and device. # Arguments - `daq::NIdaq`: A NIdaq type. @@ -63,83 +63,192 @@ Create a task of a given task type and channel. - `channel::String`: A channel name, obtained from `showchannels`. # Returns -- `t::NIDAQ.Task`: A NIDAQ.Task type. +- `t::NIDAQmx.Task`: A NIDAQmx Task type. """ -function DAQInterface.createtask(daq::NIdaq,tasktype::String,channel::String) - t = taskfunctions[tasktype](channel) - return t +function DAQInterface.createtask(daq::NIdaq, tasktype::String, channel::String) + # Extract device name from channel (e.g., "Dev1/ao0" -> "Dev1") + device = String(split(channel, "/")[1]) + + if tasktype == "AI" + # Query device's AI voltage range + ranges = NIDAQmx.ai_voltage_ranges(device) + if isempty(ranges) + # Use default range if none available + return NIDAQmx.AITask(channel) + end + # Use the widest range (last row) + min_val, max_val = ranges[end, :] + task = NIDAQmx.AITask() + NIDAQmx.add_ai_voltage!(task, channel; min_val=min_val, max_val=max_val) + return task + elseif tasktype == "AO" + # Query device's AO voltage range + ranges = NIDAQmx.ao_voltage_ranges(device) + if isempty(ranges) + # Use default range if none available + return NIDAQmx.AOTask(channel) + end + # Use the first (typically only) range + min_val, max_val = ranges[1, :] + task = NIDAQmx.AOTask() + NIDAQmx.add_ao_voltage!(task, channel; min_val=min_val, max_val=max_val) + return task + elseif tasktype == "DI" + return NIDAQmx.DITask(channel) + elseif tasktype == "DO" + return NIDAQmx.DOTask(channel) + else + error("Unknown task type: $tasktype") + end end -function DAQInterface.addchannel!(daq::NIdaq,t::NIDAQ.Task,tasktype::String, channel::String) - taskfunctions[tasktype](t,channel) +""" + addchannel!(daq::NIdaq, t::NIDAQmx.Task, tasktype::String, channel::String) + +Add a channel to an existing task. + +# Arguments +- `daq::NIdaq`: A NIdaq type. +- `t::NIDAQmx.Task`: A NIDAQmx Task. +- `tasktype::String`: A task type. Options are "AI","AO","DI","DO". +- `channel::String`: A channel name, obtained from `showchannels`. +""" +function DAQInterface.addchannel!(daq::NIdaq, t::NIDAQmx.Task, tasktype::String, channel::String) + if tasktype == "AI" + NIDAQmx.add_ai_voltage!(t, channel) + elseif tasktype == "AO" + NIDAQmx.add_ao_voltage!(t, channel) + elseif tasktype == "DI" + NIDAQmx.add_di_chan!(t, channel) + elseif tasktype == "DO" + NIDAQmx.add_do_chan!(t, channel) + else + error("Unknown task type: $tasktype") + end return nothing end """ - setvoltage(daq::NIdaq,t::NIDAQ.Task,voltage::Float64) + setvoltage(daq::NIdaq,t::NIDAQmx.AOTask,voltage::Float64) -Set the voltage of a output task. +Set the voltage of an analog output task. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQ.Task`: A NIDAQ.Task type. -- `voltage::Float64`: The voltage to set the task to, unit: volt. If tasktype is "DO", the voltage should be 0 or 1. +- `t::NIDAQmx.AOTask`: A NIDAQmx AOTask type. +- `voltage::Float64`: The voltage to set the task to, unit: volt. # Returns - `ret::Int`: The number of samples written to the task. """ -function DAQInterface.setvoltage(daq::NIdaq,t::NIDAQ.Task,voltage::Float64) - if typeof(t) == NIDAQ.DOTask - voltage = UInt8(voltage) - end - start(t) - ret = NIDAQ.write(t,[voltage]) - stop(t) - return ret +function DAQInterface.setvoltage(daq::NIdaq, t::NIDAQmx.AOTask, voltage::Float64) + NIDAQmx.write_scalar(t, voltage; auto_start=true) + return 1 end -function DAQInterface.setvoltage(daq::NIdaq,t::NIDAQ.Task,voltage::Array{Float64}) - if typeof(t) == NIDAQ.DOTask - voltage = UInt8.(voltage) - end - start(t) - ret = NIDAQ.write(t,voltage) - stop(t) - return ret +""" + setvoltage(daq::NIdaq,t::NIDAQmx.DOTask,voltage::Float64) + +Set the voltage of a digital output task. + +# Arguments +- `daq::NIdaq`: A NIdaq type. +- `t::NIDAQmx.DOTask`: A NIDAQmx DOTask type. +- `voltage::Float64`: The voltage to set (0 or 1). + +# Returns +- `ret::Int`: The number of samples written to the task. +""" +function DAQInterface.setvoltage(daq::NIdaq, t::NIDAQmx.DOTask, voltage::Float64) + NIDAQmx.write_scalar(t, UInt32(voltage); auto_start=true) + return 1 end """ - readvoltage(daq::NIdaq,t::NIDAQ.Task) + setvoltage(daq::NIdaq,t::NIDAQmx.AOTask,voltage::Array{Float64}) -Read the voltage of a input task. +Set the voltage of an analog output task with an array of values. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQ.Task`: A NIDAQ.Task type. +- `t::NIDAQmx.AOTask`: A NIDAQmx AOTask type. +- `voltage::Array{Float64}`: The voltages to set, unit: volt. + +# Returns +- `ret::Int`: The number of samples written to the task. +""" +function DAQInterface.setvoltage(daq::NIdaq, t::NIDAQmx.AOTask, voltage::Array{Float64}) + write(t, voltage; auto_start=true) +end + +""" + setvoltage(daq::NIdaq,t::NIDAQmx.DOTask,voltage::Array{Float64}) + +Set the voltage of a digital output task with an array of values. + +# Arguments +- `daq::NIdaq`: A NIdaq type. +- `t::NIDAQmx.DOTask`: A NIDAQmx DOTask type. +- `voltage::Array{Float64}`: The voltages to set (0 or 1). + +# Returns +- `ret::Int`: The number of samples written to the task. +""" +function DAQInterface.setvoltage(daq::NIdaq, t::NIDAQmx.DOTask, voltage::Array{Float64}) + write(t, UInt8.(voltage); auto_start=true) +end + +""" + readvoltage(daq::NIdaq,t::NIDAQmx.AITask) + +Read the voltage of an analog input task. + +# Arguments +- `daq::NIdaq`: A NIdaq type. +- `t::NIDAQmx.AITask`: A NIDAQmx AITask type. # Returns - `voltage::Float64`: The voltage read from the task, unit: volt. """ -function DAQInterface.readvoltage(daq::NIdaq,t::NIDAQ.Task) - start(t) - voltage = NIDAQ.read(t) - stop(t) +function DAQInterface.readvoltage(daq::NIdaq, t::NIDAQmx.AITask) + NIDAQmx.start!(t) + voltage = NIDAQmx.read_scalar(t) + NIDAQmx.stop!(t) + return voltage +end + +""" + readvoltage(daq::NIdaq,t::NIDAQmx.DITask) + +Read the voltage of a digital input task. + +# Arguments +- `daq::NIdaq`: A NIdaq type. +- `t::NIDAQmx.DITask`: A NIDAQmx DITask type. + +# Returns +- `voltage::Float64`: The voltage read from the task (0 or 1). +""" +function DAQInterface.readvoltage(daq::NIdaq, t::NIDAQmx.DITask) + NIDAQmx.start!(t) + voltage = Float64(NIDAQmx.read_scalar(t)) + NIDAQmx.stop!(t) return voltage end """ - deletetask(daq::NIdaq,t::NIDAQ.Task) + deletetask(daq::NIdaq,t::NIDAQmx.Task) Delete a task. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQ.Task`: A NIDAQ.Task type. +- `t::NIDAQmx.Task`: A NIDAQmx Task type. """ -function DAQInterface.deletetask(daq::NIdaq,t::NIDAQ.Task) - clear(t) -end +function DAQInterface.deletetask(daq::NIdaq, t::NIDAQmx.Task) + NIDAQmx.clear!(t) +end """ export_state(daq::NIdaq) @@ -149,4 +258,4 @@ function export_state(daq::NIdaq) data = nothing children = Dict() return attributes, data, children -end \ No newline at end of file +end diff --git a/src/hardware_implementations/vortran_laser_488/VortranLaserControl.jl b/src/hardware_implementations/vortran_laser_488/VortranLaserControl.jl index f927b4e..3596489 100644 --- a/src/hardware_implementations/vortran_laser_488/VortranLaserControl.jl +++ b/src/hardware_implementations/vortran_laser_488/VortranLaserControl.jl @@ -5,7 +5,7 @@ A Module for controlling a laser through a NIDAQ card. """ module VortranLaserControl -using NIDAQ +using NIDAQmx using ...MicroscopeControl import ...MicroscopeControl: export_state, initialize, shutdown From 71257455297442bd22d9543dbb2fc00372a82fd7 Mon Sep 17 00:00:00 2001 From: kalidke Date: Thu, 29 Jan 2026 16:44:06 -0700 Subject: [PATCH 06/11] Fix ThorCam mono mode, Makie deprecations, and camera display - ThorCam DCX: Use IS_CM_MONO8 instead of RGBA8 for grayscale camera - ThorCam DCX: Change buffer type from Cchar to UInt8 for proper 0-255 range - Fix Makie deprecation: resolution -> size in all GUI files - Fix camera display permutation: (H,W) data needs permutedims for Makie - Add exposure time tolerance check to avoid spurious warnings - Update CLAUDE.md with image()/heatmap() display documentation Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 4 +- .../dcam4_camera/collect_sequence.jl | 18 ++--- .../dcam4_camera/dcam_helpers.jl | 3 +- .../dcam4_camera/gui_param.jl | 2 +- src/hardware_implementations/pi_n472/gui.jl | 2 +- .../thorcam_dcx/interface_methods.jl | 29 ++++---- .../camera_interface/gui.jl | 71 +++++++++---------- src/hardware_interfaces/daq_interface/gui.jl | 2 +- .../lightsource_interface/gui.jl | 2 +- .../objective_positioner_interface/gui.jl | 2 +- .../stage_interface/gui.jl | 6 +- .../triggerscope_interface/oldgui.jl | 2 +- 12 files changed, 71 insertions(+), 72 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 813930a..8de8087 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,8 +97,10 @@ Hardware implementations use `ccall` for vendor SDKs: # CORRECT: permutedims(reshape(buffer, (W, H)), (2, 1)) -> (H, W) ``` -**Display (CairoMakie heatmap):** `heatmap()` maps dim1→x, dim2→y. For `(H, W)` data: +**Display (Makie image/heatmap):** Both `image()` and `heatmap()` map dim1→x, dim2→y. For `(H, W)` data: ```julia +# Both work the same way: +image(permutedims(data); axis=(yreversed=true,)) # W→x, H→y, origin top-left heatmap(permutedims(data); axis=(yreversed=true,)) # W→x, H→y, origin top-left ``` diff --git a/src/hardware_implementations/dcam4_camera/collect_sequence.jl b/src/hardware_implementations/dcam4_camera/collect_sequence.jl index d17e350..6cb8093 100644 --- a/src/hardware_implementations/dcam4_camera/collect_sequence.jl +++ b/src/hardware_implementations/dcam4_camera/collect_sequence.jl @@ -40,16 +40,17 @@ function collect_sequence(nframes::Int=100; timeout_milisec::Int32=Int32(1000)) end #Setup display - img = Observable(rand(Gray{N0f8}, im_height, im_width)) - imgplot = image(@lift(rotr90($img)), - axis=(aspect=DataAspect(),), - figure=(figure_padding=0, resolution=size(img[]) ./ 2)) + # Camera data is (H, W), Makie needs (W, H) with yreversed for proper image display + img = Observable(rand(Gray{N0f8}, im_width, im_height)) + imgplot = image(img, + axis=(aspect=DataAspect(), yreversed=true), + figure=(figure_padding=0, size=(im_width, im_height) .÷ 2)) # Create an Observable for the text text_data = Observable("Initial Text") neon_green = RGB(0.2, 1, 0.2) - # Add the Observable text to the plot - text!(imgplot.axis, text_data, position=(im_width / 10, im_height * 9/10), color = neon_green, textsize = 50) + # Add the Observable text to the plot (position adjusted for yreversed) + text!(imgplot.axis, text_data, position=(im_width / 10, im_height / 10), color = neon_green, fontsize = 50) hidedecorations!(imgplot.axis) display(imgplot) @@ -76,10 +77,11 @@ function collect_sequence(nframes::Int=100; timeout_milisec::Int32=Int32(1000)) return err end - framedata = dcambuf_getlastframe(hdcam) + framedata = dcambuf_getlastframe(hdcam) # Returns (H, W) max_val = maximum(framedata) min_val = minimum(framedata) - img[] = Gray{N0f8}.(framedata .* (1 / max_val)) + # Permute from (H, W) to (W, H) for Makie display + img[] = Gray{N0f8}.(permutedims(framedata) .* (1 / max_val)) text_data[] = "[$min_val $max_val]" sleep(1 / fps) diff --git a/src/hardware_implementations/dcam4_camera/dcam_helpers.jl b/src/hardware_implementations/dcam4_camera/dcam_helpers.jl index 32efb70..0c4393a 100644 --- a/src/hardware_implementations/dcam4_camera/dcam_helpers.jl +++ b/src/hardware_implementations/dcam4_camera/dcam_helpers.jl @@ -94,7 +94,8 @@ function setexposuretime!(camera::DCAM4Camera) camera.last_error = err end err, value = dcamprop_getvalue(hdcam, DCAM_IDPROP_EXPOSURETIME) - if exposure_time != value + # Only warn if difference is significant (>1% or >1ms) + if !isapprox(exposure_time, value; rtol=0.01, atol=0.001) @warn "Exposure time set to $(value) instead of $(exposure_time)" end camera.exposure_time = value diff --git a/src/hardware_implementations/dcam4_camera/gui_param.jl b/src/hardware_implementations/dcam4_camera/gui_param.jl index e12bc6d..43049fa 100644 --- a/src/hardware_implementations/dcam4_camera/gui_param.jl +++ b/src/hardware_implementations/dcam4_camera/gui_param.jl @@ -1,6 +1,6 @@ function gui_param(camera::DCAM4Camera) - prop_fig = GLMakie.Figure(resolution=(900, 1800), title="Camera Properties") + prop_fig = GLMakie.Figure(size=(900, 1800), title="Camera Properties") wd = 200 wd1 = 300 wd2 = 130 diff --git a/src/hardware_implementations/pi_n472/gui.jl b/src/hardware_implementations/pi_n472/gui.jl index c70e685..f4444a5 100644 --- a/src/hardware_implementations/pi_n472/gui.jl +++ b/src/hardware_implementations/pi_n472/gui.jl @@ -1,5 +1,5 @@ function gui(stage::N472) - gui_fig = Figure(resolution=(600, 400)) #This is the Stage GUI Figure + gui_fig = Figure(size=(600, 400)) #This is the Stage GUI Figure # create buttons to control individual axis gui_fig[2,1] = buttongrid1 = GridLayout(3,3,halign=:left) diff --git a/src/hardware_implementations/thorcam_dcx/interface_methods.jl b/src/hardware_implementations/thorcam_dcx/interface_methods.jl index 8b5cfb9..cb7922b 100644 --- a/src/hardware_implementations/thorcam_dcx/interface_methods.jl +++ b/src/hardware_implementations/thorcam_dcx/interface_methods.jl @@ -31,14 +31,15 @@ function initialize(camera::ThorcamDCXCamera) camera.camera_format = CameraFormat(maxWidth, maxHeight, pixelSize, 1, "CMOS") - bitsPixel_ptr = zeros(INT, 1) - colorMode_ptr = zeros(INT, 1) - success = is_GetColorDepth(hcam, bitsPixel_ptr, colorMode_ptr) - - camera.bits_pixel = bitsPixel_ptr[1] - camera.bytes_pixel = INT(bitsPixel_ptr[1] / 8) + # Set to 8-bit mono mode for grayscale camera + success = is_SetColorMode(hcam, IS_CM_MONO8) + if success != IS_SUCCESS + @error("Failed to set color mode to MONO8") + end - success = is_SetColorMode(hcam, IS_CM_RGBA8_PACKED) + camera.bits_pixel = 8 + camera.bytes_pixel = 1 + println("Color mode set to MONO8: bits_pixel=", camera.bits_pixel, ", bytes_pixel=", camera.bytes_pixel) pixel_clock_range = zeros(UINT,3) success = is_PixelClock(hcam,IS_PIXELCLOCK_CMD_GET_RANGE,pixel_clock_range,sizeof(pixel_clock_range)) @@ -93,16 +94,16 @@ function CameraInterface.getlastframe(camera::ThorcamDCXCamera) Width = INT(camera.roi.width) Height = INT(camera.roi.height) - img_vector = zeros(Cchar, Width * Height * camera.bytes_pixel); - success = is_CopyImageMem(camera.camera_handle,camera.pImage_Mem[1][], camera.pImage_Id[1][], img_vector) + img_vector = zeros(UInt8, Width * Height) + success = is_CopyImageMem(camera.camera_handle, camera.pImage_Mem[1][], camera.pImage_Id[1][], img_vector) if success != IS_SUCCESS @error("Failed to copy image memory") return end # Reshape and permute to column-major Julia (H, W) convention - data = reshape(img_vector, (camera.bytes_pixel, Width, Height)) - data = permutedims(data[1,:,:], (2, 1)) + # SDK returns row-major (W, H), permute to (H, W) + data = permutedims(reshape(img_vector, (Width, Height)), (2, 1)) return data end @@ -193,10 +194,10 @@ function CameraInterface.getdata(camera::ThorcamDCXCamera) Height = camera.roi.height data = zeros(UInt8, Height, Width, camera.sequence_length) # (H, W, N) convention for i in 1:camera.sequence_length - img_vector = zeros(Cchar, Width * Height * camera.bytes_pixel) + img_vector = zeros(UInt8, Width * Height) success = is_CopyImageMem(camera.camera_handle, camera.pImage_Mem[i][], camera.pImage_Id[i][], img_vector) - img = reshape(img_vector, (camera.bytes_pixel, Width, Height)) - data[:,:,i] = permutedims(img[1,:,:], (2, 1)) # Permute to (H, W) + # SDK returns row-major (W, H), permute to (H, W) + data[:,:,i] = permutedims(reshape(img_vector, (Width, Height)), (2, 1)) end # release Memory diff --git a/src/hardware_interfaces/camera_interface/gui.jl b/src/hardware_interfaces/camera_interface/gui.jl index 50c080e..2411eb9 100644 --- a/src/hardware_interfaces/camera_interface/gui.jl +++ b/src/hardware_interfaces/camera_interface/gui.jl @@ -27,52 +27,46 @@ end function create_display(camera, display_function) # Setup display + # Camera data is (H, W) where data[row, col] = data[y, x] + # Makie image() maps first dim → x, second dim → y + # So we need permutedims to get (W, H) and yreversed for top-left origin im_height = camera.roi.height im_width = camera.roi.width println("Creating display with dimensions: $im_width x $im_height") + + # Create observable with (W, H) layout for Makie img = Observable(rand(Gray{N0f8}, im_width, im_height)) imgplot = image(img, - axis=(aspect=DataAspect(),), - figure=(figure_padding=0, resolution=(im_width, im_height)), interpolate=false) + axis=(aspect=DataAspect(), yreversed=true), + figure=(figure_padding=0, size=(im_width, im_height)), interpolate=false) # Create an Observable for the text text_data = Observable("Initial Text") neon_green = RGB(0.2, 1, 0.2) - # Add the Observable text to the plot - text!(imgplot.axis, text_data, position=(im_width / 10, im_height * 9 / 10), color=neon_green, fontsize=50) + # Add the Observable text to the plot (position in data coords, adjusted for yreversed) + text!(imgplot.axis, text_data, position=(im_width / 10, im_height / 10), color=neon_green, fontsize=50) hidedecorations!(imgplot.axis) # Display the plot and store the Scene scene = display(GLMakie.Screen(), imgplot) - # Add a listener to the `window_open` Observable - # on(events(scene).window_open) do is_open - # if !is_open - # println("The window has been closed!") - # # Add your callback logic here - # end - # end - # Start live imaging or sequence based on display_function - #@sync begin - #display_function(camera) - fps = 60 - display_function(camera) - - @async begin - while camera.is_running == 1 - - framedata = getlastframe(camera) - max_val = maximum(framedata) - min_val = minimum(framedata) - img[] = Gray{N0f8}.(framedata .* (1 / max_val)) - text_data[] = "[$min_val $max_val]" - sleep(1 / fps) - end + fps = 60 + display_function(camera) + + @async begin + while camera.is_running == 1 + framedata = getlastframe(camera) # Returns (H, W) + max_val = maximum(framedata) + min_val = minimum(framedata) + # Permute from (H, W) to (W, H) for Makie display + img[] = Gray{N0f8}.(permutedims(framedata) .* (1 / max_val)) + text_data[] = "[$min_val $max_val]" + sleep(1 / fps) end - #end + end end """ @@ -126,26 +120,25 @@ end function start_capture(camera::Camera) # Start a capture - println(typeof(camera)) - framedata = capture(camera) + framedata = capture(camera) # Returns (H, W) - # Create an Observable for the text max_val = maximum(framedata) min_val = minimum(framedata) - img = Gray{N0f8}.(framedata .* (1 / max_val)) + # Permute from (H, W) to (W, H) for Makie display + img = Gray{N0f8}.(permutedims(framedata) .* (1 / max_val)) - imgplot = image(rotr90(img), - axis=(aspect=DataAspect(),), - figure=(figure_padding=0, resolution=size(img))) - - # Add [min max] text to image - text_data = "[$min_val $max_val]" im_height = camera.roi.height im_width = camera.roi.width + imgplot = image(img, + axis=(aspect=DataAspect(), yreversed=true), + figure=(figure_padding=0, size=(im_width, im_height))) + + # Add [min max] text to image (position adjusted for yreversed) + text_data = "[$min_val $max_val]" neon_green = RGB(0.2, 1, 0.2) - text!(imgplot.axis, text_data, position=(im_width / 10, im_height * 9 / 10), color=neon_green, fontsize=50) + text!(imgplot.axis, text_data, position=(im_width / 10, im_height / 10), color=neon_green, fontsize=50) hidedecorations!(imgplot.axis) display(GLMakie.Screen(), imgplot) diff --git a/src/hardware_interfaces/daq_interface/gui.jl b/src/hardware_interfaces/daq_interface/gui.jl index 1a6b7cd..1358eb6 100644 --- a/src/hardware_interfaces/daq_interface/gui.jl +++ b/src/hardware_interfaces/daq_interface/gui.jl @@ -10,7 +10,7 @@ function gui(daq::DAQ) println(typeof(daq)) # Create the figure for the control window - control_fig = Figure(resolution = (900,400)) + control_fig = Figure(size = (900,400)) # create a dropdown menu for the devices devs = showdevices(daq) diff --git a/src/hardware_interfaces/lightsource_interface/gui.jl b/src/hardware_interfaces/lightsource_interface/gui.jl index 588996e..52d1a71 100644 --- a/src/hardware_interfaces/lightsource_interface/gui.jl +++ b/src/hardware_interfaces/lightsource_interface/gui.jl @@ -10,7 +10,7 @@ function gui(light::LightSource) println(typeof(light)) # Create the figure for the control window - control_fig = Figure(resolution = (700,300), title = light.unique_id) + control_fig = Figure(size = (700,300), title = light.unique_id) # Buttons and actions # create a slider for the power diff --git a/src/hardware_interfaces/objective_positioner_interface/gui.jl b/src/hardware_interfaces/objective_positioner_interface/gui.jl index 5803d99..9bfe98f 100644 --- a/src/hardware_interfaces/objective_positioner_interface/gui.jl +++ b/src/hardware_interfaces/objective_positioner_interface/gui.jl @@ -8,7 +8,7 @@ function gui(positioner::Zpositioner) return end - fig = Figure(resolution=(600, 400)) + fig = Figure(size=(600, 400)) # Set position observables targ_pos = Observable(0.0) diff --git a/src/hardware_interfaces/stage_interface/gui.jl b/src/hardware_interfaces/stage_interface/gui.jl index c6f916a..4109ee7 100644 --- a/src/hardware_interfaces/stage_interface/gui.jl +++ b/src/hardware_interfaces/stage_interface/gui.jl @@ -35,7 +35,7 @@ end function gui1d(stage::Stage) #First we will create a figure to hold the GUI elements - stage_gui_fig = Figure(resolution=(600, 400)) #This is the Stage GUI Figure + stage_gui_fig = Figure(size=(600, 400)) #This is the Stage GUI Figure #Set up variables for motion that will be changed xstepsize = Observable(0.1) @@ -258,7 +258,7 @@ end function gui2d(stage::Stage) #First we will create a figure to hold the GUI elements - stage_gui_fig = Figure(resolution=(1200, 600)) #This is the Stage GUI Figure + stage_gui_fig = Figure(size=(1200, 600)) #This is the Stage GUI Figure #= This function should flow as follows @@ -553,7 +553,7 @@ end function gui3d(stage::Stage) #First we will create a figure to hold the GUI elements - stage_gui_fig = Figure(resolution=(1200, 600)) #This is the Stage GUI Figure + stage_gui_fig = Figure(size=(1200, 600)) #This is the Stage GUI Figure #Set up variables for motion that will be changed xstepsize = Observable(0.1) diff --git a/src/hardware_interfaces/triggerscope_interface/oldgui.jl b/src/hardware_interfaces/triggerscope_interface/oldgui.jl index 1358013..67b2478 100644 --- a/src/hardware_interfaces/triggerscope_interface/oldgui.jl +++ b/src/hardware_interfaces/triggerscope_interface/oldgui.jl @@ -10,7 +10,7 @@ function oldgui(daq::DAQ) println(typeof(daq)) # Create the figure for the control window - control_fig = Figure(resolution = (900,400)) + control_fig = Figure(size = (900,400)) # create a dropdown menu for the devices devs = showdevices(daq) From 041cca9367ef9d1a37cc2aaaa62216291566a09c Mon Sep 17 00:00:00 2001 From: kalidke Date: Thu, 29 Jan 2026 21:08:50 -0700 Subject: [PATCH 07/11] Refactor export system using Reexport.jl Replace manual export propagation with Reexport.jl for cleaner module structure. MicroscopeControl.jl reduced from 79 to 31 lines by removing 50+ manual exports and 15+ using statements. Changes: - Add Reexport.jl dependency - HardwareInterfaces.jl: @reexport all interface modules - HardwareImplementations.jl: @reexport all implementation modules - MicroscopeControl.jl: @reexport both layers, remove manual exports Adding new hardware now requires edits in only one place instead of three. Co-Authored-By: Claude Opus 4.5 --- Project.toml | 6 +- src/MicroscopeControl.jl | 77 ++++--------------- .../HardwareImplementations.jl | 53 +++++++++++-- src/hardware_interfaces/HardwareInterfaces.jl | 22 +++++- 4 files changed, 86 insertions(+), 72 deletions(-) diff --git a/Project.toml b/Project.toml index ec2d9ae..3fcf771 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "MicroscopeControl" uuid = "aa70d9ae-4a1e-49fd-870a-8ccfd99f4c3e" -authors = ["klidke@unm.edu"] version = "1.0.0-DEV" +authors = ["klidke@unm.edu"] [deps] CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82" @@ -15,12 +15,13 @@ Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" LibSerialPort = "a05a14c7-6e3b-5ba9-90a2-45558833e1df" NIDAQ = "66b72792-1abf-55ab-8064-6e9051317175" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] -CairoMakie = "0.12, 0.13, 0.14, 0.15" CEnum = "0.5.0" +CairoMakie = "0.12, 0.13, 0.14, 0.15" FFTW = "1" GLMakie = "0.10, 0.11, 0.12, 0.13" HDF5 = "0.17" @@ -28,6 +29,7 @@ ImageView = "0.12, 0.13" Images = "0.26" JLD2 = "0.5, 0.6" NIDAQ = "0.6" +Reexport = "1.2.2" Revise = "3" Statistics = "1" julia = "1.10" diff --git a/src/MicroscopeControl.jl b/src/MicroscopeControl.jl index 929c0f2..34bfccc 100644 --- a/src/MicroscopeControl.jl +++ b/src/MicroscopeControl.jl @@ -1,78 +1,31 @@ """ -MicroscopeControl is the main module for the MicroscopeControl package. It exports all the abstract types and methods for the instrument, as well as the implementations for the hardware interfaces. It also exports the methods for saving data to an HDF5 file, and the methods for controlling the camera, stage, light source, DAQ, and FPGA. Finally, it re-exports the GUI method for the user interface. +MicroscopeControl is the main module for the MicroscopeControl package. +It exports all abstract types and methods for instruments, as well as the +implementations for all hardware interfaces. It also provides methods for +saving data to HDF5 files, and the GUI methods for user interfaces. + +Uses Reexport.jl to cleanly propagate exports from submodules. """ module MicroscopeControl using HDF5 +using Reexport -# To export the abstract types and methods for all the instruments +# Core instrument abstraction include("instrument.jl") export AbstractInstrument export export_state, initialize, shutdown -# Including the hardware interfaces and implementations and the HDF5 file saving methods +# Hardware interfaces (abstract types + method signatures) include("hardware_interfaces/HardwareInterfaces.jl") -include("hardware_implementations/HardwareImplementations.jl") -include("h5_file_saving.jl") - -using .HardwareImplementations.SimulatedCamera -using .HardwareImplementations.DCAM4 -using .HardwareImplementations.PI -using .HardwareImplementations.SimulatedStage -using .HardwareImplementations.MadCityLabs -using .HardwareImplementations.PI_N472 -using .HardwareImplementations.ThorCamCSC -using .HardwareImplementations.SimulatedLight -using .HardwareImplementations.NIDAQcard -using .HardwareImplementations.TCubeLaserControl -using .HardwareImplementations.TransmissionDaqControl -using .HardwareImplementations.CrystaLaserControl -using .HardwareImplementations.VortranLaserControl -using .HardwareImplementations.OK_XEM -using .HardwareImplementations.ThorCamDCx -# using .HardwareImplementations.Triggerscope -# using .HardwareImplementations.MCLMicroPositioner +@reexport using .HardwareInterfaces -# # Export all HardwareImplementations modules -# export DCAM4, SimulatedCamera, SimulatedStage, PI, MadCityLabs, PI_N472, ThorCamCSC -# export SimulatedLight, NIDAQcard, TCubeLaserControl, TransmissionDaqControl -# export CrystaLaserControl, VortranLaserControl, OK_XEM +# Hardware implementations (concrete device drivers) +include("hardware_implementations/HardwareImplementations.jl") +@reexport using .HardwareImplementations -# Export h5 file saving methods +# HDF5 file saving utilities +include("h5_file_saving.jl") export save_h5, save_attributes_and_data -# Re-export camera implementations -export SimCamera, DCAM4Camera, ThorCamCSCCamera, ThorCamDCXCamera -export start_sequence, start_live -export getlastframe, capture, live, sequence, abort, getdata -export setexposuretime, settriggermode, setroi!, setexposuretime! -export dcamprop_getvalue, DCAM_IDPROP_INTERNALFRAMERATE, CameraROI, dcamapi_uninit - -# Re-export stage implementations -export PIStage, MCLStage, SimStage, N472 -export move, getposition, stopmotion, getrange -export setvel, reference, servoxy, movexy, servo -export move_to_z, get_z_position - -# Re-export light source implementations -export SimLight, TCubeLaser, DaqTrLight, CrystaLaser, VortranLaser -export light_on, light_off, setpower - -# Re-export DAQ implementations -export NIdaq -export showdevices, showchannels, createtask, setvoltage, readvoltage, deletetask - -# Re-export FPGA implementations -export XEM -export setexposure, enable, setupIO - -# Re-export triggerscope implementations -# export Triggerscope4 - -#re-export objective positioner implementations -# export MclZPositioner - -# Re-export common GUI methods -export gui - end diff --git a/src/hardware_implementations/HardwareImplementations.jl b/src/hardware_implementations/HardwareImplementations.jl index 2281ed9..3adaa2c 100644 --- a/src/hardware_implementations/HardwareImplementations.jl +++ b/src/hardware_implementations/HardwareImplementations.jl @@ -1,28 +1,71 @@ """ -HardwareImplementations module is a container for all hardware implementations +HardwareImplementations module is a container for all hardware implementations. +Uses Reexport.jl to automatically propagate exports from submodules. """ module HardwareImplementations +using Reexport using ..MicroscopeControl +# Camera implementations include("simulated_camera/SimulatedCamera.jl") +@reexport using .SimulatedCamera + include("dcam4_camera/DCAM4.jl") +@reexport using .DCAM4 + +include("thorcam_csc/ThorCamCSC.jl") +@reexport using .ThorCamCSC + +include("thorcam_dcx/ThorCamDCx.jl") +@reexport using .ThorCamDCx + +# Stage implementations include("pi_stage/PI.jl") +@reexport using .PI + include("simulated_stage/SimulatedStage.jl") +@reexport using .SimulatedStage + include("mcl_stage/MadCityLabs.jl") +@reexport using .MadCityLabs + include("pi_N472/PI_N472.jl") +@reexport using .PI_N472 -include("thorcam_csc/ThorCamCSC.jl") -include("thorcam_dcx/ThorCamDCx.jl") +# Light source implementations include("simulated_light/SimulatedLight.jl") -include("nidaq/NIDAQcard.jl") +@reexport using .SimulatedLight + include("tcube_laser/TCubeLaserControl.jl") +@reexport using .TCubeLaserControl + include("daq_transmission_light/TransmissionDaqControl.jl") +@reexport using .TransmissionDaqControl + include("crysta_laser_561/CrystaLaserControl.jl") +@reexport using .CrystaLaserControl + include("vortran_laser_488/VortranLaserControl.jl") +@reexport using .VortranLaserControl + +# DAQ implementation (must come before OK_XEM which depends on it) +include("nidaq/NIDAQcard.jl") +@reexport using .NIDAQcard + +# FPGA implementation (depends on NIDAQcard) include("ok_xem/OK_XEM.jl") +@reexport using .OK_XEM + +# SLM implementation include("meadowlark_slm/Meadowlark.jl") +@reexport using .Meadowlark + +# Work in progress - uncomment when ready # include("triggerscope/Triggerscope.jl") +# @reexport using .Triggerscope + # include("mcl_micro_positioner/MCLMicroPositioner.jl") +# @reexport using .MCLMicroPositioner -end \ No newline at end of file +end diff --git a/src/hardware_interfaces/HardwareInterfaces.jl b/src/hardware_interfaces/HardwareInterfaces.jl index 2b41b1e..b8f99fd 100644 --- a/src/hardware_interfaces/HardwareInterfaces.jl +++ b/src/hardware_interfaces/HardwareInterfaces.jl @@ -1,20 +1,36 @@ - """ -This module is a container for all hardware interfaces +This module is a container for all hardware interfaces. +Uses Reexport to automatically propagate exports from submodules. """ module HardwareInterfaces using ..MicroscopeControl import ..MicroscopeControl: AbstractInstrument -# import ..MicroscopeControl: export_state, initialize, shutdown +using Reexport +# Include and re-export all interface modules +# Each submodule exports its abstract types and generic function signatures include("camera_interface/CameraInterface.jl") +@reexport using .CameraInterface + include("slm_interface/SLMInterface.jl") +@reexport using .SLMInterface + include("stage_interface/StageInterface.jl") +@reexport using .StageInterface include("lightsource_interface/LightSourceInterface.jl") +@reexport using .LightSourceInterface + include("daq_interface/DAQInterface.jl") +@reexport using .DAQInterface + +# Commented out - not currently in use # include("triggerscope_interface/TrigInterface.jl") +# @reexport using .TrigInterface + # include("objective_positioner_interface/ObjPositionerInterface.jl") +# @reexport using .ObjPositionerInterface + end From 7f8bfe6ccd23ef3aeca64c6df70cf09da9772493 Mon Sep 17 00:00:00 2001 From: kalidke Date: Fri, 30 Jan 2026 07:12:05 -0700 Subject: [PATCH 08/11] Rename NIDAQmx to DAQmx Package was renamed upstream. Co-Authored-By: Claude Opus 4.5 --- Project.toml | 4 +- .../crysta_laser_561/CrystaLaserControl.jl | 2 +- .../TransmissionDaqControl.jl | 2 +- .../nidaq/NIDAQcard.jl | 2 +- .../nidaq/interface_methods.jl | 118 +++++++++--------- .../vortran_laser_488/VortranLaserControl.jl | 2 +- 6 files changed, 65 insertions(+), 65 deletions(-) diff --git a/Project.toml b/Project.toml index 7b169d3..8f97a0f 100644 --- a/Project.toml +++ b/Project.toml @@ -14,7 +14,7 @@ ImageView = "86fae568-95e7-573e-a6b2-d8a6b900c9ef" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" LibSerialPort = "a05a14c7-6e3b-5ba9-90a2-45558833e1df" -NIDAQmx = "bc903ccc-f951-4f60-9748-ff64248ad6aa" +DAQmx = "bc903ccc-f951-4f60-9748-ff64248ad6aa" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -27,7 +27,7 @@ HDF5 = "0.17" ImageView = "0.12, 0.13" Images = "0.26" JLD2 = "0.5, 0.6" -NIDAQmx = "1" +DAQmx = "1" Revise = "3" Statistics = "1" julia = "1.10" diff --git a/src/hardware_implementations/crysta_laser_561/CrystaLaserControl.jl b/src/hardware_implementations/crysta_laser_561/CrystaLaserControl.jl index f7421b8..a02e04a 100644 --- a/src/hardware_implementations/crysta_laser_561/CrystaLaserControl.jl +++ b/src/hardware_implementations/crysta_laser_561/CrystaLaserControl.jl @@ -5,7 +5,7 @@ A Module for controlling a laser through a NIDAQ card. """ module CrystaLaserControl -using NIDAQmx +using DAQmx using ...MicroscopeControl.HardwareInterfaces.LightSourceInterface using ...MicroscopeControl.HardwareImplementations.NIDAQcard diff --git a/src/hardware_implementations/daq_transmission_light/TransmissionDaqControl.jl b/src/hardware_implementations/daq_transmission_light/TransmissionDaqControl.jl index eb27f93..1038c23 100644 --- a/src/hardware_implementations/daq_transmission_light/TransmissionDaqControl.jl +++ b/src/hardware_implementations/daq_transmission_light/TransmissionDaqControl.jl @@ -6,7 +6,7 @@ Citation: Ali Kazemi Nasaban Shotorban & Sheng Liu, Lidke Lab, UNM """ module TransmissionDaqControl -using NIDAQmx +using DAQmx using ...MicroscopeControl.HardwareInterfaces.LightSourceInterface using ...MicroscopeControl.HardwareImplementations.NIDAQcard diff --git a/src/hardware_implementations/nidaq/NIDAQcard.jl b/src/hardware_implementations/nidaq/NIDAQcard.jl index dacd6c1..dc4ef2e 100644 --- a/src/hardware_implementations/nidaq/NIDAQcard.jl +++ b/src/hardware_implementations/nidaq/NIDAQcard.jl @@ -3,7 +3,7 @@ """ module NIDAQcard -using NIDAQmx +using DAQmx using ...MicroscopeControl.HardwareInterfaces.DAQInterface import ...MicroscopeControl: export_state, initialize, shutdown diff --git a/src/hardware_implementations/nidaq/interface_methods.jl b/src/hardware_implementations/nidaq/interface_methods.jl index 1e1ca09..47df1f7 100644 --- a/src/hardware_implementations/nidaq/interface_methods.jl +++ b/src/hardware_implementations/nidaq/interface_methods.jl @@ -5,12 +5,12 @@ A dictionary defines functions of corresponding channel types. `channeltype`: "AI","AO","DI","DO","CI","CO" """ channelfunctions = Dict{String,Function}( - "AI" => NIDAQmx.ai_channels, - "AO" => NIDAQmx.ao_channels, - "DI" => NIDAQmx.di_lines, - "DO" => NIDAQmx.do_lines, - "CI" => NIDAQmx.ci_channels, - "CO" => NIDAQmx.co_channels, + "AI" => DAQmx.ai_channels, + "AO" => DAQmx.ao_channels, + "DI" => DAQmx.di_lines, + "DO" => DAQmx.do_lines, + "CI" => DAQmx.ci_channels, + "CO" => DAQmx.co_channels, ) """ @@ -20,10 +20,10 @@ A dictionary defines types of corresponding task types. `tasktype`: "AI","AO","DI","DO" """ taskfunctions = Dict{String,Type}( - "AI" => NIDAQmx.AITask, - "AO" => NIDAQmx.AOTask, - "DI" => NIDAQmx.DITask, - "DO" => NIDAQmx.DOTask, + "AI" => DAQmx.AITask, + "AO" => DAQmx.AOTask, + "DI" => DAQmx.DITask, + "DO" => DAQmx.DOTask, ) """ @@ -35,7 +35,7 @@ List available devices. - `daq::NIdaq`: A NIdaq type. """ function DAQInterface.showdevices(daq::NIdaq) - NIDAQmx.device_names() + DAQmx.device_names() end """ @@ -63,7 +63,7 @@ Create a task of a given task type and channel. - `channel::String`: A channel name, obtained from `showchannels`. # Returns -- `t::NIDAQmx.Task`: A NIDAQmx Task type. +- `t::DAQmx.Task`: A DAQmx Task type. """ function DAQInterface.createtask(daq::NIdaq, tasktype::String, channel::String) # Extract device name from channel (e.g., "Dev1/ao0" -> "Dev1") @@ -71,57 +71,57 @@ function DAQInterface.createtask(daq::NIdaq, tasktype::String, channel::String) if tasktype == "AI" # Query device's AI voltage range - ranges = NIDAQmx.ai_voltage_ranges(device) + ranges = DAQmx.ai_voltage_ranges(device) if isempty(ranges) # Use default range if none available - return NIDAQmx.AITask(channel) + return DAQmx.AITask(channel) end # Use the widest range (last row) min_val, max_val = ranges[end, :] - task = NIDAQmx.AITask() - NIDAQmx.add_ai_voltage!(task, channel; min_val=min_val, max_val=max_val) + task = DAQmx.AITask() + DAQmx.add_ai_voltage!(task, channel; min_val=min_val, max_val=max_val) return task elseif tasktype == "AO" # Query device's AO voltage range - ranges = NIDAQmx.ao_voltage_ranges(device) + ranges = DAQmx.ao_voltage_ranges(device) if isempty(ranges) # Use default range if none available - return NIDAQmx.AOTask(channel) + return DAQmx.AOTask(channel) end # Use the first (typically only) range min_val, max_val = ranges[1, :] - task = NIDAQmx.AOTask() - NIDAQmx.add_ao_voltage!(task, channel; min_val=min_val, max_val=max_val) + task = DAQmx.AOTask() + DAQmx.add_ao_voltage!(task, channel; min_val=min_val, max_val=max_val) return task elseif tasktype == "DI" - return NIDAQmx.DITask(channel) + return DAQmx.DITask(channel) elseif tasktype == "DO" - return NIDAQmx.DOTask(channel) + return DAQmx.DOTask(channel) else error("Unknown task type: $tasktype") end end """ - addchannel!(daq::NIdaq, t::NIDAQmx.Task, tasktype::String, channel::String) + addchannel!(daq::NIdaq, t::DAQmx.Task, tasktype::String, channel::String) Add a channel to an existing task. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQmx.Task`: A NIDAQmx Task. +- `t::DAQmx.Task`: A DAQmx Task. - `tasktype::String`: A task type. Options are "AI","AO","DI","DO". - `channel::String`: A channel name, obtained from `showchannels`. """ -function DAQInterface.addchannel!(daq::NIdaq, t::NIDAQmx.Task, tasktype::String, channel::String) +function DAQInterface.addchannel!(daq::NIdaq, t::DAQmx.Task, tasktype::String, channel::String) if tasktype == "AI" - NIDAQmx.add_ai_voltage!(t, channel) + DAQmx.add_ai_voltage!(t, channel) elseif tasktype == "AO" - NIDAQmx.add_ao_voltage!(t, channel) + DAQmx.add_ao_voltage!(t, channel) elseif tasktype == "DI" - NIDAQmx.add_di_chan!(t, channel) + DAQmx.add_di_chan!(t, channel) elseif tasktype == "DO" - NIDAQmx.add_do_chan!(t, channel) + DAQmx.add_do_chan!(t, channel) else error("Unknown task type: $tasktype") end @@ -130,124 +130,124 @@ end """ - setvoltage(daq::NIdaq,t::NIDAQmx.AOTask,voltage::Float64) + setvoltage(daq::NIdaq,t::DAQmx.AOTask,voltage::Float64) Set the voltage of an analog output task. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQmx.AOTask`: A NIDAQmx AOTask type. +- `t::DAQmx.AOTask`: A DAQmx AOTask type. - `voltage::Float64`: The voltage to set the task to, unit: volt. # Returns - `ret::Int`: The number of samples written to the task. """ -function DAQInterface.setvoltage(daq::NIdaq, t::NIDAQmx.AOTask, voltage::Float64) - NIDAQmx.write_scalar(t, voltage; auto_start=true) +function DAQInterface.setvoltage(daq::NIdaq, t::DAQmx.AOTask, voltage::Float64) + DAQmx.write_scalar(t, voltage; auto_start=true) return 1 end """ - setvoltage(daq::NIdaq,t::NIDAQmx.DOTask,voltage::Float64) + setvoltage(daq::NIdaq,t::DAQmx.DOTask,voltage::Float64) Set the voltage of a digital output task. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQmx.DOTask`: A NIDAQmx DOTask type. +- `t::DAQmx.DOTask`: A DAQmx DOTask type. - `voltage::Float64`: The voltage to set (0 or 1). # Returns - `ret::Int`: The number of samples written to the task. """ -function DAQInterface.setvoltage(daq::NIdaq, t::NIDAQmx.DOTask, voltage::Float64) - NIDAQmx.write_scalar(t, UInt32(voltage); auto_start=true) +function DAQInterface.setvoltage(daq::NIdaq, t::DAQmx.DOTask, voltage::Float64) + DAQmx.write_scalar(t, UInt32(voltage); auto_start=true) return 1 end """ - setvoltage(daq::NIdaq,t::NIDAQmx.AOTask,voltage::Array{Float64}) + setvoltage(daq::NIdaq,t::DAQmx.AOTask,voltage::Array{Float64}) Set the voltage of an analog output task with an array of values. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQmx.AOTask`: A NIDAQmx AOTask type. +- `t::DAQmx.AOTask`: A DAQmx AOTask type. - `voltage::Array{Float64}`: The voltages to set, unit: volt. # Returns - `ret::Int`: The number of samples written to the task. """ -function DAQInterface.setvoltage(daq::NIdaq, t::NIDAQmx.AOTask, voltage::Array{Float64}) +function DAQInterface.setvoltage(daq::NIdaq, t::DAQmx.AOTask, voltage::Array{Float64}) write(t, voltage; auto_start=true) end """ - setvoltage(daq::NIdaq,t::NIDAQmx.DOTask,voltage::Array{Float64}) + setvoltage(daq::NIdaq,t::DAQmx.DOTask,voltage::Array{Float64}) Set the voltage of a digital output task with an array of values. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQmx.DOTask`: A NIDAQmx DOTask type. +- `t::DAQmx.DOTask`: A DAQmx DOTask type. - `voltage::Array{Float64}`: The voltages to set (0 or 1). # Returns - `ret::Int`: The number of samples written to the task. """ -function DAQInterface.setvoltage(daq::NIdaq, t::NIDAQmx.DOTask, voltage::Array{Float64}) +function DAQInterface.setvoltage(daq::NIdaq, t::DAQmx.DOTask, voltage::Array{Float64}) write(t, UInt8.(voltage); auto_start=true) end """ - readvoltage(daq::NIdaq,t::NIDAQmx.AITask) + readvoltage(daq::NIdaq,t::DAQmx.AITask) Read the voltage of an analog input task. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQmx.AITask`: A NIDAQmx AITask type. +- `t::DAQmx.AITask`: A DAQmx AITask type. # Returns - `voltage::Float64`: The voltage read from the task, unit: volt. """ -function DAQInterface.readvoltage(daq::NIdaq, t::NIDAQmx.AITask) - NIDAQmx.start!(t) - voltage = NIDAQmx.read_scalar(t) - NIDAQmx.stop!(t) +function DAQInterface.readvoltage(daq::NIdaq, t::DAQmx.AITask) + DAQmx.start!(t) + voltage = DAQmx.read_scalar(t) + DAQmx.stop!(t) return voltage end """ - readvoltage(daq::NIdaq,t::NIDAQmx.DITask) + readvoltage(daq::NIdaq,t::DAQmx.DITask) Read the voltage of a digital input task. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQmx.DITask`: A NIDAQmx DITask type. +- `t::DAQmx.DITask`: A DAQmx DITask type. # Returns - `voltage::Float64`: The voltage read from the task (0 or 1). """ -function DAQInterface.readvoltage(daq::NIdaq, t::NIDAQmx.DITask) - NIDAQmx.start!(t) - voltage = Float64(NIDAQmx.read_scalar(t)) - NIDAQmx.stop!(t) +function DAQInterface.readvoltage(daq::NIdaq, t::DAQmx.DITask) + DAQmx.start!(t) + voltage = Float64(DAQmx.read_scalar(t)) + DAQmx.stop!(t) return voltage end """ - deletetask(daq::NIdaq,t::NIDAQmx.Task) + deletetask(daq::NIdaq,t::DAQmx.Task) Delete a task. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQmx.Task`: A NIDAQmx Task type. +- `t::DAQmx.Task`: A DAQmx Task type. """ -function DAQInterface.deletetask(daq::NIdaq, t::NIDAQmx.Task) - NIDAQmx.clear!(t) +function DAQInterface.deletetask(daq::NIdaq, t::DAQmx.Task) + DAQmx.clear!(t) end """ diff --git a/src/hardware_implementations/vortran_laser_488/VortranLaserControl.jl b/src/hardware_implementations/vortran_laser_488/VortranLaserControl.jl index 3596489..efdebda 100644 --- a/src/hardware_implementations/vortran_laser_488/VortranLaserControl.jl +++ b/src/hardware_implementations/vortran_laser_488/VortranLaserControl.jl @@ -5,7 +5,7 @@ A Module for controlling a laser through a NIDAQ card. """ module VortranLaserControl -using NIDAQmx +using DAQmx using ...MicroscopeControl import ...MicroscopeControl: export_state, initialize, shutdown From 8a840674ed0967854ced1065a8aadc012685c48b Mon Sep 17 00:00:00 2001 From: kalidke Date: Thu, 5 Mar 2026 11:10:18 -0700 Subject: [PATCH 09/11] Fix NIDAQcard include order and restore DAQmx dependency Move NIDAQcard include before TCubeLaserControl since it depends on it. Restore DAQmx to Project.toml after it was removed during manifest cleanup. Co-Authored-By: Claude Opus 4.6 --- Project.toml | 3 +-- src/hardware_implementations/HardwareImplementations.jl | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index d19a93e..4d7df62 100644 --- a/Project.toml +++ b/Project.toml @@ -6,6 +6,7 @@ authors = ["klidke@unm.edu"] [deps] CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +DAQmx = "bc903ccc-f951-4f60-9748-ff64248ad6aa" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" @@ -14,7 +15,6 @@ ImageView = "86fae568-95e7-573e-a6b2-d8a6b900c9ef" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" LibSerialPort = "a05a14c7-6e3b-5ba9-90a2-45558833e1df" -DAQmx = "bc903ccc-f951-4f60-9748-ff64248ad6aa" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -28,7 +28,6 @@ HDF5 = "0.17" ImageView = "0.12, 0.13" Images = "0.26" JLD2 = "0.5, 0.6" -DAQmx = "1" Reexport = "1.2.2" Revise = "3" Statistics = "1" diff --git a/src/hardware_implementations/HardwareImplementations.jl b/src/hardware_implementations/HardwareImplementations.jl index 3adaa2c..d8bb3d5 100644 --- a/src/hardware_implementations/HardwareImplementations.jl +++ b/src/hardware_implementations/HardwareImplementations.jl @@ -33,6 +33,10 @@ include("mcl_stage/MadCityLabs.jl") include("pi_N472/PI_N472.jl") @reexport using .PI_N472 +# DAQ implementation (must come before modules that depend on it) +include("nidaq/NIDAQcard.jl") +@reexport using .NIDAQcard + # Light source implementations include("simulated_light/SimulatedLight.jl") @reexport using .SimulatedLight @@ -49,10 +53,6 @@ include("crysta_laser_561/CrystaLaserControl.jl") include("vortran_laser_488/VortranLaserControl.jl") @reexport using .VortranLaserControl -# DAQ implementation (must come before OK_XEM which depends on it) -include("nidaq/NIDAQcard.jl") -@reexport using .NIDAQcard - # FPGA implementation (depends on NIDAQcard) include("ok_xem/OK_XEM.jl") @reexport using .OK_XEM From 60b0f34a5427f3e1b83f2a66fa43a043bbffd028 Mon Sep 17 00:00:00 2001 From: kalidke Date: Thu, 5 Mar 2026 12:26:45 -0700 Subject: [PATCH 10/11] Add DAQmx source URL and bump minimum Julia to 1.11 DAQmx.jl is not in General registry, so [sources] is needed. [sources] requires Julia 1.11+. Co-Authored-By: Claude Opus 4.6 --- Project.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 4d7df62..6aa0c6e 100644 --- a/Project.toml +++ b/Project.toml @@ -19,6 +19,9 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +[sources] +DAQmx = {url = "https://github.com/LidkeLab/DAQmx.jl.git"} + [compat] CEnum = "0.5.0" CairoMakie = "0.12, 0.13, 0.14, 0.15" @@ -31,7 +34,7 @@ JLD2 = "0.5, 0.6" Reexport = "1.2.2" Revise = "3" Statistics = "1" -julia = "1.10" +julia = "1.11" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" From cac21d89f35adbbd5cbcb893ae10b8d6180e09d2 Mon Sep 17 00:00:00 2001 From: kalidke Date: Thu, 5 Mar 2026 12:33:15 -0700 Subject: [PATCH 11/11] Add on_close callback to live camera display When the live display window is closed, abort the camera and invoke an optional on_close callback. This lets callers (e.g. MicroscopeAdapt) hook in additional cleanup like turning off lasers and updating GUI. Co-Authored-By: Claude Opus 4.6 --- src/hardware_interfaces/camera_interface/gui.jl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/hardware_interfaces/camera_interface/gui.jl b/src/hardware_interfaces/camera_interface/gui.jl index 2411eb9..faf9d3e 100644 --- a/src/hardware_interfaces/camera_interface/gui.jl +++ b/src/hardware_interfaces/camera_interface/gui.jl @@ -25,7 +25,7 @@ function create_button(figure, row, column, label, action, camera) end -function create_display(camera, display_function) +function create_display(camera, display_function; on_close=nothing) # Setup display # Camera data is (H, W) where data[row, col] = data[y, x] # Makie image() maps first dim → x, second dim → y @@ -52,6 +52,16 @@ function create_display(camera, display_function) # Display the plot and store the Scene scene = display(GLMakie.Screen(), imgplot) + # Abort camera when display window is closed, and call on_close callback + on(events(imgplot).window_open) do open + if !open + abort(camera) + if on_close !== nothing + on_close() + end + end + end + # Start live imaging or sequence based on display_function fps = 60 display_function(camera) @@ -114,8 +124,8 @@ function gui(camera::Camera) display(GLMakie.Screen(), control_fig) end -function start_live(camera::Camera) - create_display(camera, live) +function start_live(camera::Camera; on_close=nothing) + create_display(camera, live; on_close=on_close) end function start_capture(camera::Camera)