diff --git a/beamline_support.py b/beamline_support.py index 021e3b7a..0d8a926d 100644 --- a/beamline_support.py +++ b/beamline_support.py @@ -319,9 +319,14 @@ def read_db(): def init_motors(): global motor_channel_dict + md2_motors = ["omega","sampleX","sampleY","sampleZ", "finex", "finey", "finez"] for key in list(motor_dict.keys()): - motor_channel_dict[motor_dict[key]] = EpicsMotor(motor_dict[key],name = key) + if beamline_designation == "XF:19ID" and key in md2_motors: + from mxbluesky.devices.md2 import MD2Positioner + motor_channel_dict[motor_dict[key]] = MD2Positioner(motor_dict[key],name = key) + else: + motor_channel_dict[motor_dict[key]] = EpicsMotor(motor_dict[key],name = key) def initControlPVs(): diff --git a/bin/lsdcServer_nyx.cmd b/bin/lsdcServer_nyx.cmd index b06b291b..47fe7b11 100755 --- a/bin/lsdcServer_nyx.cmd +++ b/bin/lsdcServer_nyx.cmd @@ -2,7 +2,7 @@ export PROJDIR=/nsls2/software/mx/daq/ export CONFIGDIR=${PROJDIR}bnlpx_config/ export LSDCHOME=${PROJDIR}lsdc_nyx -export EPICS_CA_AUTO_ADDR_LIST=NO +#export EPICS_CA_AUTO_ADDR_LIST=NO export PYTHONPATH=".:${CONFIGDIR}:/usr/lib64/edna-mx/mxv1/src:/usr/lib64/edna-mx/kernel/src:${LSDCHOME}:${PROJDIR}/RobotControlLib" export PATH=/usr/local/bin:/usr/bin:/bin:${PROJDIR}/software/bin:/opt/ccp4/bin source ${CONFIGDIR}daq_env_nyx.txt diff --git a/config_params.py b/config_params.py index 55310241..5453b6da 100644 --- a/config_params.py +++ b/config_params.py @@ -64,7 +64,7 @@ class OnMountAvailOptions(Enum): AUTO_RASTER = 2 # Mounts, centers and takes 2 orthogonal rasters HUTCH_TIMER_DELAY = 500 -SAMPLE_TIMER_DELAY = 0 +SAMPLE_TIMER_DELAY = 40 SERVER_CHECK_DELAY = 2000 FAST_DP_MIN_NODES = 4 @@ -81,16 +81,17 @@ class OnMountAvailOptions(Enum): PINS_PER_PUCK = 16 DETECTOR_OBJECT_TYPE_LSDC = "lsdc" # using det_lib +DETECTOR_OBJECT_TYPE_NO_INIT = "no init" # skip epics detector init DETECTOR_OBJECT_TYPE_OPHYD = "ophyd" # instantiated in start_bs, using Bluesky scans DETECTOR_OBJECT_TYPE = "detectorObjectType" DETECTOR_SAFE_DISTANCE = {"fmx": 200.0, "amx": 180.0, "nyx": 200.0} GOVERNOR_TIMEOUT = 120 # seconds for a governor move -DEWAR_SECTORS = {"amx": 8, "fmx": 8, "nyx": 5} -PUCKS_PER_DEWAR_SECTOR = {"amx": 3, "fmx": 3, "nyx": 3} +DEWAR_SECTORS = {'amx':8, 'fmx':8, 'nyx':8} +PUCKS_PER_DEWAR_SECTOR = {'amx':3, 'fmx':3, 'nyx':3} -cryostreamTempPV = {"amx": "XF:17IDB-ES:AMX{CS:1}SAMPLE_TEMP_RBV", "fmx": "FMX:cs700:gasT-I"} +cryostreamTempPV = {"amx": "XF:17IDB-ES:AMX{CS:1}SAMPLE_TEMP_RBV", "fmx": "FMX:cs700:gasT-I", "nyx":"XF:19ID2:CS700:TEMP"} VALID_EXP_TIMES = { "amx": {"min": 0.005, "max": 1, "digits": 3}, @@ -132,4 +133,5 @@ class OnMountAvailOptions(Enum): BEAMSIZE_OPTIONS = { "S": ["V0", "H0"], "L": ["V1", "H1"] -} \ No newline at end of file +} +OPHYD_COLLECTIONS = {"amx": False, "fmx": False, "nyx": True} diff --git a/daq_lib.py b/daq_lib.py index e8ec44f2..af1fc149 100644 --- a/daq_lib.py +++ b/daq_lib.py @@ -257,8 +257,8 @@ def mountSample(sampID): setPvDesc("robotZWorkPos",getPvDesc("robotZMountPos")) setPvDesc("robotOmegaWorkPos",90.0) logger.info("done setting work pos") - if (currentMountedSampleID != ""): #then unmount what's there - if (sampID!=currentMountedSampleID and not robot_lib.multiSampleGripper()): + if (currentMountedSampleID != "" and not robot_lib.multiSampleGripper()): #then unmount what's there + if (sampID!=currentMountedSampleID): puckPos = mountedSampleDict["puckPos"] pinPos = mountedSampleDict["pinPos"] if robot_lib.unmountRobotSample(gov_robot, puckPos,pinPos,currentMountedSampleID): @@ -701,22 +701,30 @@ def collect_detector_seq_hw(sweep_start,range_degrees,image_width,exposure_perio file_prefix_minus_directory = file_prefix_minus_directory[file_prefix_minus_directory.rindex("/")+1:len(file_prefix_minus_directory)] except ValueError: pass + logger.info("collect %f degrees for %f seconds %d images exposure_period = %f exposure_time = %f" % (range_degrees,range_seconds,number_of_images,exposure_period,exposure_time)) - if (protocol == "standard" or protocol == "characterize" or protocol == "ednaCol" or protocol == "burn"): - logger.info("vectorSync " + str(time.time())) - daq_macros.vectorSync() - logger.info("zebraDaq " + str(time.time())) - - vector_params = daq_macros.gatherStandardVectorParams() - logger.debug(f"vector_params: {vector_params}") - RE(daq_macros.standard_plan(flyer,angleStart,number_of_images,range_degrees,image_width,exposure_period,file_prefix_minus_directory,data_directory_name,file_number, vector_params, file_prefix_minus_directory)) - - elif (protocol == "vector"): - RE(daq_macros.vectorZebraScan(currentRequest)) - elif (protocol == "stepVector"): - daq_macros.vectorZebraStepScan(currentRequest) - else: - pass + + if OPHYD_COLLECTIONS[daq_utils.beamline]: + logger.info("ophyd collections enabled") + if (protocol == "standard"): + RE(daq_macros.standard_plan_wrapped(currentRequest)) + elif (protocol == "vector"): + RE(daq_macros.vector_plan_wrapped(currentRequest)) + else: + if (protocol == "standard" or protocol == "characterize" or protocol == "ednaCol" or protocol == "burn"): + logger.info("vectorSync " + str(time.time())) + daq_macros.vectorSync() + logger.info("zebraDaq " + str(time.time())) + + vector_params = daq_macros.gatherStandardVectorParams() + logger.debug(f"vector_params: {vector_params}") + RE(daq_macros.standard_zebra_plan(flyer,angleStart,number_of_images,range_degrees,image_width,exposure_period,file_prefix_minus_directory,data_directory_name,file_number, vector_params, file_prefix_minus_directory)) + elif (protocol == "vector"): + RE(daq_macros.vectorZebraScan(currentRequest)) + elif (protocol == "stepVector"): + daq_macros.vectorZebraStepScan(currentRequest) + else: + pass return @@ -768,6 +776,22 @@ def checkC2C_X(x,fovx): # this is to make sure the user doesn't make too much of def center_on_click(x,y,fovx,fovy,source="screen",maglevel=0,jog=0,viewangle=daq_utils.CAMERA_ANGLE_BEAM): #maglevel=0 means lowmag, high fov, #1 = himag with digizoom option, #source=screen = from screen click, otherwise from macro with full pixel dimensions #viewangle=daq_utils.CAMERA_ANGLE_BEAM, default camera angle is in-line with the beam + + if daq_utils.beamline == "nyx": + logger.info("center_on_click: %s" % str((x,y))) + lsdc_x = daq_utils.screenPixX + lsdc_y = daq_utils.screenPixY + md2_x = getPvDesc("md2CenterPixelX") * 2 + md2_y = getPvDesc("md2CenterPixelY") * 2 + scale_x = md2_x / lsdc_x + scale_y = md2_y / lsdc_y + x = x * scale_x + y = y * scale_y + str_coords = f'{x} {y}' + logger.info(f'center_on_click: {str_coords}') + setPvDesc("MD2C2C", str_coords) + return + if (getBlConfig('robot_online')): #so that we don't move things when robot moving? robotGovState = (getPvDesc("robotSaActive") or getPvDesc("humanSaActive")) if (not robotGovState): diff --git a/daq_macros.py b/daq_macros.py index 4e6da73a..debb1eb4 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -29,16 +29,22 @@ from collections import OrderedDict from threading import Thread from config_params import * +from ophyd.status import SubscriptionStatus +from ophyd.utils import WaitTimeoutError from kafka_producer import send_kafka_message import gov_lib +import urllib.request +import io from scans import (zebra_daq_prep, setup_zebra_vector_scan, setup_zebra_vector_scan_for_raster, setup_vector_program) import bluesky.plan_stubs as bps import bluesky.plans as bp -from bluesky.preprocessors import finalize_wrapper, finalize_decorator +from bluesky.preprocessors import finalize_wrapper +from bluesky.log import config_bluesky_logging +config_bluesky_logging(level='INFO') from fmx_annealer import govStatusGet, govStateSet, fmxAnnealer, amxAnnealer # for using annealer specific to FMX and AMX from config_params import ON_MOUNT_OPTION, OnMountAvailOptions, BEAMSIZE_OPTIONS from mxbluesky.plans import detect_loop, topview_optimized @@ -108,9 +114,9 @@ def move_omega(omega, relative=True): """Moves omega by a certain amount""" if gov_robot.state.get() == "SA": if relative: - RE(bps.mvr(samplexyz.omega, omega)) + RE(bps.mvr(gonio.omega, omega)) else: - RE(bps.mv(samplexyz.omega, omega)) + RE(bps.mv(gonio.omega, omega)) def changeImageCenterLowMag(x,y,czoom): zoom = int(czoom) @@ -397,7 +403,6 @@ def autoRasterLoop(currentRequest): def autoRasterLoopOld(currentRequest): global autoRasterFlag - gov_status = gov_lib.setGovRobot(gov_robot, 'SA') if not gov_status.success: return 0 @@ -995,10 +1000,12 @@ def runDozorThread(directory, ID of raster collection """ global rasterRowResultsList,processedRasterRowCount - - time.sleep(0.5) #allow for file writing - + file_writing_delay = 0.5 node = getNodeName("spot", rowIndex, 8) + if daq_utils.beamline == 'nyx': + file_writing_delay = 10 + node = "titania-cpu00"+str((rowIndex%4)+1) + time.sleep(file_writing_delay) #allow for file writing if (seqNum>-1): #eiger dozorRowDir = makeDozorInputFile(directory, @@ -1012,6 +1019,7 @@ def runDozorThread(directory, raise Exception("seqNum seems to be non-standard (<0)") comm_s = f"ssh -q {node} \"{os.environ['MXPROCESSINGSCRIPTSDIR']}dozor.sh {rasterReqID} {rowIndex}\"" + logger.info(f"using the following command: {comm_s}") os.system(comm_s) logger.info('checking for results on remote node: %s' % comm_s) logger.info("leaving thread") @@ -1206,8 +1214,11 @@ def snakeRaster(rasterReqID,grain=""): if (scannerType == "PI"): snakeRasterFine(rasterReqID,grain) else: - finalize_plan = finalize_wrapper(snakeRasterBluesky(rasterReqID,grain), bps.mv(raster_flyer.detector.cam.acquire, 0)) - yield from finalize_plan + if daq_utils.beamline == "nyx": + yield from raster_plan_wrapped(rasterReqID) + else: + finalize_plan = finalize_wrapper(snakeRasterBluesky(rasterReqID,grain), bps.mv(raster_flyer.detector.cam.acquire, 0)) + yield from finalize_plan #RE(snakeRasterBluesky(rasterReqID,grain)) def snakeRasterNoTile(rasterReqID,grain=""): @@ -1718,7 +1729,7 @@ def snakeRasterNormal(rasterReqID,grain=""): if not procFlag: #must go to known position to account for windup dist. logger.info("moving to raster start") - samplexyz.put(rasterStartX, rasterStartY, rasterStartZ, omega) + gonio.put(rasterStartX, rasterStartY, rasterStartZ, omega) logger.info("done moving to raster start") if (procFlag): @@ -2008,7 +2019,7 @@ def snakeRasterBluesky(rasterReqID, grain=""): setPvDesc("sampleProtect",0) setPvDesc("vectorGo", 0) #set to 0 to allow easier camonitoring vectorGo data_directory_name, filePrefix, file_number_start, dataFilePrefix, exptimePerCell, img_width_per_cell, wave, detDist, rasterDef, stepsize, omega, rasterStartX, rasterStartY, rasterStartZ, omegaRad, rowCount, numsteps, totalImages, rows = params_from_raster_req_id(rasterReqID) - rasterRowResultsList = [{} for i in range(0,rowCount)] + rasterRowResultsList = [{} for i in range(0,rowCount)] processedRasterRowCount = 0 rasterEncoderMap = {} @@ -2025,7 +2036,7 @@ def snakeRasterBluesky(rasterReqID, grain=""): parentRequest = db_lib.getRequestByID(parentReqID) parentReqObj = parentRequest["request_obj"] parentReqProtocol = parentReqObj["protocol"] - detDist = parentReqObj["detDist"] + detDist = parentReqObj["detDist"] rasterFilePrefix = dataFilePrefix + "_Raster" total_exposure_time = exptimePerCell*totalImages @@ -2105,10 +2116,10 @@ def snakeRasterBluesky(rasterReqID, grain=""): if not procFlag: # no, no processing. just move to raster start #must go to known position to account for windup dist. logger.info("moving to raster start") - yield from bps.mv(samplexyz.x, rasterStartX) - yield from bps.mv(samplexyz.y, rasterStartY) - yield from bps.mv(samplexyz.z, rasterStartZ) - yield from bps.mv(samplexyz.omega, omega) + yield from bps.mv(gonio.x, rasterStartX) + yield from bps.mv(gonio.y, rasterStartY) + yield from bps.mv(gonio.z, rasterStartZ) + yield from bps.mv(gonio.omega, omega) logger.info("done moving to raster start") else: # yes, do row processing @@ -2116,12 +2127,12 @@ def snakeRasterBluesky(rasterReqID, grain=""): [thread.join(timeout=120) for thread in spotFindThreadList] else: logger.info("raster aborted, do not wait for spotfind threads") - logger.info(str(processedRasterRowCount) + "/" + str(rowCount)) + logger.info(str(processedRasterRowCount) + "/" + str(rowCount)) rasterResult = generateGridMap(rasterRequest) - + logger.info(f'protocol = {reqObj["protocol"]}') if (reqObj["protocol"] == "multiCol" or parentReqProtocol == "multiColQ"): - if (parentReqProtocol == "multiColQ"): + if (parentReqProtocol == "multiColQ"): multiColThreshold = parentReqObj["diffCutoff"] else: multiColThreshold = reqObj["diffCutoff"] @@ -2133,10 +2144,10 @@ def snakeRasterBluesky(rasterReqID, grain=""): except ValueError: #must go to known position to account for windup dist. logger.info("moving to raster start because of value error in gotoMaxRaster") - yield from bps.mv(samplexyz.x, rasterStartX) - yield from bps.mv(samplexyz.y, rasterStartY) - yield from bps.mv(samplexyz.z, rasterStartZ) - yield from bps.mv(samplexyz.omega, omega) + yield from bps.mv(gonio.x, rasterStartX) + yield from bps.mv(gonio.y, rasterStartY) + yield from bps.mv(gonio.z, rasterStartZ) + yield from bps.mv(gonio.omega, omega) logger.info("done moving to raster start") """change request status so that GUI only fills heat map when @@ -2156,7 +2167,7 @@ def snakeRasterBluesky(rasterReqID, grain=""): rasterRequest["request_obj"]["rasterDef"]["status"] = ( RasterStatus.READY_FOR_SNAPSHOT.value ) - db_lib.updateRequest(rasterRequest) + db_lib.updateRequest(rasterRequest) db_lib.updatePriority(rasterRequestID,-1) #ensure gov transitions have completed successfully @@ -3476,7 +3487,12 @@ def loop_center_xrec(): pic_prefix = "findloop" output_file = 'xrec_result.txt' clean_up_files(pic_prefix, output_file) - zebraCamDaq(0,360,40,.4,pic_prefix,getBlConfig("visitDirectory"),0) + if daq_utils.beamline=='nyx': + print('post clean') + xrec_no_zebra(0) + print('post no zebra') + else: + zebraCamDaq(0,360,40,.4,pic_prefix,getBlConfig("visitDirectory"),0) comm_s = f'xrec {os.environ["CONFIGDIR"]}/xrec_360_40Fast.txt {output_file}' logger.info(comm_s) try: @@ -3529,7 +3545,35 @@ def loop_center_xrec(): #now try to get the loopshape starting from here return 1 - +def xrec_no_zebra(angle_start): + print(f'xrec_no_zebra{angle_start}') + beamline_lib.mvaDescriptor("omega", angle_start) + for omega_target in range (angle_start, angle_start+360, 40): + beamline_lib.mvaDescriptor("omega", omega_target) + logger.info(f'taking image at {omega_target}') + timeout = 5 + # change image mode to single(0) + setPvDesc("lowMagImMode",0) + # start camera + setPvDesc("lowMagAcquire",1) + try: + with urllib.request.urlopen(daq_utils.lowMagCamURL, timeout=timeout) as response: + logger.info("xnz: read") + image_data = response.read() + logger.info("xnz: read") + with open(getBlConfig("visitDirectory")+"findloop_"+str(omega_target//40)+".jpg", "wb") as filename: + logger.info("xnz: write file") + filename.write(image_data) + logger.info("xnz: write file") + except urllib.error.URLError as e: + print("Error:", e) + logger.info("xnz: sleep") + time.sleep(1) + # change image mode to continuous(2) + setPvDesc("lowMagImMode",2) + # start camera + setPvDesc("lowMagAcquire",1) + def zebraCamDaq(angle_start,scanWidth,imgWidth,exposurePeriodPerImage,filePrefix,data_directory_name,file_number_start,scanEncoder=3): #scan encoder 0=x, 1=y,2=z,3=omega #careful - there's total exposure time, exposure period, exposure time @@ -3599,10 +3643,345 @@ def gatherStandardVectorParams(): vectorParams["transmission"]=transmission return vectorParams -def standard_plan(flyer,angle_start,num_images,scanWidth,imgWidth,exposurePeriodPerImage,filePrefix,data_directory_name,file_number_start, vector_params, data_path): +def standard_zebra_plan(flyer,angle_start,num_images,scanWidth,imgWidth,exposurePeriodPerImage,filePrefix,data_directory_name,file_number_start, vector_params, data_path): final_plan = finalize_wrapper(zebraDaqBluesky(flyer, angle_start, num_images, scanWidth, imgWidth, exposurePeriodPerImage, filePrefix, data_directory_name, file_number_start, vector_params, data_path), bps.mv(flyer.detector.cam.acquire, 0)) yield from final_plan +def standard_plan_wrapped(currentRequest): + yield from finalize_wrapper(standardDaq(currentRequest), clean_up_collection()) + +def vector_plan_wrapped(currentRequest): + yield from finalize_wrapper(vectorDaq(currentRequest), clean_up_collection()) + +def raster_plan_wrapped(rasterReqID): + yield from finalize_wrapper(rasterDaq(rasterReqID), clean_up_collection()) + #time.sleep(15) + #rasterRequest = db_lib.getRequestByID(rasterReqID) + #rasterResult = generateGridMap(rasterRequest) + #rasterRequest["request_obj"]["rasterDef"]["status"] = ( + # RasterStatus.READY_FOR_SNAPSHOT.value + #) + #db_lib.updateRequest(rasterRequest) + #db_lib.updatePriority(rasterReqID,-1) + #daq_lib.set_field("xrecRasterFlag",rasterRequest["uid"]) + + + +def standardDaq(currentRequest): + # collect all parameters + # perform preparatory movements + # arm the detector + # perform governor and phase transitions + # update flyer parameters + # fly + x_beam = getPvDesc("beamCenterX") + y_beam = getPvDesc("beamCenterY") + wavelength = daq_utils.energy2wave(beamline_lib.motorPosFromDescriptor("energy"), digits=6) + det_distance_m = beamline_lib.motorPosFromDescriptor("detectorDist") / 1000 + reqObj = currentRequest["request_obj"] + file_prefix = str(reqObj["file_prefix"]) + data_directory_name = str(reqObj["directory"]) + file_number_start = reqObj["file_number_start"] + sweep_start_angle = reqObj["sweep_start"] + sweep_end_angle = reqObj["sweep_end"] + file_prefix = str(reqObj["file_prefix"]) + data_directory_name = str(reqObj["directory"]) + file_number_start = reqObj["file_number_start"] + img_width = reqObj["img_width"] + exposure_per_image = reqObj["exposure_time"] + total_num_images = int(round(((sweep_end_angle - sweep_start_angle) / img_width), 4)) + total_exposure_time = exposure_per_image * total_num_images + scan_range = float(total_num_images)*img_width + angle_start = sweep_start_angle + wavelength = daq_utils.energy2wave(beamline_lib.motorPosFromDescriptor("energy"), digits=6) + + yield from bps.mv(beamstop.distance_preset, 20.0) + md2.save_center() + if det_move_done.get() != 1: + def det_move_done_callback(value, old_value, **kwargs): + return (old_value!=1 and value ==1) + det_move_status = SubscriptionStatus(det_move_done, det_move_done_callback, run=False) + det_move_status.wait() + if flyer.detector.cam.armed.get() == 1: + daq_lib.gui_message('Detector is in armed state from previous collection! Stopping detector, but the user ' + 'should check the most recent collection to determine if it was successful. Cancelling' + 'this collection, retry when ready.') + logger.warning("Detector was in the armed state prior to this attempted collection.") + return 0 + start_time = time.time() + logger.info(f"Configuring detector for standard collection with file_prefix {file_prefix} and data_directory_name {data_directory_name}") + flyer.configure_detector(file_prefix, data_directory_name) + logger.info(f"Arming detector for standard collection with angle_start {angle_start}, img_width {img_width}, total_num_images {total_num_images}, exposure_per_image {exposure_per_image}, file_prefix {file_prefix}, data_directory_name {data_directory_name}, file_number_start {file_number_start}, x_beam {x_beam}, y_beam {y_beam}, wavelength {wavelength}, det_distance_m {det_distance_m}") + flyer.detector_arm(angle_start, img_width, total_num_images, exposure_per_image, + file_prefix, data_directory_name, file_number_start, x_beam, y_beam, + wavelength, det_distance_m) + def armed_callback(value, old_value, **kwargs): + return (old_value == 0 and value == 1) + arm_status = SubscriptionStatus(flyer.detector.cam.armed, armed_callback, run=False) + flyer.detector.cam.acquire.put(1) + govStatus = gov_lib.setGovRobot(gov_robot, "DA") + try: + arm_status.wait(timeout=20) + govStatus.wait(timeout=20) + except WaitTimeoutError: + logger.error("Timeout during arming or governor move, aborting collection") + return + logger.info(f"Governor move to DA and synchronous arming took {time.time()-start_time} seconds.") + if govStatus.exception(): + logger.error(f"Problem during start-of-collection governor move, aborting! exception: {govStatus.exception()}") + return + flyer.detector.stage() + start_time = time.time() + yield from bps.mv(md2.phase, 2) # TODO: Enum for MD2 phases and states + try: + md2.ready_status().wait(timeout=10) + except WaitTimeoutError: + logger.error("timeout: md2 failed to enter ready state, aborting collection") + return + logger.info(f"MD2 phase transition to 2-DataCollection took {time.time()-start_time} seconds.") + flyer.update_parameters(total_num_images, angle_start, scan_range, total_exposure_time) + logger.info(f"flyer handoff") + yield from bp.fly([flyer]) + logger.info(f"fly complete") + +def vectorDaq(currentRequest): + # collect all parameters + # perform preparatory movements + # arm the detector + # perform governor and phase transitions + # update flyer parameters + # fly + x_beam = getPvDesc("beamCenterX") + y_beam = getPvDesc("beamCenterY") + wavelength = daq_utils.energy2wave(beamline_lib.motorPosFromDescriptor("energy"), digits=6) + det_distance_m = beamline_lib.motorPosFromDescriptor("detectorDist") / 1000 + reqObj = currentRequest["request_obj"] + file_prefix = str(reqObj["file_prefix"]) + data_directory_name = str(reqObj["directory"]) + file_number_start = reqObj["file_number_start"] + sweep_start_angle = reqObj["sweep_start"] + sweep_end_angle = reqObj["sweep_end"] + file_prefix = str(reqObj["file_prefix"]) + data_directory_name = str(reqObj["directory"]) + file_number_start = reqObj["file_number_start"] + img_width = reqObj["img_width"] + exposure_per_image = reqObj["exposure_time"] + total_num_images = int(round(((sweep_end_angle - sweep_start_angle) / img_width), 4)) + total_exposure_time = reqObj["exposure_time"] * total_num_images + scan_range = float(total_num_images)*img_width + angle_start = sweep_start_angle + wavelength = daq_utils.energy2wave(beamline_lib.motorPosFromDescriptor("energy"), digits=6) + + vector_params = reqObj["vectorParams"] + start_y=vector_params["vecStart"]["y"] + start_z=vector_params["vecStart"]["z"] + start_cx=vector_params["vecStart"]["finex"] + start_cy=vector_params["vecStart"]["finey"] + stop_cx=vector_params["vecEnd"]["finex"] + stop_cy=vector_params["vecEnd"]["finey"] + stop_y=vector_params["vecEnd"]["y"] + stop_z=vector_params["vecEnd"]["z"] + if det_move_done.get() != 1: + def det_move_done_callback(value, old_value, **kwargs): + return (old_value!=1 and value ==1) + det_move_status = SubscriptionStatus(det_move_done, det_move_done_callback, run=False) + det_move_status.wait() + + md2.save_center() + yield from bps.mv(beamstop.distance_preset, 20.0) + + if vector_flyer.detector.cam.armed.get() == 1: + daq_lib.gui_message('Detector is in armed state from previous collection! Stopping detector, but the user ' + 'should check the most recent collection to determine if it was successful. Cancelling' + 'this collection, retry when ready.') + logger.warning("Detector was in the armed state prior to this attempted collection.") + return 0 + start_time = time.time() + vector_flyer.configure_detector(file_prefix, data_directory_name) + vector_flyer.detector_arm(angle_start, img_width, total_num_images, exposure_per_image, + file_prefix, data_directory_name, file_number_start, x_beam, y_beam, + wavelength, det_distance_m) + def armed_callback(value, old_value, **kwargs): + return (old_value == 0 and value == 1) + arm_status = SubscriptionStatus(vector_flyer.detector.cam.armed, armed_callback, run=False) + vector_flyer.detector.cam.acquire.put(1) + govStatus = gov_lib.setGovRobot(gov_robot, "DA") + try: + arm_status.wait(timeout=10) + govStatus.wait(timeout=20) + except WaitTimeoutError: + logger.error("Timeout reached during arming or governor move, aborting") + return + logger.info(f"Governor move to DA and synchronous arming took {time.time()-start_time} seconds.") + if govStatus.exception(): + logger.error(f"Problem during start-of-collection governor move, aborting! exception: {govStatus.exception()}") + return + flyer.detector.stage() + start_time = time.time() + yield from bps.mv(md2.phase, 2) # TODO: Enum for MD2 phases and states + try: + md2.ready_status().wait(timeout=10) + except WaitTimeoutError: + logger.error("timeout: md2 failed to reach ready state, aborting") + return + logger.info(f"MD2 phase transition to 2-DataCollection took {time.time()-start_time} seconds.") + vector_flyer.update_parameters(angle_start, scan_range, total_exposure_time, start_y, start_z, stop_y, stop_z, start_cx, start_cy, stop_cx, stop_cy) + yield from bp.fly([vector_flyer]) + +def rasterDaq(rasterReqID): + global rasterRowResultsList,processedRasterRowCount + data_directory_name, file_prefix, file_number_start, dataFilePrefix, exposure_per_image, img_width_per_cell, wavelength, detDist, rasterDef, stepsize, start_omega, start_x, start_y, start_z, omegaRad, number_of_lines, numsteps, total_num_images, rows = params_from_raster_req_id(rasterReqID) + rasterRowResultsList = [{} for i in range(0,number_of_lines)] + processedRasterRowCount = 0 + rasterRequest = db_lib.getRequestByID(rasterReqID) + reqObj = rasterRequest["request_obj"] + parentReqID = reqObj["parentReqID"] + + xbeam = getPvDesc("beamCenterX") + ybeam = getPvDesc("beamCenterY") + if (parentReqID != -1): + parentRequest = db_lib.getRequestByID(parentReqID) + parentReqObj = parentRequest["request_obj"] + detDist = parentReqObj["detDist"] + + rasterFilePrefix = dataFilePrefix # + "_Raster" + + logger.info(f"prepping raster with: {rasterFilePrefix}, {data_directory_name}, {file_number_start}, {dataFilePrefix}, {exposure_per_image}, {img_width_per_cell}, {wavelength}, {detDist}, {rasterDef}, {stepsize}, {start_omega}, {start_x}, {start_y}, {start_z}, {omegaRad}, {number_of_lines}, {numsteps}, {total_num_images}, {rows}") + #logger.info(f"req_obj: {reqObj}") + i = 0 + xMotAbsoluteMove, xEnd, yMotAbsoluteMove, yEnd, zMotAbsoluteMove, zEnd = raster_positions(rows[i], stepsize, (start_omega*0), start_x, start_y, start_z, i) + stepsize /= 1000 # MD2 wants mm + logger.info(f"move calculations: {xMotAbsoluteMove}, {xEnd}, {yMotAbsoluteMove}, {yEnd}, {zMotAbsoluteMove}, {zEnd}") + line_range = stepsize * numsteps + total_uturn_range = stepsize * number_of_lines + start_y = start_y - (xEnd / 1000) + start_z = start_z - (yMotAbsoluteMove / 1000) + #start_z = start_z - (xEnd / 1000) + start_cx = md2.cx.val()# + (xEnd/1000) + start_cy = md2.cy.val() + frames_per_line = numsteps + total_exposure_time = exposure_per_image * frames_per_line + invert_direction = False + use_centring_table = True + use_fast_mesh_scans = True + omega_range = 0 + logger.info(f"TASK INFO: {md2.task_info.get()}") + logger.info(f"TASK INFO[6]: {md2.task_info.get()[6]=='1'}") + logger.info(f"TASK OUTPUT: {md2.task_output}") + logger.info(f"omega_range = {omegaRad}") + logger.info(f"line_range = {line_range}") + logger.info(f"total_uturn_range = {total_uturn_range}") + logger.info(f"start_omega = {start_omega}") + logger.info(f"start_y = {start_y}") + logger.info(f"current yzcxcy: {md2.y.get()}, {md2.z.get()}, {md2.cx.get()}, {md2.cy.get()}") + logger.info(f"start_z = {start_z}") + logger.info(f"start_cx = {start_cx}") + logger.info(f"start_cy = {start_cy}") + logger.info(f"number_of_lines = {number_of_lines}") + logger.info(f"frames_per_line = {frames_per_line}") + logger.info(f"total_exposure_time = {total_exposure_time}") + logger.info(f"invert_direction = {invert_direction}") + logger.info(f"use_centring_table = {use_centring_table}") + logger.info(f"use_fast_mesh_scans = {use_fast_mesh_scans}") + if det_move_done.get() != 1: + def det_move_done_callback(value, old_value, **kwargs): + return (old_value!=1 and value ==1) + det_move_status = SubscriptionStatus(det_move_done, det_move_done_callback, run=False) + det_move_status.wait() + + md2.save_center() + yield from bps.mv(beamstop.distance_preset, 20.0) + + if raster_flyer.detector.cam.armed.get() == 1: + daq_lib.gui_message('Detector is in armed state from previous collection! Stopping detector, but the user ' + 'should check the most recent collection to determine if it was successful. Cancelling' + 'this collection, retry when ready.') + logger.warning("Detector was in the armed state prior to this attempted collection.") + return 0 + start_time = time.time() + raster_flyer.configure_detector(rasterFilePrefix, data_directory_name) + raster_flyer.detector_arm(start_omega, img_width_per_cell, total_num_images, exposure_per_image, + file_prefix, data_directory_name, file_number_start, xbeam, ybeam, + wavelength, detDist) + def armed_callback(value, old_value, **kwargs): + return (old_value == 0 and value == 1) + arm_status = SubscriptionStatus(raster_flyer.detector.cam.armed, armed_callback, run=False) + raster_flyer.detector.cam.acquire.put(1) + govStatus = gov_lib.setGovRobot(gov_robot, "DA") + try: + arm_status.wait(timeout=10) + govStatus.wait(timeout=20) + except WaitTimeoutError: + logger.error("arming or governor status failure") + return + logger.info(f"Governor move to DA and synchronous arming took {time.time()-start_time} seconds.") + if govStatus.exception(): + logger.error(f"Problem during start-of-collection governor move, aborting! exception: {govStatus.exception()}") + return + flyer.detector.stage() + start_time = time.time() + yield from bps.mv(md2.phase, 2) # TODO: Enum for MD2 phases and states + try: + md2.ready_status().wait(timeout=10) + except: + logger.error("md2 failed to reach ready state, aborting collection") + return + logger.info(f"MD2 phase transition to 2-DataCollection took {time.time()-start_time} seconds.") + raster_flyer.update_parameters(omega_range, line_range, total_uturn_range, start_omega, start_y, start_z, start_cx, start_cy, number_of_lines, frames_per_line, total_exposure_time, invert_direction, use_centring_table, use_fast_mesh_scans) + yield from bp.fly([raster_flyer]) + spotFindThreadList = [] + row_index = 1 + logger.info(f"raster prefix {rasterFilePrefix}") + rasterFilePrefix = rasterFilePrefix.split("/")[-1] + logger.info(f"raster prefix {rasterFilePrefix}") + for i in range(0, number_of_lines): + time.sleep(1.0) + row_index = i + logger.info(f'spot finding for row {i}') + seqNum = raster_flyer.detector.cam.sequence_id.get() + spotFindThread = Thread(target=runDozorThread,args=(data_directory_name, #TODO this can't move outside of the thread checking block + rasterFilePrefix, + row_index, + numsteps, + seqNum, + reqObj, + rasterReqID)) + spotFindThread.start() + spotFindThreadList.append(spotFindThread) + [thread.join(timeout=120) for thread in spotFindThreadList] + logger.info(str(processedRasterRowCount) + "/" + str(number_of_lines)) + rasterResult = generateGridMap(rasterRequest) + rasterRequestID = rasterRequest["uid"] + rasterRequest["request_obj"]["rasterDef"]["status"] = ( + RasterStatus.READY_FOR_SNAPSHOT.value + ) + db_lib.updateRequest(rasterRequest) + db_lib.updatePriority(rasterRequestID,-1) + if (rasterRequest["request_obj"]["rasterDef"]["numCells"] + > getBlConfig(RASTER_NUM_CELLS_DELAY_THRESHOLD)): + #larger rasters can delay GUI scene update + time.sleep(getBlConfig(RASTER_LONG_SNAPSHOT_DELAY)) + else: + time.sleep(getBlConfig(RASTER_SHORT_SNAPSHOT_DELAY)) + daq_lib.set_field("xrecRasterFlag",rasterRequest["uid"]) + + + + + +def clean_up_collection(): + # this is a plan that should will always be run after a collection is complete + start_time = time.time() + yield from bps.mv(flyer.detector.cam.acquire, 0) + flyer.detector.unstage() + if (lastOnSample()): + gov_status = gov_lib.setGovRobot(gov_robot, 'SA', wait=False) + gov_status.wait(timeout=30) + yield from bps.mv(md2.phase, 0) + md2.ready_status().wait(timeout= 20) + # trigger processing here + logger.info(f"clean_up took {time.time()-start_time} seconds.") + def zebraDaqBluesky(flyer, angle_start, num_images, scanWidth, imgWidth, exposurePeriodPerImage, filePrefix, data_directory_name, file_number_start, vector_params, data_path, scanEncoder=3, changeState=True): logger.info("in Zebra Daq Bluesky #1") diff --git a/daq_main2.py b/daq_main2.py index 51812ce1..6cf2da58 100755 --- a/daq_main2.py +++ b/daq_main2.py @@ -28,12 +28,9 @@ logging.getLogger('ophyd').setLevel(logging.WARN) logging.getLogger('caproto').setLevel(logging.WARN) handler1 = handlers.RotatingFileHandler('lsdcServerLog.txt', maxBytes=5000000, backupCount=100) -handler2 = handlers.RotatingFileHandler('/var/log/dama/%slsdcServerLog.txt' % os.environ['BEAMLINE_ID'], maxBytes=5000000, backupCount=100) myformat = logging.Formatter('%(asctime)s %(name)-8s %(levelname)-8s %(message)s') handler1.setFormatter(myformat) -handler2.setFormatter(myformat) logger.addHandler(handler1) -logger.addHandler(handler2) perform_server_checks() setBlConfig("visitDirectory", os.getcwd()) diff --git a/daq_main_common.py b/daq_main_common.py index d303e26d..dd459370 100755 --- a/daq_main_common.py +++ b/daq_main_common.py @@ -34,60 +34,61 @@ def setGovState(state): setGovRobot(gov_robot, state) +functions = [ + set_beamsize, + importSpreadsheet, + mvaDescriptor, + setTrans, + loop_center_xrec, + autoRasterLoop, + snakeRaster, + backlightBrighter, + backlightDimmer, + changeImageCenterHighMag, + changeImageCenterLowMag, + center_on_click, + runDCQueue, + warmupGripper, + dryGripper, + enableDewarTscreen, + parkGripper, + stopDCQueue, + continue_data_collection, + mountSample, + unmountSample, + reprocessRaster, + fastDPNodes, + spotNodes, + unmountCold, + openPort, + set_beamcenter, + closePorts, + clearMountedSample, + recoverRobot, + rebootEMBL, + restartEMBL, + openGripper, + closeGripper, + homePins, + setSlit1X, + setSlit1Y, + testRobot, + setGovState, + move_omega, + lockGUI, + unlockGUI, + DewarAutoFillOff, + DewarAutoFillOn, + logMe, + unlatchGov, + backoffDetector, + enableMount, + robotOn, + set_energy + ] -functions = [anneal, - set_beamsize, - importSpreadsheet, - mvaDescriptor, - setTrans, - loop_center_xrec, - autoRasterLoop, - snakeRaster, - ispybLib.insertRasterResult, - backlightBrighter, - backlightDimmer, - changeImageCenterHighMag, - changeImageCenterLowMag, - center_on_click, - runDCQueue, - warmupGripper, - dryGripper, - enableDewarTscreen, - parkGripper, - stopDCQueue, - continue_data_collection, - mountSample, - unmountSample, - reprocessRaster, - fastDPNodes, - spotNodes, - unmountCold, - openPort, - set_beamcenter, - closePorts, - clearMountedSample, - recoverRobot, - rebootEMBL, - restartEMBL, - openGripper, - closeGripper, - homePins, - setSlit1X, - setSlit1Y, - testRobot, - setGovState, - move_omega, - lockGUI, - unlockGUI, - DewarAutoFillOff, - DewarAutoFillOn, - logMe, - unlatchGov, - backoffDetector, - enableMount, - robotOn, - set_energy - ] +if daq_utils.beamline != "nyx": + functions = functions + [anneal] whitelisted_functions: "Dict[str, Callable]" = { func.__name__: func for func in functions @@ -112,8 +113,8 @@ def pybass_init(): daq_utils.init_environment() daq_lib.init_var_channels() - #if getBlConfig(config_params.DETECTOR_OBJECT_TYPE) == config_params.DETECTOR_OBJECT_TYPE_LSDC: - det_lib.init_detector() + if getBlConfig(config_params.DETECTOR_OBJECT_TYPE) != config_params.DETECTOR_OBJECT_TYPE_NO_INIT: + det_lib.init_detector() daq_lib.message_string_pv = beamline_support.pvCreate(daq_utils.beamlineComm + "message_string") daq_lib.gui_popup_message_string_pv = beamline_support.pvCreate(daq_utils.beamlineComm + "gui_popup_message_string") beamline_lib.read_db() diff --git a/daq_utils.py b/daq_utils.py index 3201673f..9c61ea3e 100644 --- a/daq_utils.py +++ b/daq_utils.py @@ -34,7 +34,7 @@ CAMERA_ANGLE_BEAM = 0 # viewing angle is in line with beam, upstream from the sample, facing downstream, top toward ceiling CAMERA_ANGLE_ABOVE = 1 # viewing angle is directly above sample facing downward, top of view is downstream CAMERA_ANGLE_BELOW = 2 # viewing angle is directly below sample facing upward, top of view is downstream -global mag1ViewAngle, mag2ViewAngle, mag3VivewAngle, mag4ViewAngle +global mag1ViewAngle, mag2ViewAngle, mag3ViewAngle, mag4ViewAngle mag1ViewAngle = CAMERA_ANGLE_BEAM mag2ViewAngle = CAMERA_ANGLE_BEAM mag3ViewAngle = CAMERA_ANGLE_BEAM @@ -51,8 +51,7 @@ def setBlConfig(param, value, beamline=beamline): db_lib.setBeamlineConfigParam(beamline, param, value) def init_environment(): - global beamline,detector_id,mono_mot_code,has_beamline,has_xtalview,xtal_url,xtal_url_small,unitScaling,sampleCameraCount,xtalview_user,xtalview_pass,det_type,has_dna,beamstop_x_pvname,beamstop_y_pvname,camera_offset,det_radius,lowMagFOVx,lowMagFOVy,highMagFOVx,highMagFOVy,lowMagPixX,lowMagPixY,highMagPixX,highMagPixY,screenPixX,screenPixY,screenPixCenterX,screenPixCenterY,screenProtocol,screenPhist,screenPhiend,screenWidth,screenDist,screenExptime,screenWave,screenReso,gonioPvPrefix,searchParams,screenEnergy,detectorOffline,imgsrv_host,imgsrv_port,beamlineComm,primaryDewarName,lowMagCamURL,highMagZoomCamURL,lowMagZoomCamURL,highMagCamURL,owner,dewarPlateMap,mag1ViewAngle,mag2ViewAngle,mag3ViewAngle,mag4ViewAngle - + global beamline,detector_id,mono_mot_code,has_beamline,has_xtalview,xtal_url,xtal_url_small,unitScaling,sampleCameraCount,xtalview_user,xtalview_pass,det_type,has_dna,beamstop_x_pvname,beamstop_y_pvname,camera_offset,det_radius,lowMagFOVx,lowMagFOVy,highMagFOVx,highMagFOVy,lowMagPixX,lowMagPixY,highMagPixX,highMagPixY,screenPixX,screenPixY,screenPixCenterX,screenPixCenterY,screenProtocol,screenPhist,screenPhiend,screenWidth,screenDist,screenExptime,screenWave,screenReso,gonioPvPrefix,searchParams,screenEnergy,detectorOffline,imgsrv_host,imgsrv_port,beamlineComm,primaryDewarName,lowMagCamURL,highMagZoomCamURL,lowMagZoomCamURL,highMagCamURL,owner,dewarPlateMap,mag1ViewAngle,mag2ViewAngle,mag3ViewAngle,mag4ViewAngle,exporter_enabled owner = getpass.getuser() primaryDewarName = getBlConfig("primaryDewarName") @@ -72,6 +71,10 @@ def init_environment(): highMagPixY = float(getBlConfig("highMagPixY")) screenPixX = float(getBlConfig("screenPixX")) screenPixY = float(getBlConfig("screenPixY")) + if beamline == 'nyx': + exporter_enabled = bool(getBlConfig("exporterEnabled")) + else: + exporter_enabled = False try: unitScaling = float(getBlConfig("unitScaling")) diff --git a/denso_robot.py b/denso_robot.py index 06693225..1ea4c7bf 100644 --- a/denso_robot.py +++ b/denso_robot.py @@ -13,6 +13,12 @@ class OphydRobot: def __init__(self, robot): self.robot = robot + def parkRobot(self): + try: + self.robot.parkRobot() + except Exception as e: + logger.error(f'Failed to park robot: {e}') + def warmupGripper(self): try: logger.info('drying gripper') @@ -83,7 +89,7 @@ def unmount(self, gov_robot, puck_pos: int, pin_pos: int, samp_id: str): def finish(self): ... - def multiSampleGripper(): + def multiSampleGripper(self): return True def check_sample_mounted(self, mount, puck_pos, pin_pos): # is the correct sample present/absent as expected during a mount/unmount? diff --git a/gon_lib.py b/gon_lib.py index ce0f5d28..a42d4540 100755 --- a/gon_lib.py +++ b/gon_lib.py @@ -2,11 +2,19 @@ import beamline_lib import beamline_support from beamline_support import getPvValFromDescriptor as getPvDesc, setPvValFromDescriptor as setPvDesc -from start_bs import back_light, back_light_range import logging +import daq_utils +if daq_utils.beamline == 'nyx': + from start_bs import back_light, back_light_range, md2 logger = logging.getLogger(__name__) BACK_LIGHT_STEP = 0.05 # percent of intensity range +def omegaMoveAbs(angle): + md2.omega.set(angle) + +def omegaMoveRel(angle): + md2.omega.set(md2.omega.get() + angle) + def backlightBrighter(): intensity = back_light.get() intensity += BACK_LIGHT_STEP * (back_light_range[1]-back_light_range[0]) diff --git a/gui/control_main.py b/gui/control_main.py index fac940d7..481e8853 100644 --- a/gui/control_main.py +++ b/gui/control_main.py @@ -7,7 +7,9 @@ import sys import time from typing import Dict, List, Optional +import threading +from queue import Queue import cv2 import numpy as np from epics import PV @@ -17,11 +19,15 @@ from qt_epics.QtEpicsPVEntry import QtEpicsPVEntry from qt_epics.QtEpicsPVLabel import QtEpicsPVLabel from qtpy import QtCore, QtGui, QtWidgets -from qtpy.QtCore import QModelIndex, QRectF, Qt, QTimer +from qtpy.QtCore import QModelIndex, QRectF, Qt, QTimer, QMutex, QMutexLocker from qtpy.QtGui import QIntValidator -from qtpy.QtWidgets import QApplication, QCheckBox, QFrame, QGraphicsPixmapItem +from qtpy.QtWidgets import QCheckBox, QFrame, QGraphicsPixmapItem, QApplication import daq_utils +if daq_utils.beamline == 'nyx': + from mxbluesky.devices.md2 import GonioDevice, CameraDevice, MD2Device, LightDevice, MD2ApertureDevice +else: + from mxbluesky.devices.generic import GoniometerStack as GonioDevice import db_lib import lsdcOlog from config_params import ( @@ -54,6 +60,7 @@ SnapCommentDialog, StaffScreenDialog, UserScreenDialog, + CalculatorWindow ) from gui.raster import RasterCell, RasterGroup from gui.vector import VectorMarker, VectorWidget @@ -141,7 +148,7 @@ class ControlMain(QtWidgets.QMainWindow): fastShutterSignal = QtCore.Signal(float) gripTempSignal = QtCore.Signal(float) ringCurrentSignal = QtCore.Signal(float) - beamAvailableSignal = QtCore.Signal(float) + threeClickSignal = QtCore.Signal(str) sampleExposedSignal = QtCore.Signal(float) sampMoveSignal = QtCore.Signal(int, str) roiChangeSignal = QtCore.Signal(int, str) @@ -168,9 +175,11 @@ def __init__(self): self.redPen = QtGui.QPen(QtCore.Qt.red) self.bluePen = QtGui.QPen(QtCore.Qt.blue) self.yellowPen = QtGui.QPen(QtCore.Qt.yellow) - self.albulaInterface = AlbulaInterface(ip=os.environ["EIGER_DCU_IP"], - gov_message_pv_name=daq_utils.pvLookupDict["governorMessage"],) + if daq_utils.beamline != "nyx": + self.albulaInterface = AlbulaInterface(ip=os.environ["EIGER_DCU_IP"], + gov_message_pv_name=daq_utils.pvLookupDict["governorMessage"],) self.initUI() + self.initOphyd() self.govStateMessagePV = PV(daq_utils.pvLookupDict["governorMessage"]) self.zoom1FrameRatePV = PV(daq_utils.pvLookupDict["zoom1FrameRate"]) self.zoom2FrameRatePV = PV(daq_utils.pvLookupDict["zoom2FrameRate"]) @@ -214,17 +223,17 @@ def __init__(self): self.initCallbacks() if self.scannerType != "PI": self.motPos = { - "x": self.sampx_pv.get(), - "y": self.sampy_pv.get(), - "z": self.sampz_pv.get(), - "omega": self.omega_pv.get(), + "x": self.gon.x.val(), + "y": self.gon.y.val(), + "z": self.gon.z.val(), + "omega": self.gon.omega.val(), } else: self.motPos = { - "x": self.sampx_pv.get(), - "y": self.sampy_pv.get(), - "z": self.sampz_pv.get(), - "omega": self.omega_pv.get(), + "x": self.gon.x.val(), + "y": self.gon.y.val(), + "z": self.gon.z.val(), + "omega": self.gon.omega.val(), "fineX": self.sampFineX_pv.get(), "fineY": self.sampFineY_pv.get(), "fineZ": self.sampFineZ_pv.get(), @@ -244,6 +253,7 @@ def __init__(self): self.detDistRBVLabel.getEntry().text() ) # this is to fix the current val being overwritten by reso self.proposalID = -999999 + self.sampleCameraMutex = QMutex() if len(sys.argv) > 1: if sys.argv[1] == "master": self.changeControlMasterCB(1) @@ -289,21 +299,25 @@ def parseXRFTable(self): XRFInfoDict[tokens[0]] = int(float(tokens[5]) * 100) XRFFile.close() return XRFInfoDict + + + def closeEvent(self, evnt): + self.sampleCameraThread.stop() evnt.accept() sys.exit() # doing this to close any windows left open def initVideo2(self, frequency): - self.captureHighMag = cv2.VideoCapture(daq_utils.highMagCamURL) + self.captureHighMag = daq_utils.highMagCamURL logger.debug('highMagCamURL: "' + daq_utils.highMagCamURL + '"') def initVideo4(self, frequency): - self.captureHighMagZoom = cv2.VideoCapture(daq_utils.highMagZoomCamURL) + self.captureHighMagZoom = daq_utils.highMagZoomCamURL logger.debug('highMagZoomCamURL: "' + daq_utils.highMagZoomCamURL + '"') def initVideo3(self, frequency): - self.captureLowMagZoom = cv2.VideoCapture(daq_utils.lowMagZoomCamURL) + self.captureLowMagZoom = daq_utils.lowMagZoomCamURL logger.debug('lowMagZoomCamURL: "' + daq_utils.lowMagZoomCamURL + '"') def createSampleTab(self): @@ -379,6 +393,8 @@ def createSampleTab(self): self.closeShutterButton = QtWidgets.QPushButton("Close Photon Shutter") self.closeShutterButton.clicked.connect(self.closePhotonShutterCB) self.closeShutterButton.setStyleSheet("background-color: red") + self.parkRobotButton = QtWidgets.QPushButton("Park Robot") + self.parkRobotButton.clicked.connect(self.parkRobotCB) hBoxTreeButtsLayout = QtWidgets.QHBoxLayout() vBoxTreeButtsLayoutLeft = QtWidgets.QVBoxLayout() vBoxTreeButtsLayoutRight = QtWidgets.QVBoxLayout() @@ -390,6 +406,7 @@ def createSampleTab(self): vBoxTreeButtsLayoutLeft.addWidget(warmupButton) vBoxTreeButtsLayoutRight.addWidget(self.closeShutterButton) vBoxTreeButtsLayoutRight.addWidget(unmountSampleButton) + vBoxTreeButtsLayoutRight.addWidget(self.parkRobotButton) vBoxTreeButtsLayoutRight.addWidget(deQueueSelectedButton) vBoxTreeButtsLayoutRight.addWidget(emptyQueueButton) vBoxTreeButtsLayoutRight.addWidget(endVisitButton) @@ -410,7 +427,6 @@ def createSampleTab(self): colParamsGB = QtWidgets.QGroupBox() colParamsGB.setTitle("Acquisition") vBoxColParams1 = QtWidgets.QVBoxLayout() - hBoxColParams1 = QtWidgets.QHBoxLayout() colStartLabel = QtWidgets.QLabel("Oscillation Start:") colStartLabel.setFixedWidth(140) colStartLabel.setAlignment(QtCore.Qt.AlignCenter) @@ -430,11 +446,6 @@ def createSampleTab(self): ) if daq_utils.beamline == "fmx": self.osc_end_ledit.textChanged.connect(self.calcLifetimeCB) - hBoxColParams1.addWidget(colStartLabel) - hBoxColParams1.addWidget(self.osc_start_ledit) - hBoxColParams1.addWidget(self.colEndLabel) - hBoxColParams1.addWidget(self.osc_end_ledit) - hBoxColParams2 = QtWidgets.QHBoxLayout() colRangeLabel = QtWidgets.QLabel("Oscillation Width:") colRangeLabel.setFixedWidth(140) colRangeLabel.setAlignment(QtCore.Qt.AlignCenter) @@ -468,13 +479,6 @@ def createSampleTab(self): ) ) self.exp_time_ledit.textChanged.connect(self.checkEntryState) - hBoxColParams2.addWidget(colRangeLabel) - hBoxColParams2.addWidget(self.osc_range_ledit) - - hBoxColParams2.addWidget(colExptimeLabel) - hBoxColParams2.addWidget(self.exp_time_ledit) - hBoxColParams25 = QtWidgets.QHBoxLayout() - hBoxColParams25.addWidget(self.stillModeCheckBox) totalExptimeLabel = QtWidgets.QLabel("Total Exposure Time (s):") totalExptimeLabel.setFixedWidth(155) totalExptimeLabel.setAlignment(QtCore.Qt.AlignCenter) @@ -497,18 +501,11 @@ def createSampleTab(self): daq_utils.pvLookupDict["sampleLifetime"], self, 70, 2 ) self.sampleLifetimeReadback_ledit = self.sampleLifetimeReadback.getEntry() - else: + elif daq_utils.beamline == "fmx": calcLifetimeButton = QtWidgets.QPushButton("Calc. Lifetime") calcLifetimeButton.clicked.connect(self.calcLifetimeCB) self.sampleLifetimeReadback_ledit = QtWidgets.QLabel() self.calcLifetimeCB() - hBoxColParams25.addWidget(totalExptimeLabel) - hBoxColParams25.addWidget(self.totalExptime_ledit) - # if (daq_utils.beamline == "fmx"): - # hBoxColParams25.addWidget(calcLifetimeButton) - hBoxColParams25.addWidget(sampleLifetimeLabel) - hBoxColParams25.addWidget(self.sampleLifetimeReadback_ledit) - hBoxColParams22 = QtWidgets.QHBoxLayout() if daq_utils.beamline in ("fmx", "nyx"): if getBlConfig("attenType") == "RI": self.transmissionReadback = QtEpicsPVLabel( @@ -556,13 +553,19 @@ def createSampleTab(self): setTransButton = QtWidgets.QPushButton("Set Trans") setTransButton.clicked.connect(self.setTransCB) beamsizeLabel = QtWidgets.QLabel("BeamSize:") + if daq_utils.beamline == "nyx": + # beamSizeOptionList = self.aperture.get_diameter_list() PV not working, needs investigation + beamSizeOptionList = ["10", "20", "30", "50", "100"] + current_index = self.aperture.current_index.get() + else: + beamSizeOptionList = BEAMSIZE_OPTIONS.keys() + current_index = int(self.beamSize_pv.get()) self.beamsizeComboBox = QtWidgets.QComboBox(self) - self.beamsizeComboBox.addItems(BEAMSIZE_OPTIONS.keys()) - self.beamsizeComboBox.setCurrentIndex(int(self.beamSize_pv.get())) + self.beamsizeComboBox.addItems(beamSizeOptionList) + self.beamsizeComboBox.setCurrentIndex(current_index) self.beamsizeComboBox.activated[str].connect(self.beamsizeComboActivatedCB) if daq_utils.beamline == "amx" or self.energy_pv.get() < 9000: self.beamsizeComboBox.setEnabled(False) - hBoxColParams3 = QtWidgets.QHBoxLayout() colEnergyLabel = QtWidgets.QLabel("Energy (eV):") colEnergyLabel.setAlignment(QtCore.Qt.AlignCenter) self.energyMotorEntry = QtEpicsPVLabel( @@ -578,24 +581,6 @@ def createSampleTab(self): self.energy_ledit.returnPressed.connect(self.moveEnergyMaxDeltaCB) moveEnergyButton = QtWidgets.QPushButton("Move Energy") moveEnergyButton.clicked.connect(self.moveEnergyCB) - hBoxColParams3.addWidget(colEnergyLabel) - hBoxColParams3.addWidget(self.energyReadback) - hBoxColParams3.addWidget(energySPLabel) - if daq_utils.beamline == "fmx": - if getBlConfig(SET_ENERGY_CHECK): - hBoxColParams3.addWidget(moveEnergyButton) - else: - hBoxColParams3.addWidget(self.energy_ledit) - else: - hBoxColParams3.addWidget(self.energy_ledit) - - hBoxColParams22.addWidget(colTransmissionLabel) - hBoxColParams22.addWidget(self.transmissionReadback_ledit) - hBoxColParams22.addWidget(transmisionSPLabel) - hBoxColParams22.addWidget(self.transmission_ledit) - hBoxColParams22.insertSpacing(5, 100) - hBoxColParams22.addWidget(beamsizeLabel) - hBoxColParams22.addWidget(self.beamsizeComboBox) hBoxColParams4 = QtWidgets.QHBoxLayout() colBeamWLabel = QtWidgets.QLabel("Beam Width:") colBeamWLabel.setFixedWidth(140) @@ -607,11 +592,13 @@ def createSampleTab(self): colBeamHLabel.setAlignment(QtCore.Qt.AlignCenter) self.beamHeight_ledit = QtWidgets.QLineEdit() self.beamHeight_ledit.setFixedWidth(60) + if daq_utils.beamline == 'nyx': + self.beamWidth_ledit.setText(getBlConfig("screen_default_beamWidth")) + self.beamHeight_ledit.setText(getBlConfig("screen_default_beamHeight")) hBoxColParams4.addWidget(colBeamWLabel) hBoxColParams4.addWidget(self.beamWidth_ledit) hBoxColParams4.addWidget(colBeamHLabel) hBoxColParams4.addWidget(self.beamHeight_ledit) - hBoxColParams5 = QtWidgets.QHBoxLayout() colResoLabel = QtWidgets.QLabel("Edge Resolution:") colResoLabel.setAlignment(QtCore.Qt.AlignCenter) self.resolution_ledit = QtWidgets.QLineEdit() @@ -621,7 +608,7 @@ def createSampleTab(self): if daq_utils.beamline == "nyx": self.resolution_ledit.setEnabled(False) detDistLabel = QtWidgets.QLabel("Detector Dist.") - detDistLabel.setAlignment(QtCore.Qt.AlignCenter) + #detDistLabel.setAlignment(QtCore.Qt.AlignCenter) detDistRBLabel = QtWidgets.QLabel("Readback:") self.detDistRBVLabel = QtEpicsPVLabel( daq_utils.motor_dict["detectorDist"] + ".RBV", self, 70 @@ -645,10 +632,6 @@ def createSampleTab(self): self.detDistMotorEntry.getEntry().returnPressed.connect(self.moveDetDistCB) self.moveDetDistButton = QtWidgets.QPushButton("Move Detector") self.moveDetDistButton.clicked.connect(self.moveDetDistCB) - hBoxColParams3.addWidget(detDistLabel) - hBoxColParams3.addWidget(self.detDistRBVLabel.getEntry()) - hBoxColParams3.addWidget(detDistSPLabel) - hBoxColParams3.addWidget(self.detDistMotorEntry.getEntry()) hBoxColParams6 = QtWidgets.QHBoxLayout() hBoxColParams6.setAlignment(QtCore.Qt.AlignLeft) hBoxColParams7 = QtWidgets.QHBoxLayout() @@ -692,18 +675,22 @@ def createSampleTab(self): self.protoOtherRadio = QtWidgets.QRadioButton("other") self.protoOtherRadio.setEnabled(False) self.protoRadioGroup.addButton(self.protoOtherRadio) - protoOptionList = [ - "standard", - "raster", - "vector", - "burn", - "rasterScreen", - "stepRaster", - "stepVector", - "multiCol", - "characterize", - "ednaCol", - ] # these should probably come from db + if daq_utils.beamline == "nyx": + protoOptionList = ["standard","raster","vector"] # these should probably come from db + + else: + protoOptionList = [ + "standard", + "raster", + "vector", + "burn", + "rasterScreen", + "stepRaster", + "stepVector", + "multiCol", + "characterize", + "ednaCol", + ] # these should probably come from db self.protoComboBox = QtWidgets.QComboBox(self) self.protoComboBox.addItems(protoOptionList) self.protoComboBox.activated[str].connect(self.protoComboActivatedCB) @@ -714,8 +701,6 @@ def createSampleTab(self): hBoxColParams6.addWidget(self.protoComboBox) hBoxColParams7.addWidget(centeringLabel) hBoxColParams7.addWidget(self.centeringComboBox) - hBoxColParams7.addWidget(colResoLabel) - hBoxColParams7.addWidget(self.resolution_ledit) self.processingOptionsFrame = QFrame() self.hBoxProcessingLayout1 = QtWidgets.QHBoxLayout() self.hBoxProcessingLayout1.setAlignment(QtCore.Qt.AlignLeft) @@ -889,24 +874,84 @@ def createSampleTab(self): vector_widgets_layout.addLayout(hBoxVectorLayout2) self.vectorParamsFrame.setLayout(vector_widgets_layout) - vBoxColParams1.addLayout(hBoxColParams1) - vBoxColParams1.addLayout(hBoxColParams2) - vBoxColParams1.addLayout(hBoxColParams25) - vBoxColParams1.addLayout(hBoxColParams22) - vBoxColParams1.addLayout(hBoxColParams3) - vBoxColParams1.addLayout(hBoxColParams7) - vBoxColParams1.addLayout(hBoxColParams6) - vBoxColParams1.addWidget(self.rasterParamsFrame) - vBoxColParams1.addWidget(self.multiColParamsFrame) - vBoxColParams1.addWidget(self.vectorParamsFrame) - vBoxColParams1.addWidget(self.characterizeParamsFrame) - vBoxColParams1.addWidget(self.processingOptionsFrame) + + paramsGridGB = QtWidgets.QGroupBox() + paramsGridGB.setTitle("Acquisition") + + paramSubspace = QtWidgets.QGridLayout() + + + # Parameter Collection Column 1, Labels + colStartLabel.setAlignment(QtCore.Qt.AlignLeft) + paramSubspace.addWidget(colStartLabel,1,0, alignment=QtCore.Qt.AlignLeft) + self.colEndLabel.setAlignment(QtCore.Qt.AlignLeft) + paramSubspace.addWidget(self.colEndLabel,2,0, alignment=QtCore.Qt.AlignLeft) + colRangeLabel.setAlignment(QtCore.Qt.AlignLeft) + paramSubspace.addWidget(colRangeLabel,0,0, alignment=QtCore.Qt.AlignLeft) + colExptimeLabel.setAlignment(QtCore.Qt.AlignLeft) + paramSubspace.addWidget(colExptimeLabel,3,0, alignment=QtCore.Qt.AlignLeft) + totalExptimeLabel.setAlignment(QtCore.Qt.AlignLeft) + paramSubspace.addWidget(totalExptimeLabel,4,0, alignment=QtCore.Qt.AlignLeft) + if daq_utils.beamline in ['amx', 'fmx']: + paramSubspace.addWidget(sampleLifetimeLabel, 5, 0, alignment=QtCore.Qt.AlignLeft) + # Parameter Collection Column 2, Input Boxes + paramSubspace.addWidget(self.osc_start_ledit,1,1, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(self.osc_end_ledit,2,1, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(self.osc_range_ledit,0,1, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(self.exp_time_ledit,3,1, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(self.totalExptime_ledit,4,1, alignment=QtCore.Qt.AlignLeft) + # Parameter Collection Column 3, Labels + paramSubspace.addWidget(detDistLabel,0,2, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(colResoLabel,1,2, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(colEnergyLabel,2,2, alignment=QtCore.Qt.AlignLeft) + colTransmissionLabel.setAlignment(QtCore.Qt.AlignLeft) + paramSubspace.addWidget(colTransmissionLabel,3,2, alignment=QtCore.Qt.AlignLeft) + transmisionSPLabel.setAlignment(QtCore.Qt.AlignLeft) + paramSubspace.addWidget(beamsizeLabel,4,2, alignment=QtCore.Qt.AlignLeft) + if daq_utils.beamline in ['amx', 'fmx']: + paramSubspace.addWidget(self.sampleLifetimeReadback_ledit, 5, 1, alignment=QtCore.Qt.AlignLeft) + + # Parameter Collection Column 4, Input Boxes + paramSubspace.addWidget(self.detDistMotorEntry.getEntry(),0,3, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(self.resolution_ledit,1,3, alignment=QtCore.Qt.AlignLeft) + if daq_utils.beamline == "fmx": + if getBlConfig(SET_ENERGY_CHECK): + paramSubspace.addWidget(moveEnergyButton,2,3, alignment=QtCore.Qt.AlignLeft) + else: + paramSubspace.addWidget(self.energy_ledit,2,3, alignment=QtCore.Qt.AlignLeft) + else: + paramSubspace.addWidget(self.energy_ledit,2,3, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(self.transmission_ledit,3,3, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(self.beamsizeComboBox,4,3, alignment=QtCore.Qt.AlignLeft) + + # Param Collection Column 5, RBV + paramSubspace.addWidget(self.energyReadback,2,4, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(self.detDistRBVLabel.getEntry(),0,4, alignment=QtCore.Qt.AlignLeft) + paramSubspace.addWidget(self.transmissionReadback_ledit,3,4, alignment=QtCore.Qt.AlignLeft) + + improvedParamSpacing = QtWidgets.QVBoxLayout() + improvedParamSpacing.addWidget(self.stillModeCheckBox) + improvedParamSpacing.addLayout(paramSubspace) + improvedParamSpacing.addLayout(hBoxColParams7) + improvedParamSpacing.addLayout(hBoxColParams6) + improvedParamSpacing.addWidget(self.rasterParamsFrame) + improvedParamSpacing.addWidget(self.multiColParamsFrame) + improvedParamSpacing.addWidget(self.vectorParamsFrame) + improvedParamSpacing.addWidget(self.characterizeParamsFrame) + improvedParamSpacing.addWidget(self.processingOptionsFrame) + paramsGridGB.setLayout(improvedParamSpacing) + self.rasterParamsFrame.hide() self.multiColParamsFrame.hide() self.characterizeParamsFrame.hide() colParamsGB.setLayout(vBoxColParams1) self.dataPathGB = DataLocInfo(self) - vBoxMainColLayout.addWidget(colParamsGB) + hBoxDisplayOptionLayout= QtWidgets.QHBoxLayout() + self.albulaDispCheckBox = QCheckBox("Display Data (Albula)") + self.albulaDispCheckBox.setChecked(False) + hBoxDisplayOptionLayout.addWidget(self.albulaDispCheckBox) + vBoxMainColLayout.addWidget(paramsGridGB) + vBoxMainColLayout.addWidget(self.dataPathGB) self.mainColFrame.setLayout(vBoxMainColLayout) self.mainToolBox.addItem(self.mainColFrame, "Collection Parameters") @@ -964,17 +1009,24 @@ def createSampleTab(self): self.initVideo3, (0.25,) ) # this sets up lowMagDigiZoom if self.zoom1FrameRatePV.get() != 0: - self.captureLowMag = cv2.VideoCapture(daq_utils.lowMagCamURL) + #self.captureLowMag = cv2.VideoCapture(daq_utils.lowMagCamURL) + self.captureLowMag = daq_utils.lowMagCamURL logger.debug('lowMagCamURL: "' + daq_utils.lowMagCamURL + '"') + + #self.captureLowMag = cv2.VideoCapture(daq_utils.lowMagCamURL) self.capture = self.captureLowMag + #self.frame_queue = Queue() + #self.active_camera_threads = [] self.timerSample = QTimer() - self.timerSample.timeout.connect(self.timerSampleRefresh) - self.timerSample.start(SAMPLE_TIMER_DELAY) + #self.timerSample.timeout.connect(self.sampleFrameCB) + #self.timerSample.timeout.connect(self.timerSampleRefresh) + #self.timerSample.start(SAMPLE_TIMER_DELAY) self.centeringMarksList = [] self.rasterList = [] self.rasterDefList = [] self.polyPointItems = [] + self.threeClickLines = [] self.rasterPoly = None self.measureLine = None self.scene = QtWidgets.QGraphicsScene(0, 0, 640, 512, self) @@ -1011,8 +1063,8 @@ def createSampleTab(self): self.centerMarker.setFont(font) self.scene.addItem(self.centerMarker) self.centerMarker.setPos( - daq_utils.screenPixCenterX - self.centerMarkerCharOffsetX, - daq_utils.screenPixCenterY - self.centerMarkerCharOffsetY, + self.getBeamCenterX() - self.centerMarkerCharOffsetX, + self.getBeamCenterY() - self.centerMarkerCharOffsetY, ) self.zoomRadioGroup = QtWidgets.QButtonGroup() self.zoom1Radio = QtWidgets.QRadioButton("Mag1") @@ -1039,6 +1091,18 @@ def createSampleTab(self): self.zoomRadioGroup.addButton(self.zoom3Radio) if daq_utils.sampleCameraCount >= 4: self.zoomRadioGroup.addButton(self.zoom4Radio) + else: + self.zoomLevelComboBox = QtWidgets.QComboBox(self) + self.zoomLevelComboBox.addItems(["1","2","3","4","5","6","7"]) + self.zoomLevelComboBox.activated[str].connect(self.zoomLevelComboActivatedCB) + self.zoom1Radio.hide() + self.zoom2Radio.hide() + self.zoom3Radio.hide() + self.zoom4Radio.hide() + hBoxZoomLevelLayout = QtWidgets.QHBoxLayout() + hBoxZoomLevelLayout.addWidget(self.zoomLevelComboBox) + + beamOverlayPen = QtGui.QPen(QtCore.Qt.red) self.tempBeamSizeXMicrons = 30 self.tempBeamSizeYMicrons = 30 @@ -1096,14 +1160,14 @@ def createSampleTab(self): setDC2CPButton.setFixedWidth(50) setDC2CPButton.clicked.connect(self.setDCStartCB) omegaLabel = QtWidgets.QLabel("Omega:") - omegaMonitorPV = str(getBlConfig("omegaMonitorPV")) + #omegaMonitorPV = str(getBlConfig("omegaMonitorPV")) self.sampleOmegaRBVLedit = QtEpicsPVLabel( - daq_utils.motor_dict["omega"] + "." + omegaMonitorPV, self, 70 + self.gon.omega.readback.pvname, self, 70 ) omegaSPLabel = QtWidgets.QLabel("SetPoint:") omegaSPLabel.setFixedWidth(70) self.sampleOmegaMoveLedit = QtEpicsPVEntry( - daq_utils.motor_dict["omega"] + ".VAL", self, 70, 2 + self.gon.omega.setpoint.pvname, self, 70, 2 ) self.sampleOmegaMoveLedit.getEntry().returnPressed.connect(self.moveOmegaCB) moveOmegaButton = QtWidgets.QPushButton("Move") @@ -1186,6 +1250,8 @@ def createSampleTab(self): hBoxVidControlLayout.addWidget(self.zoom3Radio) if daq_utils.sampleCameraCount >= 4: hBoxVidControlLayout.addWidget(self.zoom4Radio) + else: + hBoxVidControlLayout.addLayout(hBoxZoomLevelLayout) hBoxVidControlLayout.addWidget(focusLabel) hBoxVidControlLayout.addWidget(focusPlusButton) hBoxVidControlLayout.addWidget(focusMinusButton) @@ -1208,6 +1274,7 @@ def createSampleTab(self): clearGraphicsButton.clicked.connect(self.eraseCB) self.click3Button = QtWidgets.QPushButton("3-Click\nCenter") self.click3Button.clicked.connect(self.center3LoopCB) + self.threeClickCount = 0 saveCenteringButton = QtWidgets.QPushButton("Save\nCenter") saveCenteringButton.clicked.connect(self.saveCenterCB) @@ -1221,6 +1288,7 @@ def createSampleTab(self): hBoxSampleAlignLayout.addWidget(self.click3Button) hBoxSampleAlignLayout.addWidget(snapshotButton) hBoxSampleAlignLayout.addWidget(self.hideRastersCheckBox) + self.click3Button.setMaximumSize(self.click3Button.sizeHint()) hBoxRadioLayout100 = QtWidgets.QHBoxLayout() vidActionLabel = QtWidgets.QLabel("Video Click Mode:") self.vidActionRadioGroup = QtWidgets.QButtonGroup() @@ -1240,6 +1308,7 @@ def createSampleTab(self): self.vidActionRasterSelectRadio = QtWidgets.QRadioButton("Raster Select") self.vidActionRasterSelectRadio.setChecked(False) self.vidActionRasterSelectRadio.toggled.connect(self.vidActionToggledCB) + self.vidActionRadioGroup.addButton(self.vidActionRasterSelectRadio) self.vidActionRasterDefRadio = QtWidgets.QRadioButton("Define Raster") self.vidActionRasterDefRadio.setChecked(False) self.vidActionRasterDefRadio.setEnabled(False) @@ -1373,6 +1442,15 @@ def createSampleTab(self): ringCurrentMessageLabel = QtWidgets.QLabel("Ring (mA):") self.ringCurrentMessage = QtWidgets.QLabel(str(self.ringCurrent_pv.get())) beamAvailable = self.beamAvailable_pv.get() + + ''' + changing beam available label + + + + ''' + + if beamAvailable: self.beamAvailLabel = QtWidgets.QLabel("Beam Available") self.beamAvailLabel.setStyleSheet("background-color: #99FF66;") @@ -1399,6 +1477,13 @@ def createSampleTab(self): else: self.cryostreamTempLabel = QtWidgets.QLabel("N/A") + + + + ''' + Adding bottom labels to gui + + ''' fileHBoxLayout.addWidget(gripperLabel) fileHBoxLayout.addWidget(self.gripperTempLabel) fileHBoxLayout.addWidget(cryostreamLabel) @@ -1418,27 +1503,52 @@ def createSampleTab(self): # 12/19 - uncomment this to expose the PyMCA XRF interface. It's not connected to anything. self.zoomLevelToggledCB("Zoom1") - if daq_utils.beamline == "nyx": # Temporarily disabling unusued buttons on NYX - self.protoRasterRadio.setDisabled(True) - self.protoStandardRadio.setDisabled(True) - self.protoVectorRadio.setDisabled(True) - self.protoOtherRadio.setDisabled(True) - self.autoProcessingCheckBox.setDisabled(True) - self.fastEPCheckBox.setDisabled(True) - self.dimpleCheckBox.setDisabled(True) - self.centeringComboBox.setDisabled(True) - self.beamsizeComboBox.setDisabled(True) - annealButton.setDisabled(True) - centerLoopButton.setDisabled(True) - clearGraphicsButton.setDisabled(True) - saveCenteringButton.setDisabled(True) - selectAllCenteringButton.setDisabled(True) - snapshotButton.setDisabled(True) - self.hideRastersCheckBox.setDisabled(True) - self.vidActionC2CRadio.setDisabled(True) - self.vidActionRasterExploreRadio.setDisabled(True) - self.vidActionRasterDefRadio.setDisabled(True) - self.vidActionDefineCenterRadio.setDisabled(True) + if daq_utils.beamline == "nyx": # hiding unused GUI elements + self.protoRasterRadio.setVisible(False) + self.protoStandardRadio.setVisible(False) + self.protoVectorRadio.setVisible(False) + self.protoOtherRadio.setVisible(False) + self.autoProcessingCheckBox.setVisible(False) + self.fastEPCheckBox.setVisible(False) + self.dimpleCheckBox.setVisible(False) + self.centeringComboBox.setVisible(False) + annealButton.setVisible(False) + centerLoopButton.setVisible(False) + clearGraphicsButton.setVisible(False) + saveCenteringButton.setVisible(False) + selectAllCenteringButton.setVisible(False) + snapshotButton.setVisible(False) + annealTimeLabel.setVisible(False) + self.annealTime_ledit.setVisible(False) + self.vidActionDefineCenterRadio.setVisible(False) + self.hideRastersCheckBox.setEnabled(True) + self.vidActionC2CRadio.setEnabled(True) + self.vidActionRasterExploreRadio.setEnabled(True) + self.vidActionRasterDefRadio.setEnabled(True) + + + + #self.captureLowMag = cv2.VideoCapture(daq_utils.lowMagCamURL) + #self.captureLowMag.set(cv2.CAP_PROP_BUFFERSIZE, 1) + self.captureLowMag = daq_utils.lowMagCamURL + self.capture = self.captureLowMag + + if daq_utils.beamline == "nyx": + self.sampleCameraThread = VideoThread( + parent=self, delay=HUTCH_TIMER_DELAY, url=daq_utils.highMagCamURL + ) + else: + self.sampleCameraThread = VideoThread( + parent=self, delay=SAMPLE_TIMER_DELAY, mjpg_url=self.capture + ) + self.sampleZoomChangeSignal.connect(self.sampleCameraThread.updateCam) + + self.sampleCameraThread.frame_ready.connect( + lambda frame: self.updateCam(self.pixmap_item, frame) + ) + self.sampleCameraThread.start() + + self.hutchCornerCamThread = VideoThread( parent=self, delay=HUTCH_TIMER_DELAY, url=getBlConfig("hutchCornerCamURL") @@ -1460,7 +1570,11 @@ def createSampleTab(self): serverCheckThread.start() def updateCam(self, pixmapItem: "QGraphicsPixmapItem", frame): - pixmapItem.setPixmap(frame) + if pixmapItem == self.pixmap_item: + with QMutexLocker(self.sampleCameraMutex): + pixmapItem.setPixmap(frame) + else: + pixmapItem.setPixmap(frame) def annealButtonCB(self): try: @@ -1584,9 +1698,9 @@ def adjustGraphics4ZoomChange(self, fov): self.processSampMove(self.sampy_pv.get(), "y") self.processSampMove(self.sampz_pv.get(), "z") if self.centeringMarksList != []: - self.processSampMove(self.sampx_pv.get(), "x") - self.processSampMove(self.sampy_pv.get(), "y") - self.processSampMove(self.sampz_pv.get(), "z") + self.processSampMove(self.gon.x.val(), "x") + self.processSampMove(self.gon.y.val(), "y") + self.processSampMove(self.gon.z.val(), "z") def flushBuffer(self, vidStream): if vidStream == None: @@ -1599,29 +1713,35 @@ def flushBuffer(self, vidStream): if commTime > 0.01: return + def zoomLevelComboActivatedCB(self, identifier): + self.camera.zoom.put(identifier) + self.centerMarker.setPos(self.getBeamCenterX()-self.centerMarkerCharOffsetX, self.getBeamCenterY()-self.centerMarkerCharOffsetY) + #self.flushBuffer(self.capture) + #self.capture.release() + #self.capture = cv2.VideoCapture(daq_utils.lowMagZoomCamURL) + def zoomLevelToggledCB(self, identifier): fov = {} - zoomedCursorX = daq_utils.screenPixCenterX - self.centerMarkerCharOffsetX - zoomedCursorY = daq_utils.screenPixCenterY - self.centerMarkerCharOffsetY + zoomedCursorX = self.getBeamCenterX() - self.centerMarkerCharOffsetX + zoomedCursorY = self.getBeamCenterY() - self.centerMarkerCharOffsetY if self.zoom2Radio.isChecked(): - self.flushBuffer(self.captureLowMagZoom) self.capture = self.captureLowMagZoom fov["x"] = daq_utils.lowMagFOVx / 2.0 fov["y"] = daq_utils.lowMagFOVy / 2.0 unzoomedCursorX = self.lowMagCursorX_pv.get() - self.centerMarkerCharOffsetX unzoomedCursorY = self.lowMagCursorY_pv.get() - self.centerMarkerCharOffsetY - if unzoomedCursorX * 2.0 < daq_utils.screenPixCenterX: + if unzoomedCursorX * 2.0 < self.getBeamCenterX(): zoomedCursorX = unzoomedCursorX * 2.0 - if unzoomedCursorY * 2.0 < daq_utils.screenPixCenterY: + if unzoomedCursorY * 2.0 < self.getBeamCenterY(): zoomedCursorY = unzoomedCursorY * 2.0 if ( - unzoomedCursorX - daq_utils.screenPixCenterX - > daq_utils.screenPixCenterX / 2 + unzoomedCursorX - self.getBeamCenterX() + > self.getBeamCenterX() / 2 ): zoomedCursorX = (unzoomedCursorX * 2.0) - daq_utils.screenPixX if ( - unzoomedCursorY - daq_utils.screenPixCenterY - > daq_utils.screenPixCenterY / 2 + unzoomedCursorY - self.getBeamCenterY() + > self.getBeamCenterY() / 2 ): zoomedCursorY = (unzoomedCursorY * 2.0) - daq_utils.screenPixY self.centerMarker.setPos(zoomedCursorX, zoomedCursorY) @@ -1638,7 +1758,6 @@ def zoomLevelToggledCB(self, identifier): self.beamSizeYPixels, ) elif self.zoom1Radio.isChecked(): - self.flushBuffer(self.captureLowMag) self.capture = self.captureLowMag fov["x"] = daq_utils.lowMagFOVx fov["y"] = daq_utils.lowMagFOVy @@ -1659,7 +1778,6 @@ def zoomLevelToggledCB(self, identifier): self.beamSizeYPixels, ) elif self.zoom4Radio.isChecked(): - self.flushBuffer(self.captureHighMagZoom) self.capture = self.captureHighMagZoom fov["x"] = daq_utils.highMagFOVx / 2.0 fov["y"] = daq_utils.highMagFOVy / 2.0 @@ -1669,18 +1787,18 @@ def zoomLevelToggledCB(self, identifier): unzoomedCursorY = ( self.highMagCursorY_pv.get() - self.centerMarkerCharOffsetY ) - if unzoomedCursorX * 2.0 < daq_utils.screenPixCenterX: + if unzoomedCursorX * 2.0 < self.getBeamCenterX(): zoomedCursorX = unzoomedCursorX * 2.0 - if unzoomedCursorY * 2.0 < daq_utils.screenPixCenterY: + if unzoomedCursorY * 2.0 < self.getBeamCenterY(): zoomedCursorY = unzoomedCursorY * 2.0 if ( - unzoomedCursorX - daq_utils.screenPixCenterX - > daq_utils.screenPixCenterX / 2 + unzoomedCursorX - self.getBeamCenterX() + > self.getBeamCenterX() / 2 ): zoomedCursorX = (unzoomedCursorX * 2.0) - daq_utils.screenPixX if ( - unzoomedCursorY - daq_utils.screenPixCenterY - > daq_utils.screenPixCenterY / 2 + unzoomedCursorY - self.getBeamCenterY() + > self.getBeamCenterY() / 2 ): zoomedCursorY = (unzoomedCursorY * 2.0) - daq_utils.screenPixY self.centerMarker.setPos(zoomedCursorX, zoomedCursorY) @@ -1697,7 +1815,6 @@ def zoomLevelToggledCB(self, identifier): self.beamSizeYPixels, ) elif self.zoom3Radio.isChecked(): - self.flushBuffer(self.captureHighMag) self.capture = self.captureHighMag fov["x"] = daq_utils.highMagFOVx fov["y"] = daq_utils.highMagFOVy @@ -1723,7 +1840,8 @@ def zoomLevelToggledCB(self, identifier): def saveVidSnapshotButtonCB(self): comment, useOlog, ok = SnapCommentDialog.getComment() if ok: - self.saveVidSnapshotCB(comment, useOlog) + with QMutexLocker(self.sampleCameraMutex): + self.saveVidSnapshotCB(comment, useOlog) def saveVidSnapshotCB( self, comment="", useOlog=False, reqID=None, rasterHeatJpeg=None @@ -1841,23 +1959,23 @@ def processROIChange(self, posRBV, ID): pass def processLowMagCursorChange(self, posRBV, ID): - zoomedCursorX = daq_utils.screenPixCenterX - self.centerMarkerCharOffsetX - zoomedCursorY = daq_utils.screenPixCenterY - self.centerMarkerCharOffsetY + zoomedCursorX = self.getBeamCenterX() - self.centerMarkerCharOffsetX + zoomedCursorY = self.getBeamCenterY() - self.centerMarkerCharOffsetY if self.zoom2Radio.isChecked(): # lowmagzoom unzoomedCursorX = self.lowMagCursorX_pv.get() - self.centerMarkerCharOffsetX unzoomedCursorY = self.lowMagCursorY_pv.get() - self.centerMarkerCharOffsetY - if unzoomedCursorX * 2.0 < daq_utils.screenPixCenterX: + if unzoomedCursorX * 2.0 < self.getBeamCenterX(): zoomedCursorX = unzoomedCursorX * 2.0 - if unzoomedCursorY * 2.0 < daq_utils.screenPixCenterY: + if unzoomedCursorY * 2.0 < self.getBeamCenterY(): zoomedCursorY = unzoomedCursorY * 2.0 if ( - unzoomedCursorX - daq_utils.screenPixCenterX - > daq_utils.screenPixCenterX / 2 + unzoomedCursorX - self.getBeamCenterX() + > self.getBeamCenterX() / 2 ): zoomedCursorX = (unzoomedCursorX * 2.0) - daq_utils.screenPixX if ( - unzoomedCursorY - daq_utils.screenPixCenterY - > daq_utils.screenPixCenterY / 2 + unzoomedCursorY - self.getBeamCenterY() + > self.getBeamCenterY() / 2 ): zoomedCursorY = (unzoomedCursorY * 2.0) - daq_utils.screenPixY self.centerMarker.setPos(zoomedCursorX, zoomedCursorY) @@ -1892,8 +2010,8 @@ def processLowMagCursorChange(self, posRBV, ID): ) def processHighMagCursorChange(self, posRBV, ID): - zoomedCursorX = daq_utils.screenPixCenterX - self.centerMarkerCharOffsetX - zoomedCursorY = daq_utils.screenPixCenterY - self.centerMarkerCharOffsetY + zoomedCursorX = self.getBeamCenterX() - self.centerMarkerCharOffsetX + zoomedCursorY = self.getBeamCenterY() - self.centerMarkerCharOffsetY if self.zoom4Radio.isChecked(): # highmagzoom unzoomedCursorX = ( self.highMagCursorX_pv.get() - self.centerMarkerCharOffsetX @@ -1901,18 +2019,18 @@ def processHighMagCursorChange(self, posRBV, ID): unzoomedCursorY = ( self.highMagCursorY_pv.get() - self.centerMarkerCharOffsetY ) - if unzoomedCursorX * 2.0 < daq_utils.screenPixCenterX: + if unzoomedCursorX * 2.0 < self.getBeamCenterX(): zoomedCursorX = unzoomedCursorX * 2.0 - if unzoomedCursorY * 2.0 < daq_utils.screenPixCenterY: + if unzoomedCursorY * 2.0 < self.getBeamCenterY(): zoomedCursorY = unzoomedCursorY * 2.0 if ( - unzoomedCursorX - daq_utils.screenPixCenterX - > daq_utils.screenPixCenterX / 2 + unzoomedCursorX - self.getBeamCenterX() + > self.getBeamCenterX() / 2 ): zoomedCursorX = (unzoomedCursorX * 2.0) - daq_utils.screenPixX if ( - unzoomedCursorY - daq_utils.screenPixCenterY - > daq_utils.screenPixCenterY / 2 + unzoomedCursorY - self.getBeamCenterY() + > self.getBeamCenterY() / 2 ): zoomedCursorY = (unzoomedCursorY * 2.0) - daq_utils.screenPixY self.centerMarker.setPos(zoomedCursorX, zoomedCursorY) @@ -2055,6 +2173,12 @@ def processMountedPin(self, mountedPinPos): self.eraseCB() self.treeChanged_pv.put(1) + + ''' + functions to bottom status variables + + ''' + def processFastShutter(self, shutterVal): if round(shutterVal) == round(self.fastShutterOpenPos_pv.get()): self.shutterStateLabel.setText("Shutter State:Open") @@ -2087,13 +2211,26 @@ def processRingCurrent(self, ringCurrentVal): else: self.ringCurrentMessage.setStyleSheet("background-color: #99FF66;") - def processBeamAvailable(self, beamAvailVal): - if int(beamAvailVal) == 1: - self.beamAvailLabel.setText("Beam Available") - self.beamAvailLabel.setStyleSheet("background-color: #99FF66;") + ''' + change beam abailable set text depending on input + + function is processThreClickCentering + ''' + def processThreeClickCentering(self, beamAvailVal): + if daq_utils.beamline == 'nyx': + if beamAvailVal == '0': + self.beamAvailLabel.setText("Beam Available") + self.beamAvailLabel.setStyleSheet("background-color: #99FF66;") + else: + self.beamAvailLabel.setText(beamAvailVal) + self.beamAvailLabel.setStyleSheet("background-color: yellow") else: - self.beamAvailLabel.setText("No Beam") - self.beamAvailLabel.setStyleSheet("background-color: red;") + if beamAvailVal == "1": + self.beamAvailLabel.setText("Beam Available") + self.beamAvailLabel.setStyleSheet("background-color: #99FF66;") + elif beamAvailVal == "0": + self.beamAvailLabel.setText("No Beam") + self.beamAvailLabel.setStyleSheet("background-color: red") def processSampleExposed(self, sampleExposedVal): if int(sampleExposedVal) == 1: @@ -2492,8 +2629,11 @@ def protoRadioToggledCB(self, text): pass def beamsizeComboActivatedCB(self, text): - self.send_to_server("set_beamsize", BEAMSIZE_OPTIONS[text]) - + if daq_utils.beamline == "nyx": + index = self.beamsizeComboBox.findText(str(text)) + self.aperture.current_index.put(index) + else: + self.send_to_server("set_beamsize", BEAMSIZE_OPTIONS[text]) def protoComboActivatedCB(self, text): self.showProtParams() @@ -2586,9 +2726,13 @@ def popBaseDirectoryDialogCB(self): ) if fname != "": self.dataPathGB.setBasePath_ledit(fname) + ''' + Creating function to for Actions under file drop down + + ''' def popImportDialogCB(self): - self.timerSample.stop() + #self.timerSample.stop() fname = QtWidgets.QFileDialog.getOpenFileName( self, "Choose Spreadsheet File", @@ -2596,7 +2740,7 @@ def popImportDialogCB(self): filter="*.xls *.xlsx", options=QtWidgets.QFileDialog.DontUseNativeDialog, ) - self.timerSample.start(SAMPLE_TIMER_DELAY) + #self.timerSample.start(SAMPLE_TIMER_DELAY) if fname != "": self.send_to_server("importSpreadsheet", [fname[0], daq_utils.owner]) @@ -2605,6 +2749,14 @@ def setUserModeCB(self): def setExpertModeCB(self): self.vidActionDefineCenterRadio.setEnabled(True) + + + def openResolution(self): + self.sub = CalculatorWindow(self) + self.sub.show() + + + def upPriorityCB( self, @@ -2818,13 +2970,13 @@ def focusTweakCB(self, tv): if self.controlEnabled(): tvY = tvf * ( - math.cos(math.radians(view_omega_offset + self.motPos["omega"])) + math.cos(math.radians(view_omega_offset + self.gon.omega.val())) ) # these are opposite C2C tvZ = tvf * ( - math.sin(math.radians(view_omega_offset + self.motPos["omega"])) + math.sin(math.radians(view_omega_offset + self.gon.omega.val())) ) - self.sampyTweak_pv.put(tvY) - self.sampzTweak_pv.put(tvZ) + self.gon.y.move(self.gon.y.val() + tvY) + self.gon.z.move(self.gon.z.val() + tvZ) else: self.popupServerMessage("You don't have control") @@ -2933,8 +3085,13 @@ def measurePolyCB(self): def center3LoopCB(self): logger.info("3-click center loop") self.threeClickCount = 1 + self.threeClickSignal.emit('{} more clicks'.format(str(4-self.threeClickCount))) + #time.sleep(0.3) self.click3Button.setStyleSheet("background-color: yellow") - self.send_to_server("mvaDescriptor", ["omega", 0]) + if(daq_utils.exporter_enabled): + self.md2.exporter.cmd("startManualSampleCentring", "") + else: + self.send_to_server("mvaDescriptor", ["omega", 0]) def fillPolyRaster( self, rasterReq @@ -3008,7 +3165,11 @@ def fillPolyRaster( ): # this is trying to figure out row direction cellIndex = spotLineCounter else: - if i % 2 == 0: # this is trying to figure out row direction + if daq_utils.beamline == "nyx": + is_raster_inverted = 1 + else: + is_raster_inverted = 0 + if i % 2 == is_raster_inverted: # this is trying to figure out row direction cellIndex = spotLineCounter else: cellIndex = rowStartIndex + ((numsteps - 1) - j) @@ -3185,9 +3346,9 @@ def saveCenterCB(self): marker.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) self.centeringMark = { "sampCoords": { - "x": self.sampx_pv.get(), - "y": self.sampy_pv.get(), - "z": self.sampz_pv.get(), + "x": self.gon.x.val(), + "y": self.gon.y.val(), + "z": self.gon.z.val(), }, "graphicsItem": marker, "centerCursorX": self.centerMarker.x(), @@ -3272,24 +3433,70 @@ def getCurrentFOV(self): return fov def screenXPixels2microns(self, pixels): - fov = self.getCurrentFOV() - fovX = fov["x"] - return float(pixels) * (fovX / daq_utils.screenPixX) + if daq_utils.beamline == 'nyx': + img_scale_factor = self.getMD2ImageXRatio() + pixels_per_mm = 1 / self.camera.scale_x.get() + pixels_per_micron = pixels_per_mm / 1000.0 + return float(pixels * img_scale_factor) / pixels_per_micron + else: + fov = self.getCurrentFOV() + fovX = fov["x"] + return float(pixels) * (fovX / daq_utils.screenPixX) def screenYPixels2microns(self, pixels): - fov = self.getCurrentFOV() - fovY = fov["y"] - return float(pixels) * (fovY / daq_utils.screenPixY) + if daq_utils.beamline == 'nyx': + pixels_per_mm = 1 / self.camera.scale_y.get() + pixels_per_micron = pixels_per_mm / 1000.0 + img_scale_factor = self.getMD2ImageYRatio() + return float(pixels * img_scale_factor) / pixels_per_micron + else: + fov = self.getCurrentFOV() + fovY = fov["y"] + return float(pixels) * (fovY / daq_utils.screenPixY) def screenXmicrons2pixels(self, microns): - fov = self.getCurrentFOV() - fovX = fov["x"] - return int(round(microns * (daq_utils.screenPixX / fovX))) + if daq_utils.beamline == 'nyx': + pixels_per_mm = 1 / self.camera.scale_x.get() + pixels_per_micron = pixels_per_mm / 1000.0 + img_scale_factor = self.getMD2ImageXRatio() + return float(microns * pixels_per_micron) / img_scale_factor + else: + fov = self.getCurrentFOV() + fovX = fov["x"] + return int(round(microns * (daq_utils.screenPixX / fovX))) def screenYmicrons2pixels(self, microns): - fov = self.getCurrentFOV() - fovY = fov["y"] - return int(round(microns * (daq_utils.screenPixY / fovY))) + if daq_utils.beamline == 'nyx': + pixels_per_mm = 1 / self.camera.scale_y.get() + pixels_per_micron = pixels_per_mm / 1000.0 + img_scale_factor = self.getMD2ImageYRatio() + return float(microns * pixels_per_micron) / img_scale_factor + else: + fov = self.getCurrentFOV() + fovY = fov["y"] + return int(round(microns * (daq_utils.screenPixY / fovY))) + + def getMD2ImageXRatio(self): + md2_img_width = daq_utils.highMagPixX + lsdc_img_width = daq_utils.screenPixX + return float(md2_img_width) / float(lsdc_img_width) + + def getMD2ImageYRatio(self): + md2_img_height = daq_utils.highMagPixY + lsdc_img_height = daq_utils.screenPixY + return float(md2_img_height) / float(lsdc_img_height) + + def getBeamCenterX(self): + if daq_utils.beamline == 'nyx': + return self.md2.center_pixel_x.get() / self.getMD2ImageXRatio() + else: + return daq_utils.screenPixCenterX + + def getBeamCenterY(self): + if daq_utils.beamline == 'nyx': + return self.md2.center_pixel_y.get() / self.getMD2ImageYRatio() + else: + return daq_utils.screenPixCenterY def definePolyRaster( self, raster_w, raster_h, stepsizeXPix, stepsizeYPix, point_x, point_y, stepsize @@ -3316,10 +3523,10 @@ def definePolyRaster( "beamWidth": beamWidth, "beamHeight": beamHeight, "status": RasterStatus.NEW.value, - "x": self.sampx_pv.get() + self.sampFineX_pv.get(), - "y": self.sampy_pv.get() + self.sampFineY_pv.get(), - "z": self.sampz_pv.get() + self.sampFineZ_pv.get(), - "omega": self.omega_pv.get(), + "x": self.gon.x.val() + self.sampFineX_pv.get(), + "y": self.gon.y.val() + self.sampFineY_pv.get(), + "z": self.gon.z.val() + self.sampFineZ_pv.get(), + "omega": self.gon.omega.val(), "stepsize": stepsize, "rowDefs": [], } # just storing step as microns, not using her @@ -3329,10 +3536,10 @@ def definePolyRaster( "beamWidth": beamWidth, "beamHeight": beamHeight, "status": RasterStatus.NEW.value, - "x": self.sampx_pv.get(), - "y": self.sampy_pv.get(), - "z": self.sampz_pv.get(), - "omega": self.omega_pv.get(), + "x": self.gon.x.val(), + "y": self.gon.y.val(), + "z": self.gon.z.val(), + "omega": self.gon.omega.val(), "stepsize": stepsize, "rowDefs": [], } # just storing step as microns, not using here @@ -3505,10 +3712,33 @@ def drawPolyRaster( } self.rasterList.append(newRasterGraphicsDesc) + def sampleFrameCB(self): + frames = str(self.capture.get(cv2.CAP_PROP_BUFFERSIZE)) + text = frames + " frames" + self.imageScaleText = self.scene.addSimpleText( + text, font=QtGui.QFont("Times", 13) + ) + ''' + for thread in self.active_camera_threads: + if not thread.is_alive(): # remove old threads + self.active_camera_threads.remove(thread) + if not len(self.active_camera_threads) > 0: + self.active_camera_threads.append(threading.Thread(target=self.timerSampleRefresh)) + self.active_camera_threads[-1].start() + + if not self.frame_queue.empty(): + self.pixmap_item.setPixmap(self.frame_queue.get()) + ''' def timerSampleRefresh(self): if self.capture is None: return + # uncomment this for frame resizing + # self.currentFrame = self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, 640) + # self.currentFrame = self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 512) + start_time = time.time() retval, self.currentFrame = self.capture.read() + self.capture.set(cv2.CAP_PROP_BUFFERSIZE, 1) + capture_time = time.time() if self.currentFrame is None: logger.debug( "no frame read from stream URL - ensure the URL does not end with newline and that the filename is correct" @@ -3521,6 +3751,11 @@ def timerSampleRefresh(self): qimage = qimage.rgbSwapped() pixmap_orig = QtGui.QPixmap.fromImage(qimage) self.pixmap_item.setPixmap(pixmap_orig) + #while(self.frame_queue.qsize() > 1): + # self.frame_queue.get() # remove old frames + #self.frame_queue.put(pixmap_orig) + end_time = time.time() + #logger.info(f"capture time: {capture_time - start_time}, total time: {end_time - start_time}") def sceneKey(self, event): if ( @@ -3547,13 +3782,28 @@ def sceneKey(self, event): self.centeringMarksList[i]["graphicsItem"] ) self.centeringMarksList[i] = None + ''' + When picking pixels + + ''' def pixelSelect(self, event): super(QtWidgets.QGraphicsPixmapItem, self.pixmap_item).mousePressEvent(event) x_click = float(event.pos().x()) y_click = float(event.pos().y()) penGreen = QtGui.QPen(QtCore.Qt.green) penRed = QtGui.QPen(QtCore.Qt.red) + ''' + For three click centering, this if statement checks the omega state of the motor. + This ideally gives feedback on wether the MD2 is in the rotation portion of the three click centering + + ''' + if daq_utils.beamline == 'nyx': + state = self.md2.exporter.read('OmegaState') + if state != 'Ready': + logger.info('waiting for motor rotation') + logger.info('Click not registered') + return if self.vidActionDefineCenterRadio.isChecked(): self.vidActionC2CRadio.setChecked( True @@ -3584,11 +3834,11 @@ def pixelSelect(self, event): self.drawInteractiveRasterCB() return fov = self.getCurrentFOV() - correctedC2C_x = daq_utils.screenPixCenterX + ( - x_click - (self.centerMarker.x() + self.centerMarkerCharOffsetX) + correctedC2C_x = self.getBeamCenterX() + ( + x_click - (self.centerMarker.x() - self.centerMarkerCharOffsetX) - 20 ) - correctedC2C_y = daq_utils.screenPixCenterY + ( - y_click - (self.centerMarker.y() + self.centerMarkerCharOffsetY) + correctedC2C_y = self.getBeamCenterY() + ( + y_click - (self.centerMarker.y() - self.centerMarkerCharOffsetY) - 40 ) current_viewangle = daq_utils.mag1ViewAngle @@ -3598,10 +3848,48 @@ def pixelSelect(self, event): current_viewangle = daq_utils.mag3ViewAngle elif self.zoom4Radio.isChecked(): current_viewangle = daq_utils.mag4ViewAngle - + ''' + Three click centering will update self.threeClickSignal.emit(self.threeClickCount) + + ''' if self.threeClickCount > 0: # 3-click centering self.threeClickCount = self.threeClickCount + 1 - comm_s = ( + self.threeClickSignal.emit('{} more clicks'.format(str(4-self.threeClickCount))) + #adding drawing for three click centering + logger.info('Drawing 3 click line {} at x_value: {} and y_value {}'.format(self.threeClickCount, x_click, y_click)) + self.threeClickLines.append( + self.scene.addLine(x_click, 0, x_click, 512, penGreen) + ) + + + if daq_utils.exporter_enabled: + correctedC2C_x = x_click + 5 + ((daq_utils.screenPixX/2) - (self.centerMarker.x() + self.centerMarkerCharOffsetX)) + correctedC2C_y = y_click - 35 + ((daq_utils.screenPixY/2) - (self.centerMarker.y() + self.centerMarkerCharOffsetY)) + lsdc_x = daq_utils.screenPixX + lsdc_y = daq_utils.screenPixY + md2_x = self.md2.center_pixel_x.get() * 2 + md2_y = self.md2.center_pixel_y.get() * 2 + scale_x = md2_x / lsdc_x + scale_y = md2_y / lsdc_y + correctedC2C_x = correctedC2C_x * scale_x + correctedC2C_y = correctedC2C_y * scale_y + self.md2.centring_click.put(f"{correctedC2C_x} {correctedC2C_y}") + #logger.info('waiting for motor rotation') + #time.sleep(0.2) + #self.omegaMoveCheck(0.02,'OmegaState') + + if self.threeClickCount == 4: + self.threeClickCount = 0 + self.threeClickSignal.emit('0') + self.click3Button.setStyleSheet("background-color: None") + #removing drawing for three click centering + logger.info('Removing 3 click lines') + for i in range(len(self.threeClickLines)): + self.scene.removeItem(self.threeClickLines[i]) + self.threeClickLines = [] + return + else: + comm_s = ( "center_on_click", [ correctedC2C_x, @@ -3626,9 +3914,35 @@ def pixelSelect(self, event): self.aux_send_to_server(*comm_s) if self.threeClickCount == 4: self.threeClickCount = 0 + self.threeClickSignal.emit('0') self.click3Button.setStyleSheet("background-color: None") + #removing drawing for three cick centering + logger.info('Removing 3 click lines') + for i in range(len(self.threeClickLines)): + self.scene.removeItem(self.threeClickLines[i]) + logger.info('Removed line {}'.format(i)) + self.threeClickLines = [] + return + ''' + Function to check if MD motors are rotating or not + ''' + def omegaMoveCheck(self, sleeptime,call='OmegaState'): + state = self.md2.exporter.read(call) + while(state == 'Moving'): + time.sleep(sleeptime) + state = self.md2.exporter.read(call) + #logger.info('\nIn Moving\n{}\n'.format(state)) + if state == 'Ready': + logger.info('ready for next click') + return state + else: + logger.info('\ndone moving, current state is: {}'.format(state)) + return state + + + def editScreenParamsCB(self): self.screenDefaultsDialog = ScreenDefaultsDialog(self) self.screenDefaultsDialog.show() @@ -4025,9 +4339,9 @@ def addSampleRequestCB(self, rasterDef=None, selectedSampleID=None): centeringOption == "Interactive" and self.mountedPin_pv.get() == self.selectedSampleID ) or centeringOption == "Testing": # user centered manually - reqObj["pos_x"] = float(self.sampx_pv.get()) - reqObj["pos_y"] = float(self.sampy_pv.get()) - reqObj["pos_z"] = float(self.sampz_pv.get()) + reqObj["pos_x"] = float(self.gon.x.val()) + reqObj["pos_y"] = float(self.gon.y.val()) + reqObj["pos_z"] = float(self.gon.z.val()) reqObj["runNum"] = runNum try: reqObj["sweep_start"] = float(self.osc_start_ledit.text()) @@ -4204,7 +4518,7 @@ def parkGripperCB(self): def restartServerCB(self): if self.controlEnabled(): msg = "Desperation move. Are you sure?" - self.timerSample.stop() + #self.timerSample.stop() reply = QtWidgets.QMessageBox.question( self, "Message", @@ -4212,7 +4526,7 @@ def restartServerCB(self): QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No, ) - self.timerSample.start(SAMPLE_TIMER_DELAY) + #self.timerSample.start(SAMPLE_TIMER_DELAY) if reply == QtWidgets.QMessageBox.Yes: if daq_utils.beamline == "fmx" or daq_utils.beamline == "amx": restart_pv = PV(daq_utils.beamlineComm + "RestartServerSignal") @@ -4231,13 +4545,19 @@ def popUserScreenCB(self): else: self.popupServerMessage("You don't have control") + def parkRobotCB(self): + if daq_utils.beamline == "nyx": + self.send_to_server("parkRobot()") + else: + self.send_to_server("parkGripper") + def closePhotonShutterCB(self): self.photonShutterClose_pv.put(1) def removePuckCB(self): - self.timerSample.stop() + #self.timerSample.stop() dewarPos, ok = DewarDialog.getDewarPos(parent=self, action="remove") - self.timerSample.start(SAMPLE_TIMER_DELAY) + #self.timerSample.start(SAMPLE_TIMER_DELAY) def setVectorPointCB(self, pointName): """Callback function to update a vector point @@ -4290,13 +4610,13 @@ def clearVectorCB(self): def puckToDewarCB(self): while 1: - self.timerSample.stop() + #self.timerSample.stop() puckName, ok = PuckDialog.getPuckName() - self.timerSample.start(SAMPLE_TIMER_DELAY) + #self.timerSample.start(SAMPLE_TIMER_DELAY) if ok: - self.timerSample.stop() + #self.timerSample.stop() dewarPos, ok = DewarDialog.getDewarPos(parent=self, action="add") - self.timerSample.start(SAMPLE_TIMER_DELAY) + #self.timerSample.start(SAMPLE_TIMER_DELAY) if ok and dewarPos is not None and puckName is not None: ipos = int(dewarPos) + 1 db_lib.insertIntoContainer( @@ -4322,9 +4642,13 @@ def stopQueueCB(self): def mountSampleCB(self): + + + if getBlConfig("mountEnabled") == 0: self.popupServerMessage("Mounting disabled!! Call staff!") return + logger.info("mount selected sample") self.eraseCB() if ( @@ -4422,7 +4746,8 @@ def refreshCollectionParams(self, selectedSampleRequest, validate_hdf5=True): firstFilename = daq_utils.create_filename(prefix_long, fnumstart) if validate_hdf5: if validation.validate_master_HDF5_file(firstFilename): - self.albulaInterface.open_file(firstFilename) + if daq_utils.beamline != "nyx": + self.albulaInterface.open_file(firstFilename) else: QtWidgets.QMessageBox.information( self, @@ -4440,12 +4765,15 @@ def refreshCollectionParams(self, selectedSampleRequest, validate_hdf5=True): else: self.rasterGrainCustomRadio.setChecked(True) rasterStep = int(reqObj["gridStep"]) + logger.info("reaching redraw now") if not self.hideRastersCheckBox.isChecked() and ( reqObj["protocol"] in ("raster", "stepRaster", "multiCol") ): - if not self.rasterIsDrawn(selectedSampleRequest): - self.drawPolyRaster(selectedSampleRequest) - self.fillPolyRaster(selectedSampleRequest) + #if not self.rasterIsDrawn(selectedSampleRequest): + # always erase and then draw + self.eraseRastersCB() + self.drawPolyRaster(selectedSampleRequest) + self.fillPolyRaster(selectedSampleRequest) if ( str(self.govStateMessagePV.get(as_string=True)) == "state SA" @@ -4453,13 +4781,14 @@ def refreshCollectionParams(self, selectedSampleRequest, validate_hdf5=True): and self.selectedSampleRequest["sample"] # with control enabled == self.mountedPin_pv.get() ): # And the sample of the selected request is mounted - self.processSampMove(self.sampx_pv.get(), "x") - self.processSampMove(self.sampy_pv.get(), "y") - self.processSampMove(self.sampz_pv.get(), "z") + logger.info("attempting to move to raster start") + self.processSampMove(self.gon.x.val(), "x") + self.processSampMove(self.gon.y.val(), "y") + self.processSampMove(self.gon.z.val(), "z") if ( abs( selectedSampleRequest["request_obj"]["rasterDef"]["omega"] - - self.omega_pv.get() + - self.gon.omega.val() ) > 5.0 ): @@ -4700,8 +5029,8 @@ def ringCurrentChangedCB(self, value=None, char_value=None, **kw): self.ringCurrentSignal.emit(ringCurrentVal) def beamAvailableChangedCB(self, value=None, char_value=None, **kw): - beamAvailableVal = value - self.beamAvailableSignal.emit(beamAvailableVal) + threeClickVal = value + self.threeClickSignal.emit(threeClickVal) def sampleExposedChangedCB(self, value=None, char_value=None, **kw): sampleExposedVal = value @@ -4747,6 +5076,20 @@ def pauseButtonStateCB(self, value=None, char_value=None, **kw): pauseButtonStateVar = value self.pauseButtonStateSignal.emit(pauseButtonStateVar) + def initOphyd(self): + if daq_utils.beamline == "nyx": + # initialize devices + self.gon = GonioDevice("XF:19IDC-ES{MD2}:", name="gonio") + self.camera = CameraDevice("XF:19IDC-ES{MD2}:", name="camera") + self.md2 = MD2Device("XF:19IDC-ES{MD2}:", name="md2") + self.front_light = LightDevice("XF:19IDC-ES{MD2}:Front", name="front_light") + self.back_light = LightDevice("XF:19IDC-ES{MD2}:Back", name="back_light") + self.aperture = MD2ApertureDevice("XF:19IDC-ES{MD2}:", name="aperture") + elif daq_utils.beamline == "amx": + self.gon = GonioDevice("XF:17IDB-ES:AMX{Gon:1", name="gonio") + elif daq_utils.beamline == "fmx": + self.gon = GonioDevice("XF:17IDC-ES:FMX{Gon:1", name="gonio") + def initUI(self): self.tabs = QtWidgets.QTabWidget() self.comm_pv = PV(daq_utils.beamlineComm + "command_s") @@ -4762,6 +5105,26 @@ def initUI(self): splitter1.addWidget(self.tabs) self.setCentralWidget(splitter1) splitterSizes = [600, 100] + + ''' + creating drop down menu items under File + + for now has + importAction - importing spreadsheet manually + @function - popImportDialogCB + + userAction - User Mode + @function - setUserModeCB + + expertAction - Expert Mode + @function - setExpertModeCB + + staffAction - Staff Panel + @function - popStaffDialogCB + + resolutionAction - Resolution Calculator + @function - openResolution + ''' importAction = QtWidgets.QAction("Import Spreadsheet...", self) importAction.triggered.connect(self.popImportDialogCB) modeGroup = QtWidgets.QActionGroup(self) @@ -4773,6 +5136,8 @@ def initUI(self): self.expertAction.triggered.connect(self.setExpertModeCB) self.staffAction = QtWidgets.QAction("Staff Panel...", self) self.staffAction.triggered.connect(self.popStaffDialogCB) + self.resolutionAction = QtWidgets.QAction("Resolution Calculator", self) + self.resolutionAction.triggered.connect(self.openResolution) modeGroup.addAction(self.userAction) modeGroup.addAction(self.expertAction) exitAction = QtWidgets.QAction(QtGui.QIcon("exit24.png"), "Exit", self) @@ -4782,6 +5147,7 @@ def initUI(self): self.statusBar() self.queue_collect_status_widget = QtWidgets.QLabel("Queue Collect: ON") self.statusBar().addPermanentWidget(self.queue_collect_status_widget) + menubar = self.menuBar() fileMenu = menubar.addMenu("&File") settingsMenu = menubar.addMenu("Settings") @@ -4789,6 +5155,7 @@ def initUI(self): fileMenu.addAction(self.userAction) fileMenu.addAction(self.expertAction) fileMenu.addAction(self.staffAction) + fileMenu.addAction(self.resolutionAction) # Define all of the available actions for the overlay color group self.BlueOverlayAction = QtWidgets.QAction("Blue", self, checkable=True) self.RedOverlayAction = QtWidgets.QAction("Red", self, checkable=True) @@ -4975,12 +5342,12 @@ def initCallbacks(self): self.energyChangeSignal.connect(self.processEnergyChange) self.energy_pv.add_callback(self.processEnergyChangeCB, motID="x") - self.sampx_pv = PV(daq_utils.motor_dict["sampleX"] + ".RBV") + self.sampx_pv = PV(self.gon.x.readback.pvname) self.sampMoveSignal.connect(self.processSampMove) self.sampx_pv.add_callback(self.processSampMoveCB, motID="x") - self.sampy_pv = PV(daq_utils.motor_dict["sampleY"] + ".RBV") + self.sampy_pv = PV(self.gon.y.readback.pvname) self.sampy_pv.add_callback(self.processSampMoveCB, motID="y") - self.sampz_pv = PV(daq_utils.motor_dict["sampleZ"] + ".RBV") + self.sampz_pv = PV(self.gon.z.readback.pvname) self.sampz_pv.add_callback(self.processSampMoveCB, motID="z") if self.scannerType == "PI": @@ -4991,14 +5358,14 @@ def initCallbacks(self): self.sampFineZ_pv = PV(daq_utils.motor_dict["fineZ"] + ".RBV") self.sampFineZ_pv.add_callback(self.processSampMoveCB, motID="fineZ") - self.omega_pv = PV(daq_utils.motor_dict["omega"] + ".VAL") - self.omegaTweak_pv = PV(daq_utils.motor_dict["omega"] + ".RLV") - self.sampyTweak_pv = PV(daq_utils.motor_dict["sampleY"] + ".RLV") - if daq_utils.beamline == "nyx": - self.sampzTweak_pv = PV(daq_utils.motor_dict["sampleX"] + ".RLV") - else: - self.sampzTweak_pv = PV(daq_utils.motor_dict["sampleZ"] + ".RLV") - self.omegaRBV_pv = PV(daq_utils.motor_dict["omega"] + ".RBV") + self.omega_pv = PV(self.gon.omega.setpoint.pvname) + self.omegaTweak_pv = PV(self.gon.omega.setpoint.pvname) + self.sampyTweak_pv = PV(self.gon.y.setpoint.pvname) + #if daq_utils.beamline == "nyx": + # self.sampzTweak_pv = PV(self.gon.x.setpoint.pvname + ".RLV") + #else: + self.sampzTweak_pv = PV(self.gon.z.setpoint.pvname) + self.omegaRBV_pv = PV(self.gon.omega.readback.pvname) self.omegaRBV_pv.add_callback( self.processSampMoveCB, motID="omega" ) # I think monitoring this allows for the textfield to monitor val and this to deal with the graphics. Else next line has two callbacks on same thing. @@ -5014,7 +5381,7 @@ def initCallbacks(self): self.cryostreamTemp_pv.add_callback(self.cryostreamTempChangedCB) self.ringCurrentSignal.connect(self.processRingCurrent) self.ringCurrent_pv.add_callback(self.ringCurrentChangedCB) - self.beamAvailableSignal.connect(self.processBeamAvailable) + self.threeClickSignal.connect(self.processThreeClickCentering) self.beamAvailable_pv.add_callback(self.beamAvailableChangedCB) self.sampleExposedSignal.connect(self.processSampleExposed) self.sampleExposed_pv.add_callback(self.sampleExposedChangedCB) diff --git a/gui/dialog/__init__.py b/gui/dialog/__init__.py index b50f1daa..08dc984c 100644 --- a/gui/dialog/__init__.py +++ b/gui/dialog/__init__.py @@ -1,8 +1,14 @@ -from .snap_comment import SnapCommentDialog +from .dewar import DewarDialog +from .puck_dialog import PuckDialog from .raster_explore import RasterExploreDialog +from .resolution_dialog import CalculatorWindow +from .screen_defaults import ScreenDefaultsDialog +from .set_energy import SetEnergyDialog +from .snap_comment import SnapCommentDialog from .staff_screen import StaffScreenDialog from .user_screen import UserScreenDialog from .puck_dialog import PuckDialog from .dewar import DewarDialog from .screen_defaults import ScreenDefaultsDialog from .set_energy import SetEnergyDialog +from .resolution_dialog import CalculatorWindow diff --git a/gui/dialog/dewar.py b/gui/dialog/dewar.py index b3ae55cd..fb510540 100644 --- a/gui/dialog/dewar.py +++ b/gui/dialog/dewar.py @@ -59,7 +59,8 @@ def initUI(self): for j in range(0, self.pucksPerDewarSector): dataIndex = (i * self.pucksPerDewarSector) + j self.allButtonList[dataIndex] = QtWidgets.QPushButton( - (str(self.data[dataIndex])) + #(str(self.data[dataIndex])) + '{}: {}'.format(str(dataIndex+1),str(self.data[dataIndex])) ) self.allButtonList[dataIndex].clicked.connect( functools.partial(self.on_button, str(dataIndex)) diff --git a/gui/dialog/resolution_dialog.py b/gui/dialog/resolution_dialog.py new file mode 100644 index 00000000..25306a52 --- /dev/null +++ b/gui/dialog/resolution_dialog.py @@ -0,0 +1,219 @@ +import typing + +from qtpy import QtWidgets +from qtpy.QtGui import QDoubleValidator +from qtpy.QtWidgets import QLabel, QLineEdit, QPushButton, QRadioButton, QVBoxLayout + +from utils.resolution_calculator import Calculator + +if typing.TYPE_CHECKING: + from lsdcGui import ControlMain + +WINDOW_SIZE = 480 + +# main qtpy window the calculator exists in + + +class CalculatorWindow(QtWidgets.QDialog): + def __init__(self, parent: "ControlMain"): + super(CalculatorWindow, self).__init__(parent) + self.setFixedSize(WINDOW_SIZE, WINDOW_SIZE) + # making radio buttons to choose formula + self.buttonDictionary = { + "L": {"picker": QRadioButton("Caluclate crystal to detector distance")}, + "d": {"picker": QRadioButton("Calculate resolution")}, + "theta": {"picker": QRadioButton("Calculate detector 2theta")}, + "wavelength": {"picker": QRadioButton("Calculate wavelength")}, + "r": {"value": None}, + } + + self.r_value_enter = QLineEdit() + self.r_value_enter.setPlaceholderText("Set r value") + self.buttonDictionary["r"]["value"] = self.r_value_enter + self.r_value_enter.setValidator(QDoubleValidator()) + # setting inputs to Double only + + self.L_value_enter = QLineEdit() + self.L_value_enter.setPlaceholderText("Set L value") + self.buttonDictionary["L"]["value"] = self.L_value_enter + self.L_value_enter.setValidator(QDoubleValidator()) + + self.d_value_enter = QLineEdit() + self.d_value_enter.setPlaceholderText("Set d value") + self.buttonDictionary["d"]["value"] = self.d_value_enter + self.d_value_enter.setValidator(QDoubleValidator()) + + self.theta_value_enter = QLineEdit() + self.theta_value_enter.setPlaceholderText("Set theta value") + self.buttonDictionary["theta"]["value"] = self.theta_value_enter + self.theta_value_enter.setValidator(QDoubleValidator()) + + self.wave_value_enter = QLineEdit() + self.wave_value_enter.setPlaceholderText("Set wavelength value") + self.buttonDictionary["wavelength"]["value"] = self.wave_value_enter + self.wave_value_enter.setValidator(QDoubleValidator()) + + self.final_button = QPushButton("Calculate", self) + self.final_button.clicked.connect(self.calculateValue) + + self.bottom_text = QLabel() + self.bottom_text.setText("Enter values and Press button to calculate") + + # creating calculator object + self.calculator = Calculator() + + layout = QVBoxLayout() + layout.addWidget(self.r_value_enter) + for key in self.buttonDictionary: + if "picker" in self.buttonDictionary[key].keys(): + layout.addWidget(self.buttonDictionary[key]["picker"]) + layout.addWidget(self.buttonDictionary[key]["value"]) + layout.addWidget(self.final_button) + layout.addWidget(self.bottom_text) + self.setLayout(layout) + + """ + calls resolution calculator to calculate value depending on inputs from widgets + + -outputs + -value_to_return = value from formula calculated if no problems + -returns nothing if a problem occured, changes bottom_text + """ + + def calculateValue(self): + checked_key = None + # checking which formula to use + for key in self.buttonDictionary: + if key != "r" and self.buttonDictionary[key]["picker"].isChecked(): + checked_key = key + if not checked_key: + self.bottom_text.setText( + "No calculation specified (press one of the radio buttons)" + ) + return + + r_value = self.r_value_enter.displayText() + # checking if value is a number string or empty string + if r_value == "" or r_value[0].isalpha(): + self.bottom_text.setText( + "formula to calculate {} requires r value".format(checked_key) + ) + return + elif float(r_value) < 140 or float(r_value) > 350: + self.bottom_text.setText("r value must be between 140 and 350") + return + + r_value = float(r_value) + + d_value = self.d_value_enter.displayText() + # checking if value is string or none if not calculating that value (trying to use .isalpha but not when value is None) + if (d_value == "" or d_value[0].isalpha()) and checked_key != "d": + self.bottom_text.setText( + "formula to calculate {} requires d value".format(checked_key) + ) + return + + l_value = self.L_value_enter.displayText() + if (l_value == "" or l_value[0].isalpha()) and checked_key != "L": + self.bottom_text.setText( + "formula to calculate {} requires L value".format(checked_key) + ) + return + + theta_value = self.theta_value_enter.displayText() + if (theta_value == "" or theta_value[0].isalpha()) and checked_key != "theta": + self.bottom_text.setText( + "formula to calculate {} requires theta value".format(checked_key) + ) + return + + wave_value = self.wave_value_enter.displayText() + if ( + wave_value == "" or wave_value[0].isalpha() + ) and checked_key != "wavelength": + self.bottom_text.setText( + "formula to calculate {} requires the wavelenght".format(checked_key) + ) + return + + # setting value to return if want value returned + value_to_return = None + + if checked_key == "d": + l_value = float(self.L_value_enter.displayText()) + theta_value = float(self.theta_value_enter.displayText()) + wave_value = float(self.wave_value_enter.displayText()) + + variableDict = { + "L": l_value, + "theta": theta_value, + "wavelength": wave_value, + "r": r_value, + } + + self.calculator.set_all_variables(variableDict) + d_value = self.calculator.calcD() + value_to_return = d_value + self.d_value_enter.setText(str(d_value)) + self.calculator.set_variables("d", d_value) + + elif checked_key == "L": + + d_value = float(self.d_value_enter.displayText()) + theta_value = float(self.theta_value_enter.displayText()) + wave_value = float(self.wave_value_enter.displayText()) + + variableDict = { + "d": d_value, + "theta": theta_value, + "wavelength": wave_value, + "r": r_value, + } + + self.calculator.set_all_variables(variableDict) + L_value = self.calculator.calcL() + value_to_return = L_value + self.L_value_enter.setText(str(L_value)) + self.calculator.set_variables("L", L_value) + + elif checked_key == "theta": + + l_value = float(self.L_value_enter.displayText()) + d_value = float(self.d_value_enter.displayText()) + wave_value = float(self.wave_value_enter.displayText()) + + variableDict = { + "L": l_value, + "d": d_value, + "wavelength": wave_value, + "r": r_value, + } + + self.calculator.set_all_variables(variableDict) + theta_value = self.calculator.calcTheta() + value_to_return = theta_value + self.theta_value_enter.setText(str(theta_value)) + self.calculator.set_variables("theta", theta_value) + + elif checked_key == "wavelength": + + l_value = float(self.L_value_enter.displayText()) + theta_value = float(self.theta_value_enter.displayText()) + d_value = float(self.d_value_enter.displayText()) + variableDict = { + "L": l_value, + "d": d_value, + "theta": theta_value, + "r": r_value, + } + + self.calculator.set_all_variables(variableDict) + wave_value = self.calculator.calcWavelength() + self.calculator.set_variables("wavelength", wave_value) + value_to_return = wave_value + self.wave_value_enter.setText(str(wave_value)) + + self.bottom_text.setText( + "- Done Calculating - \n {} value = {}".format(checked_key, value_to_return) + ) + return value_to_return diff --git a/gui/dialog/staff_screen.py b/gui/dialog/staff_screen.py index b0f673ab..1375f7be 100644 --- a/gui/dialog/staff_screen.py +++ b/gui/dialog/staff_screen.py @@ -1,5 +1,6 @@ import logging import typing +import daq_utils from qtpy import QtCore, QtWidgets from qtpy.QtWidgets import QCheckBox @@ -233,7 +234,7 @@ def show(self): def getSpotNodeList(self): nodeList = [] for i in range(0, self.spotNodeCount): - nodeList.append(int(getBlConfig("spotNode" + str(i + 1)).split("cpu")[1])) + nodeList.append(int(getBlConfig("spotNode"+str(i+1)).split('cpu')[1])) return nodeList def getFastDPNodeList(self): diff --git a/gui/vector.py b/gui/vector.py index 391ddfc8..ef14a501 100644 --- a/gui/vector.py +++ b/gui/vector.py @@ -342,7 +342,7 @@ def clear_vector(self, scene: QtWidgets.QGraphicsScene) -> None: def transform_vector_coords( self, prev_coords: "dict[str, float]", current_raw_coords: "dict[str, float]" - ) -> dict[str, float]: + ) -> "dict[str, float]": """Updates y and z co-ordinates of vector points when they are moved This function tweaks the y and z co-ordinates such that when a vector start or diff --git a/mxbluesky/devices/__init__.py b/mxbluesky/devices/__init__.py index 5a8e42c7..70cf77c8 100644 --- a/mxbluesky/devices/__init__.py +++ b/mxbluesky/devices/__init__.py @@ -1,4 +1,33 @@ -from .auto_center import * -from .generic import * -from .top_align import * -from .zebra import * +from functools import wraps +from ophyd import Device +try: + from .auto_center import * + from .top_align import * + from .zebra import * +except Exception as e: + pass + + +def standardize_readback(cls): + original_init = cls.__init__ + + @wraps(original_init) + def new_init(self, *args, **kwargs): + methods_to_extend = {"readback": "user_readback", + "setpoint": "user_setpoint", + "val": "user_readback.get"} + original_init(self, *args, **kwargs) + for attr_name in self.component_names: + comp = getattr(self, attr_name) + if not isinstance(comp, Device): + continue + + for new_method_name, old_method_name in methods_to_extend.items(): + if not hasattr(comp, new_method_name): + if hasattr(comp, old_method_name): + setattr(comp, new_method_name, getattr(comp, old_method_name)) + + cls.__init__ = new_init + return cls + +from .generic import * \ No newline at end of file diff --git a/mxbluesky/devices/generic.py b/mxbluesky/devices/generic.py index 2e458240..e9c9cbdd 100644 --- a/mxbluesky/devices/generic.py +++ b/mxbluesky/devices/generic.py @@ -1,6 +1,6 @@ from ophyd import Component as Cpt from ophyd import Device, EpicsMotor, EpicsSignal - +from mxbluesky.devices import standardize_readback class WorkPositions(Device): gx = Cpt(EpicsSignal, "{Gov:Robot-Dev:gx}Pos:Work-Pos") @@ -15,7 +15,7 @@ class MountPositions(Device): pz = Cpt(EpicsSignal, "{Gov:Robot-Dev:gpz}Pos:Mount-Pos") o = Cpt(EpicsSignal, "{Gov:Robot-Dev:go}Pos:Mount-Pos") - +@standardize_readback class GoniometerStack(Device): gx = Cpt(EpicsMotor, "-Ax:GX}Mtr") gy = Cpt(EpicsMotor, "-Ax:GY}Mtr") @@ -23,3 +23,14 @@ class GoniometerStack(Device): o = Cpt(EpicsMotor, "-Ax:O}Mtr") py = Cpt(EpicsMotor, "-Ax:PY}Mtr") pz = Cpt(EpicsMotor, "-Ax:PZ}Mtr") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Renaming to match MD2 GonioDevice + self.x = self.gx + self.cx = self.gx + self.y = self.py + self.cy = self.py + self.z = self.pz + self.cz = self.pz + self.omega = self.o \ No newline at end of file diff --git a/mxbluesky/devices/md2.py b/mxbluesky/devices/md2.py new file mode 100644 index 00000000..7fa98c59 --- /dev/null +++ b/mxbluesky/devices/md2.py @@ -0,0 +1,392 @@ +import os +import socket + +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, PVPositioner +from ophyd.status import SubscriptionStatus + + +class MD2Positioner(PVPositioner): + setpoint = Cpt(EpicsSignal, "Position", name="setpoint") + readback = Cpt(EpicsSignal, "Position", name="readback") + state = Cpt(EpicsSignalRO, "State", name="state") + done = Cpt(EpicsSignalRO, "State", name="done") + precision = Cpt(EpicsSignalRO, "Precision", name="precision") + done_value = 4 # MD2 Enum, 4 = Ready + # TODO: Add limits, settle_time, timeout or defaults for each + + def val(self): + return self.get().readback + + +class ExporterComponent(Cpt): + def __init__(self, address, port, name, **kwargs): + super().__init__(self, **kwargs) + self.name = name + self.address = address + self.port = port + self.sock = None # socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # self.sock.settimeout(5) + + def get(self): + return () + + def connect(self): + # for establishing connection, using context manager is preferred + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((self.address, self.port)) + + def disconnect(self): + self.sock.close() + self.sock = None + + def send_data(self, data): + STX = chr(2) + ETX = chr(3) + data = STX + data + ETX + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((self.address, self.port)) + s.sendto(data.encode(), (self.address, self.port)) + state = s.recv(4096) + print(f"state: {self.decipher_reply(state)}") + ret = None + while ret is None: + output = s.recv(4096) + output = self.decipher_reply(output) + ret = self.process_ret(output) + return ret + + def read_stream(self): + output = None + while output is None: + output, addr = self.sock.recvfrom(4096) + print(f"output:{self.decipher_reply(output)}") + output = None + + def write(self, attribute, value): + return self.send_data("WRTE " + attribute + " " + str(value)) + + def read(self, attribute): + return (self.send_data("READ " + attribute + " ")).split(" ")[0][4:] + + def cmd(self, method, parameters): + parameters = map(str, parameters) + params = "\t".join(parameters) + return self.send_data("EXEC " + method + " " + str(params)) + + def decipher_reply(self, reply): + # Specifically for decoding MD2 Exporter replies + reply = str(reply) + reply = reply.replace("\\x03", "") + reply = reply.replace("\\x1f", ", ") + reply = reply.replace("\\x02", "\n") + reply = reply.replace("\\t", " ") + return reply[2:-1] + + def process_ret(self, ret): + # Returns only when an error or return value is found, + # ignores events to ensure flyer kickoff functions as expected + for line in ret.split("\n"): + if "ERR:" in line: + print(f"error: {line}") + return line + elif "RET:" in line: + print(f"returned: {line}") + return line + elif "NULL" in line: + print(f"null error: {line}") + return line + elif "EVT:" in line: # print here if you want to see the events + pass # return self.process_evt(line) + + +class LightDevice(Device): + control = Cpt(EpicsSignal, "LightIsOn", name="control") + factor = Cpt(EpicsSignal, "LightFactor", name="factor") + level = Cpt(EpicsSignal, "LightLevel", name="level") + + def is_on(self): + return self.control.get() == 1 + + def turn_on(self): + self.control.set(1) + + def turn_off(self): + self.control.set(0) + + def set_factor(self, factor): + self.factor.set(factor) + + def set_level(self, level): + self.level.set(level) + + +class BeamstopDevice(Device): + distance = Cpt(MD2Positioner, "BeamstopDistance", name="distance") + distance_preset = Cpt(EpicsSignal, "BeamstopDistance", name="distance_preset") + x = Cpt(MD2Positioner, "BeamstopX", name="x") + y = Cpt(MD2Positioner, "BeamstopY", name="y") + z = Cpt(MD2Positioner, "BeamstopZ", name="z") + pos = Cpt(EpicsSignal, "BeamstopPosition", name="pos") + + +class MD2SimpleHVDevice(Device): + horizontal = Cpt(MD2Positioner, "HVHorizontal", name="horizontal") + vertical = Cpt(MD2Positioner, "HVVertical", name="vertical") + pos = Cpt(EpicsSignal, "HVPosition", name="pos") + # Current aperture/scintillator/capillary predifined position. + # Enum: the aperture position: + # 0: PARK, under cover. + # 1: BEAM, selected aperture aligned with beam. + # 2: OFF, just below the OAV. + # 3: UNKNOWN, not in a predefined position (this cannot be set). + + +class GonioDevice(Device): + omega = Cpt(MD2Positioner, "Omega", name="omega") + x = Cpt(MD2Positioner, "AlignmentX", name="x") + y = Cpt(MD2Positioner, "AlignmentY", name="y") + z = Cpt(MD2Positioner, "AlignmentZ", name="z") + cx = Cpt(MD2Positioner, "CentringX", name="cx") + cy = Cpt(MD2Positioner, "CentringY", name="cy") + + +class MD2Device(GonioDevice): + # TODO: Enum for MD2 phases and states + cx = Cpt(MD2Positioner, "CentringX", name="cx") + cy = Cpt(MD2Positioner, "CentringY", name="cy") + center_pixel_x = Cpt(EpicsSignalRO, "BeamPositionHorizontal", name="center_pixel_x") + center_pixel_y = Cpt(EpicsSignalRO, "BeamPositionVertical", name="center_pixel_y") + centring_click = Cpt(EpicsSignal, "setCentringClick", name="centring_click") + centring_save = Cpt(EpicsSignal, "saveCentringPositions", name="centring_save") + state = Cpt(EpicsSignalRO, "State", name="state") + phase = Cpt(EpicsSignal, "CurrentPhase", name="phase") + phase_index = Cpt(EpicsSignalRO, "CurrentPhaseIndex", name="phase_index") + detector_state = Cpt(EpicsSignal, "DetectorState", name="det_state") + detector_gate_pulse_enabled = Cpt( + EpicsSignal, "DetectorGatePulseEnabled", name="det_gate_pulse_enabled" + ) + exporter = Cpt( + ExporterComponent, + address=os.environ["EXPORTER_HOST"], + port=int(os.environ["EXPORTER_PORT"]), + name="exporter", + ) + + task_info = Cpt(EpicsSignalRO, "LastTaskInfo", name="task_info") + task_output = Cpt(EpicsSignalRO, "LastTaskOutput", name="task_output") + + def save_center(self): + self.centring_save.put("__EMPTY__") + + def task_complete(self): + return self.task_output == "true" + + def task_status(self): + def check_task(*, old_value, value, **kwargs): + "Return True when last task completes" + return value[6] == "1" + + return SubscriptionStatus(self.task_info, check_task) + + def is_ready(self): + return self.state.get() == 4 + + def ready_status(self): + # returns an ophyd status object that monitors the state pv for operations to complete + def check_ready(*, old_value, value, **kwargs): + "Return True when the MD2 is ready" + return value == 4 + + return SubscriptionStatus(self.state, check_ready) + + def phase_transition(self, phase): + # returns an ophyd status object that monitors the phase pv for operations to complete + self.phase.put(phase) + + def check_transition_state(*, old_value, value, **kwargs): + "Return True when the MD2 is ready" + return value == 4 and self.phase.get() == phase + + return SubscriptionStatus(self.state, check_transition_state) + + def standard_scan( + self, + frame_number=0, # int: frame ID just for logging purposes. + num_images=1, # int: number of frames. Needed solely when the detector use gate enabled trigger. + start_angle=0, # double: angle (deg) at which the shutter opens and omega speed is stable. + scan_range=0.1, # double: omega relative move angle (deg) before closing the shutter. + exposure_time=0.1, # double: exposure time (sec) to control shutter command. + num_passes=1, # int: number of moves forward and reverse between start angle and stop angle + ): + command = "startScanEx2" + if start_angle is None: + start_angle = self.omega.get() + return self.exporter.cmd( + command, + [ + frame_number, + num_images, + start_angle, + scan_range, + exposure_time, + num_passes, + ], + ) + + def vector_scan( + self, + start_angle=None, # double: angle (deg) at which the shutter opens and omega speed is stable. + scan_range=10, # double: omega relative move angle (deg) before closing the shutter. + exposure_time=1, # double: exposure time (sec) to control shutter command. + start_y=None, # double: PhiY axis position at the beginning of the exposure. + start_z=None, # double: PhiZ axis position at the beginning of the exposure. + start_cx=None, # double: CentringX axis position at the beginning of the exposure. + start_cy=None, # double: CentringY axis position at the beginning of the exposure. + stop_y=None, # double: PhiY axis position at the end of the exposure. + stop_z=None, # double: PhiZ axis position at the end of the exposure. + stop_cx=None, # double: CentringX axis position at the end of the exposure. + stop_cy=None, # double: CentringY axis position at the end of the exposure. + ): + command = "startScan4DEx" + if start_angle is None: + start_angle = self.omega.val() + if start_y is None: + start_y = self.y.val() + if start_z is None: + start_z = self.z.val() + if start_cx is None: + start_cx = self.cx.val() + if start_cy is None: + start_cy = self.cy.val() + if stop_y is None: + stop_y = self.y.val() + if stop_z is None: + stop_z = self.z.val() + if stop_cx is None: + stop_cx = self.cx.val() + if stop_cy is None: + stop_cy = self.cy.val() + + # List of scan parameters values, comma separated. The axes start values define the beginning + # of the exposure, that is when all the axes have a steady speed and when the shutter/detector + # are triggered. + # The axes stop values are for the end of detector exposure and define the position at the + # beginning of the deceleration. + # Inputs names: "start_angle", "scan_range", "exposure_time", "start_y", "start_z", "start_cx", + # "start_cy", "stop_y", "stop_z", "stop_cx", "stop_cy" + param_list = [ + start_angle, + scan_range, + exposure_time, + start_y, + start_z, + start_cx, + start_cy, + stop_y, + stop_z, + stop_cx, + stop_cy, + ] + return self.exporter.cmd(command, param_list) + + def raster_scan( + self, + omega_range=0, # double: omega relative move angle (deg) before closing the shutter. + line_range=0.1, # double: horizontal range of the grid (mm). + total_uturn_range=0.1, # double: vertical range of the grid (mm). + start_omega=None, # double: angle (deg) at which the shutter opens and omega speed is stable. + start_y=None, # double: PhiY axis position at the beginning of the exposure. + start_z=None, # double: PhiZ axis position at the beginning of the exposure. + start_cx=None, # double: CentringX axis position at the beginning of the exposure. + start_cy=None, # double: CentringY axis position at the beginning of the exposure. + number_of_lines=5, # int: number of frames on the vertical range. + frames_per_line=5, # int: number of frames on the horizontal range. + exposure_time=1.2, # double: exposure time (sec) to control shutter command. +1, based on the exaples given + invert_direction=True, # boolean: true to enable passes in the reverse direction. + use_centring_table=True, # boolean: true to use the centring table to do the pitch movements. + use_fast_mesh_scans=True, # boolean: true to use the fast raster scan if available (power PMAC). + ): + + command = "startRasterScanEx" + if start_omega is None: + start_omega = self.omega.val() + if start_y is None: + start_y = self.y.val() + if start_z is None: + start_z = self.z.val() + if start_cx is None: + start_cx = self.cx.val() + if start_cy is None: + start_cy = self.cy.val() + # List of scan parameters values, "/t" separated. The axes start values define the beginning + # of the exposure, that is when all the axes have a steady speed and when the shutter/detector + # are triggered. + # The axes stop values are for the end of detector exposure and define the position at the + # beginning of the deceleration. + # Inputs names: "omega_range", "line_range", "total_uturn_range", "start_omega", "start_y", + # "start_z", "start_cx", "start_cy", "number_of_lines", "frames_per_lines", "exposure_time", + # "invert_direction", "use_centring_table", "use_fast_mesh_scans" + param_list = [ + omega_range, + line_range, + total_uturn_range, + start_omega, + start_y, + start_z, + start_cx, + start_cy, + number_of_lines, + frames_per_line, + exposure_time, + invert_direction, + use_centring_table, + use_fast_mesh_scans, + ] + return self.exporter.cmd(command, param_list) + + +class MD2ApertureDevice(Device): + # this device needs additional signals for "CurrentApertureDiameterIndex" and "ApertureDiameters" + current_index = Cpt( + EpicsSignal, "CurrentApertureDiameterIndex", name="current_index" + ) + diameters = Cpt(EpicsSignalRO, "ApertureDiameters", name="diameters") + + def get_diameter_list(self): + # must format list for GUI + # also, can't currently get from PV + return self.diameters.get() + + def set_diameter(self, diameter): + if diameter in self.get_diameter_list(): + self.current_index.put(self.get_diameter_list().index(diameter)) + + +class ShutterDevice(Device): + control = Cpt( + EpicsSignal, "{MD2}:FastShutterIsOpen", name="control" + ) # PV to send control signal + pos_opn = Cpt(EpicsSignalRO, "{Gon:1-Sht}Pos:Opn-I", name="pos_opn") + pos_cls = Cpt(EpicsSignalRO, "{Gon:1-Sht}Pos:Cls-I", name="pos_cls") + + def is_open(self): + return self.control.get() == 1 # self.pos_opn.get() + + def open_shutter(self): + self.control.set( + 1 + ) # self.pos_opn.get()) iocs are down, so just setting it to 1 + + def close_shutter(self): + self.control.set(0) # self.pos_cls.get()) + + +class CameraDevice(Device): + # MD2 camera has the following attributes: + # CoaxCamScaleX + # CoaxCamScaleY + # CoaxialCameraZoomValue + scale_x = Cpt(EpicsSignalRO, "CoaxCamScaleX", name="scale_x") + scale_y = Cpt(EpicsSignalRO, "CoaxCamScaleY", name="scale_y") + zoom = Cpt(EpicsSignal, "CoaxialCameraZoomValue", name="zoom") diff --git a/mxbluesky/devices/md2_flyers.py b/mxbluesky/devices/md2_flyers.py new file mode 100644 index 00000000..01021d65 --- /dev/null +++ b/mxbluesky/devices/md2_flyers.py @@ -0,0 +1,284 @@ +import logging +import os +from collections import deque +import getpass +import grp +from ophyd.sim import NullStatus +from ophyd.status import SubscriptionStatus +from ophyd.utils import WaitTimeoutError + +logger = logging.getLogger(__name__) + +DEFAULT_DATUM_DICT = {"data": None, "omega": None} + +INTERNAL_SERIES = 0 +INTERNAL_ENABLE = 1 +EXTERNAL_SERIES = 2 +EXTERNAL_ENABLE = 3 + +class MD2StandardFlyer(): + def __init__(self, md2, detector=None) -> None: + self.name = "MD2StandardFlyer" + self.detector = detector + self.md2 = md2 + self.collection_params = {} + + self._asset_docs_cache = deque() + self._resource_uids = [] + self._datum_counter = None + self._datum_ids = DEFAULT_DATUM_DICT + self._master_file = None + self._master_metadata = [] + + self._collection_dictionary = None + + def kickoff(self): + md2_msg = self.md2.standard_scan(num_images=self.collection_params["total_num_images"], + start_angle=self.collection_params["start_angle"], + scan_range=self.collection_params["scan_range"], + exposure_time=self.collection_params["exposure_time"]) + logger.info(f"md2 KICKOFF msg: {md2_msg}") + return NullStatus() + + def update_parameters(self, total_num_images, start_angle, scan_range, exposure_time): + self.collection_params = { + "total_num_images": total_num_images, + "start_angle": start_angle, + "scan_range": scan_range, + "exposure_time": exposure_time, + } + + def configure_detector(self, file_prefix, data_directory_name): + self.detector.file.external_name.put(file_prefix) + self.detector.file.write_path_template = data_directory_name + + def detector_arm(self, angle_start, img_width, total_num_images, exposure_per_image, + file_prefix, data_directory_name, file_number_start, x_beam, y_beam, + wavelength, det_distance_m): + self.detector.cam.save_files.put(1) + self.detector.cam.sequence_id.put(file_number_start) + self.detector.cam.det_distance.put(det_distance_m) + self.detector.cam.file_owner.put(getpass.getuser()) + self.detector.cam.file_owner_grp.put(grp.getgrgid(os.getgid())[0]) + self.detector.cam.file_perms.put(420) + file_prefix_minus_directory = str(file_prefix) + file_prefix_minus_directory = file_prefix_minus_directory.split("/")[-1] + self.detector.cam.acquire_time.put(exposure_per_image) + self.detector.cam.acquire_period.put(exposure_per_image) + self.detector.cam.num_triggers.put(1) + self.detector.cam.num_images.put(total_num_images) + self.detector.cam.trigger_mode.put( + EXTERNAL_SERIES + ) # must be external_enable to get the correct number of triggers and stop acquire + self.detector.cam.file_path.put(data_directory_name) + self.detector.cam.fw_name_pattern.put(f"{file_prefix_minus_directory}_$id") + self.detector.cam.beam_center_x.put(x_beam) + self.detector.cam.beam_center_y.put(y_beam) + self.detector.cam.omega_incr.put(img_width) + self.detector.cam.omega_start.put(angle_start) + self.detector.cam.wavelength.put(wavelength) + self.detector.file.file_write_images_per_file.put(500) + + #def armed_callback(value, old_value, **kwargs): + # if old_value == 0 and value == 1: + # return True + # return False + + #status = SubscriptionStatus(self.detector.cam.armed, armed_callback, run=False) + #self.detector.cam.acquire.put(1) + #yield status + + def complete(self): + # monitor md2 status, wait for ready or timeout and then return + #ready_status = self.md2.ready_status() + + #logger.info(f"TASK INFO[6]: {self.md2.task_info[6]}") + #logger.info(f"TASK OUTPUT: {self.md2.task_output}") + logger.debug(f"FLYER COMPLETE FUNCTION") + task_status = self.md2.task_status() + logger.debug(f"assigning task status") + timeout = (self.collection_params["exposure_time"] * 3) + 80 + logger.info(f"TASK TIMEOUT: {timeout}") + #ready_status.wait(timeout=timeout) + try: + task_status.wait(timeout=timeout) + except WaitTimeoutError: + logger.info("reached task timeout") + logger.info(f"TASK INFO: {self.md2.task_info}") + logger.info(f"TASK OUTPUT: {self.md2.task_output}") + return + return task_status + + def describe_collect(self): + return {"stream_name": {}} + #return {self.name: self._collection_dictionary} + + def collect(self): + logger.debug("raster_flyer.collect(): going to unstage now") + yield {"data": {}, "timestamps": {}, "time": 0, "seq_num": 0} + #return self._collection_dictionary + + def unstage(self): + logger.debug("flyer unstaging") + self.collection_params = {} + + def read_configuration(self): + return {} + + def describe_configuration(self): + return {} + + # def collect_asset_docs(self): + # for _ in (): + # yield _ + + def collect_asset_docs(self): + asset_docs_cache = [] + + # Get the Resource which was produced when the detector was staged. + ((name, resource),) = self.detector.file.collect_asset_docs() + + asset_docs_cache.append(("resource", resource)) + self._datum_ids = DEFAULT_DATUM_DICT + # Generate Datum documents from scratch here, because the detector was + # triggered externally by the DeltaTau, never by ophyd. + resource_uid = resource["uid"] + # num_points = int(math.ceil(self.detector.cam.num_images.get() / + # self.detector.cam.fw_num_images_per_file.get())) + + # We are currently generating only one datum document for all frames, that's why + # we use the 0th index below. + # + # Uncomment & update the line below if more datum documents are needed: + # for i in range(num_points): + + seq_id = self.detector.cam.sequence_id.get() + + self._master_file = f"{resource['root']}/{resource['resource_path']}_{seq_id}_master.h5" + if not os.path.isfile(self._master_file): + raise RuntimeError(f"File {self._master_file} does not exist") + + # The pseudocode below is from Tom Caswell explaining the relationship between resource, datum, and events. + # + # resource = { + # "resource_id": "RES", + # "resource_kwargs": {}, # this goes to __init__ + # "spec": "AD-EIGER-MX", + # ...: ..., + # } + # datum = { + # "datum_id": "a", + # "datum_kwargs": {"data_key": "data"}, # this goes to __call__ + # "resource": "RES", + # ...: ..., + # } + # datum = { + # "datum_id": "b", + # "datum_kwargs": {"data_key": "omega"}, + # "resource": "RES", + # ...: ..., + # } + + # event = {...: ..., "data": {"detector_img": "a", "omega": "b"}} + + for data_key in self._datum_ids.keys(): + datum_id = f"{resource_uid}/{data_key}" + self._datum_ids[data_key] = datum_id + datum = { + "resource": resource_uid, + "datum_id": datum_id, + "datum_kwargs": {"data_key": data_key}, + } + asset_docs_cache.append(("datum", datum)) + return tuple(asset_docs_cache) + + def _extract_metadata(self, field="omega"): + with h5py.File(self._master_file, "r") as hf: + return hf.get(f"entry/sample/goniometer/{field}")[()] + +class MD2VectorFlyer(MD2StandardFlyer): + def __init__(self, md2, detector=None) -> None: + super().__init__(md2, detector) + self.name = "MD2VectorFlyer" + + def kickoff(self): + # params used are start_angle, scan_range, exposure_time, start_y, start_z, stop_y, stop_z + md2_msg = self.md2.vector_scan(start_angle=self.collection_params["start_angle"], + scan_range=self.collection_params["scan_range"], + exposure_time=self.collection_params["exposure_time"], + start_cx=self.collection_params["start_cx"], + start_cy=self.collection_params["start_cy"], + start_y=self.collection_params["start_y"], + start_z=self.collection_params["start_z"], + stop_cx=self.collection_params["stop_cx"], + stop_cy=self.collection_params["stop_cy"], + stop_y=self.collection_params["stop_y"], + stop_z=self.collection_params["stop_z"],) + logger.info(f"md2 VEC KICKOFF msg: {md2_msg}") + return NullStatus() + + def update_parameters(self, start_angle, scan_range, exposure_time, start_y, start_z, stop_y, stop_z, start_cx, start_cy, stop_cx, stop_cy): + self.collection_params = { + "start_angle": start_angle, + "scan_range": scan_range, + "exposure_time": exposure_time, + "start_cx": start_cx, + "start_cy": start_cy, + "start_y": start_y, + "start_z": start_z, + "stop_cx": stop_cx, + "stop_cy": stop_cy, + "stop_y": stop_y, + "stop_z": stop_z, + } + +class MD2RasterFlyer(MD2StandardFlyer): + # List of scan parameters values, "/t" separated. The axes start values define the beginning + # of the exposure, that is when all the axes have a steady speed and when the shutter/detector + # are triggered. + # The axes stop values are for the end of detector exposure and define the position at the + # beginning of the deceleration. + # Inputs names: "omega_range", "line_range", "total_uturn_range", "start_omega", "start_y", + # "start_z", "start_cx", "start_cy", "number_of_lines", "frames_per_lines", "exposure_time", + # "invert_direction", "use_centring_table", "use_fast_mesh_scans" + + def __init__(self, md2, detector=None) -> None: + super().__init__(md2, detector) + self.name = "MD2RasterFlyer" + + def kickoff(self): + # params used are start_angle, scan_range, exposure_time, start_y, start_z, stop_y, stop_z + md2_msg = self.md2.raster_scan(omega_range=self.collection_params["omega_range"], + line_range=self.collection_params["line_range"], + total_uturn_range=self.collection_params["total_uturn_range"], + start_omega=self.collection_params["start_omega"], + start_y=self.collection_params["start_y"], + start_z=self.collection_params["start_z"], + start_cx=self.collection_params["start_cx"], + start_cy=self.collection_params["start_cy"], + number_of_lines=self.collection_params["number_of_lines"], + frames_per_line=self.collection_params["frames_per_line"], + exposure_time=self.collection_params["exposure_time"], + invert_direction=self.collection_params["invert_direction"], + use_centring_table=self.collection_params["use_centring_table"], + use_fast_mesh_scans=self.collection_params["use_fast_mesh_scans"]) + logger.info(f"md2 RASTER KICKOFF msg: {md2_msg}") + return NullStatus() + + def update_parameters(self, omega_range, line_range, total_uturn_range, start_omega, start_y, start_z, start_cx, start_cy, number_of_lines, frames_per_line, exposure_time, invert_direction, use_centring_table, use_fast_mesh_scans): + self.collection_params = { + "omega_range": omega_range, + "line_range": line_range, + "total_uturn_range": total_uturn_range, + "start_omega": start_omega, + "start_y": start_y, + "start_z": start_z, + "start_cx": start_cx, + "start_cy": start_cy, + "number_of_lines": number_of_lines, + "frames_per_line": frames_per_line, + "exposure_time": exposure_time, + "invert_direction": invert_direction, + "use_centring_table": use_centring_table, + "use_fast_mesh_scans": use_fast_mesh_scans, + } diff --git a/robot_lib.py b/robot_lib.py index e553f9c2..e92478c4 100644 --- a/robot_lib.py +++ b/robot_lib.py @@ -122,6 +122,9 @@ def closePorts(): def rebootEMBL(): robot.rebootEMBL() +def parkRobot(): + robot.parkRobot() + def parkGripper(): robot.parkGripper() diff --git a/start_bs.py b/start_bs.py index 3d5f7499..44064966 100755 --- a/start_bs.py +++ b/start_bs.py @@ -9,8 +9,6 @@ from mxtools.governor import _make_governors from ophyd.signal import EpicsSignalBase EpicsSignalBase.set_defaults(timeout=10, connection_timeout=10) # new style -from mxbluesky.devices import (WorkPositions, TwoClickLowMag, LoopDetector, MountPositions, - TopAlignerFast, TopAlignerSlow, GoniometerStack) #12/19 - author unknown. DAMA can help """ @@ -87,6 +85,7 @@ class ABBIXMercury(Mercury1, SoftDXPTrigger): pass + class VerticalDCM(Device): b = Cpt(EpicsMotor, '-Ax:B}Mtr') g = Cpt(EpicsMotor, '-Ax:G}Mtr') @@ -107,12 +106,14 @@ def filter_camera_data(camera): camera.stats5.read_attrs = ['total', 'centroid'] class SampleXYZ(Device): - x = Cpt(EpicsMotor, ':GX}Mtr') - y = Cpt(EpicsMotor, ':PY}Mtr') - z = Cpt(EpicsMotor, ':PZ}Mtr') + x = Cpt(EpicsMotor, ':X}Mtr') + y = Cpt(EpicsMotor, ':Y}Mtr') + z = Cpt(EpicsMotor, ':Z}Mtr') omega = Cpt(EpicsMotor, ':O}Mtr') if (beamline=="amx"): + from mxbluesky.devices import (WorkPositions, TwoClickLowMag, LoopDetector, MountPositions, + TopAlignerFast, TopAlignerSlow, GoniometerStack) mercury = ABBIXMercury('XF:17IDB-ES:AMX{Det:Mer}', name='mercury') mercury.read_attrs = ['mca.spectrum', 'mca.preset_live_time', 'mca.rois.roi0.count', 'mca.rois.roi1.count', 'mca.rois.roi2.count', 'mca.rois.roi3.count'] @@ -146,6 +147,8 @@ class SampleXYZ(Device): elif beamline == "fmx": + from mxbluesky.devices import (WorkPositions, TwoClickLowMag, LoopDetector, MountPositions, + TopAlignerFast, TopAlignerSlow, GoniometerStack) mercury = ABBIXMercury('XF:17IDC-ES:FMX{Det:Mer}', name='mercury') mercury.read_attrs = ['mca.spectrum', 'mca.preset_live_time', 'mca.rois.roi0.count', 'mca.rois.roi1.count', 'mca.rois.roi2.count', 'mca.rois.roi3.count'] @@ -180,19 +183,31 @@ class SampleXYZ(Device): import setenergy_lsdc elif beamline=="nyx": + from mxbluesky.devices.md2 import LightDevice, BeamstopDevice, MD2SimpleHVDevice, MD2Device, ShutterDevice + two_click_low = mount_pos = loop_detector = top_aligner_fast = top_aligner_slow = work_pos = None mercury = ABBIXMercury('XF:17IDC-ES:FMX{Det:Mer}', name='mercury') mercury.read_attrs = ['mca.spectrum', 'mca.preset_live_time', 'mca.rois.roi0.count', 'mca.rois.roi1.count', 'mca.rois.roi2.count', 'mca.rois.roi3.count'] vdcm = VerticalDCM('XF:17IDA-OP:FMX{Mono:DCM', name='vdcm') + md2 = MD2Device("XF:19IDC-ES{MD2}:", name="md2") + gonio = md2 + shutter = ShutterDevice('XF:19IDC-ES{MD2}:', name='shutter') + beamstop = BeamstopDevice('XF:19IDC-ES{MD2}:', name='beamstop') + front_light = LightDevice('XF:19IDC-ES{MD2}:Front', name='front_light') + back_light = LightDevice('XF:19IDC-ES{MD2}:Back', name='back_light') + aperature = MD2SimpleHVDevice('XF:19IDC-ES{MD2}:Aperature', name='aperature') + scintillator = MD2SimpleHVDevice('XF:19IDC-ES{MD2}:Scintillator', name='scintillator') + capillary = MD2SimpleHVDevice('XF:19IDC-ES{MD2}:Capillary', name='capillary') zebra = Zebra('XF:19IDC-ES{Zeb:1}:', name='zebra') from nyxtools.vector import VectorProgram vector = VectorProgram("XF:19IDC-ES{Gon:1-Vec}", name="vector") from mxtools.eiger import EigerSingleTriggerV26 detector = EigerSingleTriggerV26("XF:19ID-ES:NYX{Det:Eig9M}", name="detector", beamline=beamline) - from nyxtools.flyer_eiger2 import NYXEiger2Flyer - flyer = NYXEiger2Flyer(vector, zebra, detector) - from mxtools.raster_flyer import MXRasterFlyer - raster_flyer = MXRasterFlyer(vector, zebra, detector) + #from nyxtools.flyer_eiger2 import NYXEiger2Flyer + from mxbluesky.md2_flyers import MD2StandardFlyer, MD2VectorFlyer, MD2RasterFlyer + flyer = MD2StandardFlyer(md2, detector) + vector_flyer = MD2VectorFlyer(md2, detector) + raster_flyer = MD2RasterFlyer(md2, detector) from nyxtools.isara_robot import IsaraRobotDevice from denso_robot import OphydRobot @@ -201,10 +216,12 @@ class SampleXYZ(Device): govs = _make_governors("XF:19IDC-ES", name="govs") gov_robot = govs.gov.Robot - back_light = EpicsSignal(read_pv="XF:19IDD-CT{DIODE-Box_D1:4}InCh00:Data-RB",write_pv="XF:19IDD-CT{DIODE-Box_D1:4}OutCh00:Data-SP",name="back_light") + det_move_done = EpicsSignalRO("XF:19IDC-ES{Det:1-Ax:Z}Mtr.DMOV", name="det_move_done") + #back_light = EpicsSignal(read_pv="XF:19IDD-CT{DIODE-Box_D1:4}InCh00:Data-RB",write_pv="XF:19IDD-CT{DIODE-Box_D1:4}OutCh00:Data-SP",name="back_light") back_light_low_limit = EpicsSignalRO("XF:19IDD-CT{DIODE-Box_D1:4}CfgCh00:LowLimit-RB",name="back_light_low_limit") back_light_high_limit = EpicsSignalRO("XF:19IDD-CT{DIODE-Box_D1:4}CfgCh00:HighLimit-RB",name="back_light_high_limit") back_light_range = (back_light_low_limit.get(), back_light_high_limit.get()) + samplexyz = SampleXYZ("XF:19IDC-ES{Gon:1-Ax", name="samplexyz") else: raise Exception(f"Invalid beamline name provided: {beamline}") diff --git a/threads.py b/threads.py index 45118e26..a8a5457e 100644 --- a/threads.py +++ b/threads.py @@ -1,6 +1,7 @@ from qtpy.QtCore import QThread, QTimer, QEventLoop, Signal, QPoint, Qt, QObject from qtpy import QtGui from PIL import Image, ImageQt +import cv2 import os import sys import urllib @@ -8,6 +9,10 @@ import logging from config_params import SERVER_CHECK_DELAY import raddoseLib +import cv2 +import time +import numpy as np +import requests logger = logging.getLogger() @@ -33,42 +38,59 @@ def camera_refresh(self): self.frame_ready.emit(pixmap_orig) self.showing_error = True - if self.camera_object: - retval,self.currentFrame = self.camera_object.read() + if self.video_capture: + if self.new_mjpg_url != self.old_mjpg_url: + self.video_capture.open(self.new_mjpg_url) + self.old_mjpg_url = self.new_mjpg_url + retval,self.currentFrame = self.video_capture.read() + if self.currentFrame is None: - logger.debug('no frame read from stream URL - ensure the URL does not end with newline and that the filename is correct') + #logger.debug('no frame read from stream URL - ensure the URL does not end with newline and that the filename is correct') return + height,width=self.currentFrame.shape[:2] qimage= QtGui.QImage(self.currentFrame,width,height,3*width,QtGui.QImage.Format_RGB888) qimage = qimage.rgbSwapped() pixmap_orig = QtGui.QPixmap.fromImage(qimage) if self.width and self.height: pixmap_orig = pixmap_orig.scaled(self.width, self.height) - + + if not self.showing_error: self.frame_ready.emit(pixmap_orig) - def __init__(self, *args, delay=1000, url='', camera_object=None, width=None, height=None,**kwargs): + def __init__(self, *args, delay=1000, url='', mjpg_url=None, width=None, height=None,**kwargs): self.delay = delay self.width = width self.height = height self.url = url - self.camera_object = camera_object + self.mjpg_url = mjpg_url + self.old_mjpg_url = None + self.new_mjpg_url = None + self.video_capture = None + if self.mjpg_url and self.mjpg_url.lower().endswith(".mjpg"): + self.video_capture = cv2.VideoCapture(self.mjpg_url) + self.old_mjpg_url = self.mjpg_url + self.new_mjpg_url = self.mjpg_url + self.mjpg_url = None self.showing_error = False self.is_running = True QThread.__init__(self, *args, **kwargs) - def updateCam(self, camera_object): - self.camera_object = camera_object + def updateCam(self, url): + if url.lower().endswith(".mjpg"): + self.new_mjpg_url = url def run(self): while self.is_running: self.camera_refresh() self.msleep(self.delay) + def stop(self): self.is_running = False + self.wait() class RaddoseThread(QThread): diff --git a/utils/healthcheck.py b/utils/healthcheck.py index 0e8bd601..0120b711 100644 --- a/utils/healthcheck.py +++ b/utils/healthcheck.py @@ -75,7 +75,7 @@ def check_working_directory(): check_working_directory.remediation = f'Please start LSDC in {daq_utils.beamline} data directory. Current directory: {working_dir}' return False if daq_utils.getBlConfig("visitDirectory") != os.getcwd(): - check_working_directory.remediation = ("Working directory mismatch. Please start LSDC GUI in the same folder as the server is running.") + check_working_directory.remediation = (f"Working directory mismatch. Please start LSDC GUI in the same folder as the server is running.") return False return True diff --git a/utils/resolution_calculator.py b/utils/resolution_calculator.py new file mode 100644 index 00000000..e0216fb3 --- /dev/null +++ b/utils/resolution_calculator.py @@ -0,0 +1,79 @@ +import math + + +class Calculator: + """ + Make a calculator object that can calculate resolution formulas (and nothing else) + + """ + + def __init__(self): + self.r = None + self.d = None + self.L = None + self.theta = None + self.wavelength = None + + def set_all_variables(self, variable_dict): + for key in variable_dict: + self.set_variables(key, variable_dict[key]) + + def set_variables(self, name, value): + if name == "r": + self.r = value + elif name == "d": + self.d = value + elif name == "L": + self.L = value + elif name == "theta": + self.theta = value + elif name == "wavelength": + self.wavelength = value + + def calcD(self, r=None, L=None, wavelength=None, theta=None): + r = r or self.r + L = L or self.L + wavelength = wavelength or self.wavelength + theta = theta or self.theta + try: + denominator = 2 * (math.sin((0.5 * math.atan(r / L)) + theta)) + numerator = wavelength + return numerator / denominator + except Exception as e: + return e + + def calcL(self, r=None, d=None, wavelength=None, theta=None): + r = r or self.r + d = d or self.d + wavelength = wavelength or self.wavelength + theta = theta or self.theta + try: + denominator = math.tan((2 * math.asin(wavelength / (2 * d))) - (2 * theta)) + numerator = r + return numerator / denominator + except Exception as e: + return e + + def calcTheta(self, r=None, L=None, wavelength=None, d=None): + r = r or self.r + L = L or self.L + wavelength = wavelength or self.wavelength + d = d or self.d + try: + val1 = math.asin(wavelength / (2 * d)) + val2 = 0.5 * math.atan(r / L) + return val1 - val2 + except Exception as e: + return e + + def calcWavelength(self, r=None, L=None, d=None, theta=None): + r = r or self.r + L = L or self.L + d = d or self.d + theta = theta or self.theta + valin = 0.5 * math.atan(r / L) + try: + wavelength = 2 * d * math.sin(valin + theta) + return wavelength + except Exception as e: + return e