diff --git a/CLAUDE.md b/CLAUDE.md index 5488b74..8de8087 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,6 +87,25 @@ 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 (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 +``` + +**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/Project.toml b/Project.toml index ec2d9ae..6aa0c6e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,11 +1,12 @@ 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" 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,23 +15,26 @@ 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" +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] -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" 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" +julia = "1.11" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/src/MicroscopeControl.jl b/src/MicroscopeControl.jl index 929c0f2..e1628d0 100644 --- a/src/MicroscopeControl.jl +++ b/src/MicroscopeControl.jl @@ -1,78 +1,32 @@ """ -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 +export AbstractSystem, AbstractSystemState +export export_state, initialize, shutdown, get_state, set_state -# 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/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/HardwareImplementations.jl b/src/hardware_implementations/HardwareImplementations.jl index 2281ed9..d8bb3d5 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") -include("simulated_light/SimulatedLight.jl") +# 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 + 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 + +# 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_implementations/crysta_laser_561/CrystaLaserControl.jl b/src/hardware_implementations/crysta_laser_561/CrystaLaserControl.jl index bbe544b..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 NIDAQ +using DAQmx using ...MicroscopeControl.HardwareInterfaces.LightSourceInterface using ...MicroscopeControl.HardwareImplementations.NIDAQcard 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/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/daq_transmission_light/TransmissionDaqControl.jl b/src/hardware_implementations/daq_transmission_light/TransmissionDaqControl.jl index 97242b3..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 NIDAQ +using DAQmx using ...MicroscopeControl.HardwareInterfaces.LightSourceInterface using ...MicroscopeControl.HardwareImplementations.NIDAQcard 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 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/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/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/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/nidaq/NIDAQcard.jl b/src/hardware_implementations/nidaq/NIDAQcard.jl index c5d15b0..dc4ef2e 100644 --- a/src/hardware_implementations/nidaq/NIDAQcard.jl +++ b/src/hardware_implementations/nidaq/NIDAQcard.jl @@ -3,7 +3,7 @@ """ module NIDAQcard -using NIDAQ +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 fa80037..47df1f7 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" => DAQmx.ai_channels, + "AO" => DAQmx.ao_channels, + "DI" => DAQmx.di_lines, + "DO" => DAQmx.do_lines, + "CI" => DAQmx.ci_channels, + "CO" => DAQmx.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" => DAQmx.AITask, + "AO" => DAQmx.AOTask, + "DI" => DAQmx.DITask, + "DO" => DAQmx.DOTask, ) """ @@ -35,13 +35,13 @@ List available devices. - `daq::NIdaq`: A NIdaq type. """ function DAQInterface.showdevices(daq::NIdaq) - devices() + DAQmx.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::DAQmx.Task`: A DAQmx 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 = DAQmx.ai_voltage_ranges(device) + if isempty(ranges) + # Use default range if none available + return DAQmx.AITask(channel) + end + # Use the widest range (last row) + min_val, max_val = ranges[end, :] + 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 = DAQmx.ao_voltage_ranges(device) + if isempty(ranges) + # Use default range if none available + return DAQmx.AOTask(channel) + end + # Use the first (typically only) range + min_val, max_val = ranges[1, :] + task = DAQmx.AOTask() + DAQmx.add_ao_voltage!(task, channel; min_val=min_val, max_val=max_val) + return task + elseif tasktype == "DI" + return DAQmx.DITask(channel) + elseif tasktype == "DO" + return DAQmx.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::DAQmx.Task, tasktype::String, channel::String) + +Add a channel to an existing task. + +# Arguments +- `daq::NIdaq`: A NIdaq type. +- `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::DAQmx.Task, tasktype::String, channel::String) + if tasktype == "AI" + DAQmx.add_ai_voltage!(t, channel) + elseif tasktype == "AO" + DAQmx.add_ao_voltage!(t, channel) + elseif tasktype == "DI" + DAQmx.add_di_chan!(t, channel) + elseif tasktype == "DO" + DAQmx.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::DAQmx.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::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::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::DAQmx.AOTask, voltage::Float64) + DAQmx.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::DAQmx.DOTask,voltage::Float64) + +Set the voltage of a digital output task. + +# Arguments +- `daq::NIdaq`: A NIdaq 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::DAQmx.DOTask, voltage::Float64) + DAQmx.write_scalar(t, UInt32(voltage); auto_start=true) + return 1 end """ - readvoltage(daq::NIdaq,t::NIDAQ.Task) + setvoltage(daq::NIdaq,t::DAQmx.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::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::DAQmx.AOTask, voltage::Array{Float64}) + write(t, voltage; auto_start=true) +end + +""" + 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::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::DAQmx.DOTask, voltage::Array{Float64}) + write(t, UInt8.(voltage); auto_start=true) +end + +""" + readvoltage(daq::NIdaq,t::DAQmx.AITask) + +Read the voltage of an analog input task. + +# Arguments +- `daq::NIdaq`: A NIdaq 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::NIDAQ.Task) - start(t) - voltage = NIDAQ.read(t) - 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::DAQmx.DITask) + +Read the voltage of a digital input task. + +# Arguments +- `daq::NIdaq`: A NIdaq 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::DAQmx.DITask) + DAQmx.start!(t) + voltage = Float64(DAQmx.read_scalar(t)) + DAQmx.stop!(t) return voltage end """ - deletetask(daq::NIdaq,t::NIDAQ.Task) + deletetask(daq::NIdaq,t::DAQmx.Task) Delete a task. # Arguments - `daq::NIdaq`: A NIdaq type. -- `t::NIDAQ.Task`: A NIDAQ.Task type. +- `t::DAQmx.Task`: A DAQmx Task type. """ -function DAQInterface.deletetask(daq::NIdaq,t::NIDAQ.Task) - clear(t) -end +function DAQInterface.deletetask(daq::NIdaq, t::DAQmx.Task) + DAQmx.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/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/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_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/thorcam_dcx/interface_methods.jl b/src/hardware_implementations/thorcam_dcx/interface_methods.jl index d442941..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,15 +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 - data = reshape(img_vector, (camera.bytes_pixel,Width, Height)); - data = data[1,:,:] + # Reshape and permute to column-major Julia (H, W) convention + # SDK returns row-major (W, H), permute to (H, W) + data = permutedims(reshape(img_vector, (Width, Height)), (2, 1)) return data end @@ -190,12 +192,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) + 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] = img[1,:,:] + # 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_implementations/vortran_laser_488/VortranLaserControl.jl b/src/hardware_implementations/vortran_laser_488/VortranLaserControl.jl index f927b4e..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 NIDAQ +using DAQmx using ...MicroscopeControl import ...MicroscopeControl: export_state, initialize, shutdown 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 """ 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/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 diff --git a/src/hardware_interfaces/camera_interface/gui.jl b/src/hardware_interfaces/camera_interface/gui.jl index 50c080e..faf9d3e 100644 --- a/src/hardware_interfaces/camera_interface/gui.jl +++ b/src/hardware_interfaces/camera_interface/gui.jl @@ -25,54 +25,58 @@ 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 + # 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 + # 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 - #@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 """ @@ -120,32 +124,31 @@ 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) # 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)) - - imgplot = image(rotr90(img), - axis=(aspect=DataAspect(),), - figure=(figure_padding=0, resolution=size(img))) + # Permute from (H, W) to (W, H) for Makie display + img = Gray{N0f8}.(permutedims(framedata) .* (1 / max_val)) - # 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) 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