Files
AareDAQ/daq/src/aaredaq/server.py
T

671 lines
22 KiB
Python

import asyncio
import io
import os, time
from typing import Tuple, AsyncGenerator
import json
import cv2
import urllib3
import uvicorn
from aaredaqlib.coordinate import SmargonCoordinate, Coordinate
from aaredaqlib.logger_config import setup_logger
from aaredaqlib.models import SampleShortInfo, DAQStatusModel, BeamlineStateEnum, BeamlineSettingsModel, \
SampleShortInfoList, SessionStatus, SampleCameraSettings, AutofocusSettings, TokenData, \
CryojetSettingsModel, SimpleScanParameters, CrystalSize, FluorescenceSpectrumParameterModel, \
FluorescenceSpectrumOutputModel
from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid
from aaredaqlib.rotation_scan import RotationScanRequest, CompletedRotationScan
from aaredaqlib.sample_geometry import SampleGeometryModel
from fastapi import FastAPI, Depends
from fastapi import HTTPException
from fastapi import status as api_status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from starlette.responses import StreamingResponse
from urllib3.exceptions import InsecureRequestWarning
from aaredaq import auth
from aaredaqlib.beamline import mx_beamline
from aaredaq.config import BeamlineConfig
from aaredaq.daq import AareDAQ
from aaredaq.camera_stat_thread import start_image_stats_receiver, stop_image_stats_receiver
app = FastAPI()
# OAuth2 setup
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
bl = mx_beamline()
cfg = BeamlineConfig(bl)
daq = AareDAQ(cfg, bl)
_all_pgroups_cache: dict[str, tuple[list[str], float]] = {}
_ALL_PGROUPS_TTL_S = 60.0 # adjust TTL as needed
logger = setup_logger("aareDAQ")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
data = auth.authenticate_user(cfg, form_data)
return {"access_token": data, "token_type": "bearer"}
@app.get("/status")
async def status(token: str = Depends(oauth2_scheme)) -> DAQStatusModel:
data = auth.parse_token(token)
try:
full = daq.status
active_pgroup = cfg.pgroup
is_staff = data.staff
in_allowed_groups = (active_pgroup is not None and active_pgroup in data.pgroups)
in_ro = is_staff or in_allowed_groups
sample_pgroup = full.sample.user if full.sample is not None else None
sample_view_allowed = sample_pgroup in data.pgroups or is_staff
masked_sample = full.sample if in_ro and sample_view_allowed else None
masked_box = full.box if in_ro else None
masked_best_res = full.last_best_res if in_ro else None
masked_best_b = full.last_best_b_factor if in_ro else None
masked_crystal = full.crystal_size if in_ro else CrystalSize(x=0,y=0,z=0)
return DAQStatusModel(
state=full.state,
busy=full.busy,
geom=full.geom,
diffraction=full.diffraction,
bl=full.bl,
sample=masked_sample,
session=SessionStatus(
current_pgroup=cfg.pgroup,
session=cfg.session_state(data.session),
staff=data.staff
),
box=masked_box,
last_best_res=masked_best_res,
last_best_b_factor=masked_best_b,
crystal_size=masked_crystal
)
except Exception as e:
logger.error(f"Error getting status: {e}")
raise HTTPException(
status_code=api_status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting status: {e}"
)
@app.get("/beamline/geometry")
async def sample_geometry(
token: str = Depends(oauth2_scheme),
) -> SampleGeometryModel:
auth.check_jwt_ro(cfg, auth.parse_token(token))
return daq.sample_geometry
@app.put("/beamline/omega")
async def omega(val: float, token: str = Depends(oauth2_scheme)):
logger.debug(f"Setting omega to {val}")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.omega = val
return "OK"
@app.put("/beamline/light")
async def light(val: float, token: str = Depends(oauth2_scheme)):
logger.debug(f"Setting light to {val}")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.light = val
return "OK"
@app.put("/beamline/zoom")
async def zoom(val: float, token: str = Depends(oauth2_scheme)):
logger.debug(f"Setting zoom to {val}")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.zoom = val
return "OK"
@app.put("/beamline/smargon")
async def smargon(val: SmargonCoordinate, token: str = Depends(oauth2_scheme)):
logger.debug(f"Setting smargon to {val}")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.smargon = val
return "OK"
@app.post("/beamline/tweak_abr_meas_pos")
async def tweak_abr_meas_pos(val: Coordinate, token: str = Depends(oauth2_scheme)):
logger.debug(f"Setting abr to {val}")
auth.check_jwt_staff(cfg, auth.parse_token(token))
daq.tweak_abr_meas_pos(val)
return "OK"
@app.post("/beamline/save_abr_meas_pos")
async def save_abr_meas_pos(token: str = Depends(oauth2_scheme)):
logger.debug(f"Save abr")
auth.check_jwt_staff(cfg, auth.parse_token(token))
daq.save_abr_meas_pos()
return "OK"
@app.post("/beamline/anneal")
async def anneal(time_s: float, token: str = Depends(oauth2_scheme)):
logger.debug(f"Anneal {time_s}")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.anneal(time_s)
return "OK"
@app.post("/beamline/goto_abr_meas_pos")
async def goto_abr_meas_pos(token: str = Depends(oauth2_scheme)):
logger.debug(f"Go to ABR meas pos")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.goto_abr_meas_pos()
return "OK"
@app.post("/beam_mark/add")
async def mark_beam(x: float, y: float, token: str = Depends(oauth2_scheme)):
logger.debug(f"Marking beam to {x}, {y}")
auth.check_jwt_staff(cfg, auth.parse_token(token))
daq.mark_beam(x, y)
return "OK"
@app.post("/beam_mark/clear")
async def clear_beam_mark(token: str = Depends(oauth2_scheme)):
logger.debug(f"Clear beam mark")
auth.check_jwt_staff(cfg, auth.parse_token(token))
daq.clear_mark_beam()
return "OK"
@app.post("/beamline/beam_center")
async def beam_center(x: float, y: float, token: str = Depends(oauth2_scheme)):
logger.debug(f"Beam Center {x}, {y}")
auth.check_jwt_staff(cfg, auth.parse_token(token))
daq.beam_center = (x, y)
return "OK"
@app.post("/beamline/beam_size_mm")
async def beam_size_mm(x: float, y: float, token: str = Depends(oauth2_scheme)):
logger.debug(f"Beam Size {x}, {y}")
auth.check_jwt_staff(cfg, auth.parse_token(token))
daq.beam_size_mm = Coordinate(x=x, y=y)
return "OK"
@app.put("/beamline/samcam")
async def samcam_settings(s: SampleCameraSettings, token: str = Depends(oauth2_scheme)):
logger.debug(f"SamCam settings: {s}")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.samcam_settings = s
return "OK"
@app.post("/samcam/autofocus")
async def samcam_autofocus(s: AutofocusSettings, token: str = Depends(oauth2_scheme)):
logger.debug(f"SamCam AutoFocus")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.autofocus(s)
return "OK"
@app.post("/beamline/shutter")
async def shutter(val: bool, token: str = Depends(oauth2_scheme)):
logger.debug(f"Shutter {val}")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.shutter = val
return "OK"
async def get_image(token: str = Depends(oauth2_scheme)):
auth.check_jwt_ro(cfg, auth.parse_token(token))
_, encoded_image = cv2.imencode(
".jpg", daq.camera_image
) # Encodes the image in JPEG format
image_bytes = io.BytesIO(
encoded_image.tobytes()
) # Convert OpenCV byte format to a file-like object
return StreamingResponse(image_bytes, media_type="image/jpeg")
# TELL procedures
@app.get("/sample/curr_sample")
async def sample(token: str = Depends(oauth2_scheme)) -> SampleShortInfo:
token_data = auth.parse_token(token)
auth.check_jwt_ro(cfg, auth.parse_token(token))
s = daq.sample
if token_data.staff or s.user in token_data.pgroups:
return daq.sample
else:
return SampleShortInfo(
sample_name="Other user sample",
puck_name="",
dewar_name="",
db_id=-1,
pin=s.pin,
location=s.location
)
@app.post("/sample/mount")
async def mount(dbid: int, token: str = Depends(oauth2_scheme), reference: bool = False):
token_data = auth.parse_token(token)
logger.debug(f"Mount {dbid}")
auth.check_jwt_rw(cfg, auth.parse_token(token))
if reference:
st = daq.reference_tools
else:
st = daq.sample_spreadsheet
index = -1
for i in range(len(st.s)):
if st.s[i].db_id == dbid:
index = i
if index == -1:
raise RuntimeError("Sample not found")
if token_data.staff or st.s[index].user in token_data.pgroups:
daq.sample = st.s[index]
return "OK"
else:
raise HTTPException(
status_code=api_status.HTTP_401_UNAUTHORIZED,
detail="Sample belongs to a different user.",
headers={"WWW-Authenticate": "Bearer"},
)
@app.post("/sample/unmount")
async def unmount(token: str = Depends(oauth2_scheme)):
logger.debug(f"Unmount")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.sample = None
return "OK"
@app.post("/sample/manual")
async def manual(s: SampleShortInfo, token: str = Depends(oauth2_scheme)):
logger.debug(f"Manual Sample {s.sample_name}")
auth.check_jwt_rw(cfg, auth.parse_token(token))
print(f"DB ID prior creating {s.db_id}")
daq.create_sample(s)
print(f"DB ID after creating {s.db_id}")
def get_spreadsheet(data: TokenData) -> SampleShortInfoList:
if data.staff:
return cfg.spreadsheet
else:
return cfg.spreadsheet_pgroup(data.pgroups)
def get_reference_tools() -> SampleShortInfoList:
return cfg.reference_tools
async def reference_tools_event_stream() -> AsyncGenerator[str, None]:
try:
while True:
yield get_reference_tools().model_dump_json()
await asyncio.sleep(10)
except asyncio.CancelledError:
return
async def spreadsheet_event_stream(data: TokenData) -> AsyncGenerator[str, None]:
try:
while True:
yield get_spreadsheet(data).model_dump_json()
await asyncio.sleep(10)
except asyncio.CancelledError:
return
@app.get("/sse/spreadsheet")
async def spreadsheet_sse(token: str = Depends(oauth2_scheme)):
data = auth.parse_token(token)
return StreamingResponse(
spreadsheet_event_stream(data),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control"
}
)
@app.get("/sse/reference_tools")
async def reference_tools_sse():
return StreamingResponse(
reference_tools_event_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control"
}
)
@app.get("/sample/spreadsheet")
async def spreadsheet(token: str = Depends(oauth2_scheme)) -> SampleShortInfoList:
return get_spreadsheet(auth.parse_token(token))
@app.get("/sample/reference_tools")
async def reference_tools(token: str = Depends(oauth2_scheme)) -> SampleShortInfoList:
auth.check_jwt_ro(cfg, auth.parse_token(token))
return get_reference_tools()
# State transitions
@app.post("/state/dewar_exchange")
async def dewar_exchange(token: str = Depends(oauth2_scheme)):
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.state = BeamlineStateEnum.DewarTransfer
return "OK"
@app.post("/state/sample_exchange")
async def dewar_exchange(token: str = Depends(oauth2_scheme)):
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.state = BeamlineStateEnum.SampleExchange
return "OK"
@app.post("/state/sample_alignment")
async def sample_alignment(token: str = Depends(oauth2_scheme)):
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.state = BeamlineStateEnum.SampleAlignment
@app.post("/state/beam_location")
async def beam_location(token: str = Depends(oauth2_scheme)):
auth.check_jwt_staff(cfg, auth.parse_token(token))
daq.state = BeamlineStateEnum.BeamLocation
# Scans
@app.post("/scan/raster")
async def raster(val: RasterGridRequest, auto: bool = False, token: str = Depends(oauth2_scheme)) -> CompletedRasterGrid:
auth.check_jwt_rw(cfg, auth.parse_token(token))
return daq.measure_raster(val, auto)
@app.post("/scan/rotation")
async def rotation(val: RotationScanRequest, token: str = Depends(oauth2_scheme)) -> CompletedRotationScan:
auth.check_jwt_rw(cfg, auth.parse_token(token))
return daq.measure_rotation(val)
@app.post("/scan/auto")
async def auto(s: SampleShortInfo, token: str = Depends(oauth2_scheme)):
auth.check_jwt_rw(cfg, auth.parse_token(token))
runtime = daq.measure(s)
return f"{runtime:0.3f}"
@app.post("/scan/smart_params")
async def set_smart_params(p: SimpleScanParameters, token: str = Depends(oauth2_scheme)) -> str:
token_data = auth.parse_token(token)
logger.debug(f"{token_data.session} Try to set smart params: {p}")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.smart_params = p
return "OK"
@app.post("/scan/cancel")
async def cancel(token: str = Depends(oauth2_scheme)):
logger.debug(f"Scan Cancel")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.cancel()
# ALC routines
@app.post("/alc/background")
async def alc_background(token: str = Depends(oauth2_scheme)) -> str:
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.get_background()
return "OK"
@app.post("/alc/center_loop")
async def alc_center_loop(token: str = Depends(oauth2_scheme)) -> str:
logger.debug(f"ALC")
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.auto_loop_center()
return "OK"
@app.post("/alc/ml_bounding_box")
async def alc_ml_bounding_box(token: str = Depends(oauth2_scheme)) -> RasterGridRequest | None:
auth.check_jwt_rw(cfg, auth.parse_token(token))
return daq.ml_bounding_box()
@app.post("/face_detection/run")
async def face_detection_run(steps: int, step_size: int, token: str = Depends(oauth2_scheme)) -> dict:
logger.debug(f"Face detection run: {steps} steps, {step_size} step size")
auth.check_jwt_rw(cfg, auth.parse_token(token))
result = daq.face_detection(steps=steps, step_size=step_size)
return result
# Access management
@app.get("/access/pgroup")
async def pgroup(token: str = Depends(oauth2_scheme)) -> str:
auth.check_jwt_ro(cfg, auth.parse_token(token))
return cfg.pgroup
@app.put("/access/pgroup")
async def set_pgroup(val: str, token: str = Depends(oauth2_scheme)) -> str:
auth.check_jwt_ro(cfg, auth.parse_token(token))
cfg.pgroup = val
return "OK"
@app.delete("/access/pgroup")
async def del_pgroup(token: str = Depends(oauth2_scheme)) -> str:
auth.check_jwt_ro(cfg, auth.parse_token(token))
cfg.pgroup = None
return "OK"
@app.put("/beamline/commissioning_mode")
async def set_commissioning_mode(val: bool, token: str = Depends(oauth2_scheme)) -> str:
auth.check_jwt_staff(cfg, auth.parse_token(token))
cfg.commissioning_mode = val
return "OK"
@app.post("/access/end_session")
async def end_session(token: str = Depends(oauth2_scheme)) -> str:
logger.debug(f"Try to end Session")
# End active session will only delete session, if it is equal to token value
# so no need to check R/W permissions
token_data = auth.parse_token(token)
cfg.end_active_session(token_data.session)
return "OK"
@app.post("/access/force_current_session")
async def force_current_session(token: str = Depends(oauth2_scheme)) -> str:
logger.debug(f"Try to grab session")
data = auth.parse_token(token)
# Counterintuitive, this operation requires only R/O permission
# as this is actually acquiring R/W permissions
auth.check_jwt_ro(cfg, data)
auth.force_current_sesion(cfg, data)
return "OK"
@app.get("/beamline/settings")
async def get_settings(token: str = Depends(oauth2_scheme)) -> BeamlineSettingsModel:
logger.debug(f"Get settings")
auth.check_jwt_staff(cfg, auth.parse_token(token))
return cfg.settings
@app.put("/beamline/settings")
async def put_settings(s: BeamlineSettingsModel, token: str = Depends(oauth2_scheme)):
logger.debug(f"Putting settings: {s}")
auth.check_jwt_staff(cfg, auth.parse_token(token))
cfg.settings = s
@app.get("/beamline/cryo_settings")
async def get_cryo_settings(token: str = Depends(oauth2_scheme)) -> CryojetSettingsModel:
logger.debug(f"Get Cryo Settings")
auth.check_jwt_staff(cfg, auth.parse_token(token))
return cfg.cryojet_settings
@app.put("/beamline/cryo_settings")
async def put_cryo_settings(s: CryojetSettingsModel, token: str = Depends(oauth2_scheme)):
logger.debug(f"Put Cryo Settings: {s}")
auth.check_jwt_staff(cfg, auth.parse_token(token))
cfg.cryojet_settings = s
@app.put("/access/all_pgroups")
async def get_all_pgroups(token: str = Depends(oauth2_scheme)):
logger.debug(f"Get all pgroups")
auth.check_jwt_staff(cfg, auth.parse_token(token))
base_path = "/sls/mx/data/"
now=time.monotonic()
cached = _all_pgroups_cache.get(base_path)
if cached is not None:
items, ts = cached
if (now - ts) < _ALL_PGROUPS_TTL_S:
return items
# Refresh: fast scan using scandir, no regex
try:
names: list[str] = []
with os.scandir(base_path) as it:
for entry in it:
name = entry.name
# quick name filter: p + 5 digits, and directory
if (
len(name) == 6 and
name[0] == "p" and
name[1:].isdigit() and
entry.is_dir(follow_symlinks=False)
):
names.append(name)
names.sort(key=lambda d: int(d[1:]))
_all_pgroups_cache[base_path] = (names, now)
return names
except Exception as e:
logger.error(f"Failed to list pgroups: {e}")
return []
@app.post("/fluorimeter/spectrum")
async def fluorimeter_spectrum(input: FluorescenceSpectrumParameterModel, token: str = Depends(oauth2_scheme)) -> FluorescenceSpectrumOutputModel:
auth.check_jwt_rw(cfg, auth.parse_token(token))
return daq.fluorimeter_take_spectrum(input)
@app.post("/fluorimeter/start")
async def fluorimeter_start(erase: bool = False, token: str = Depends(oauth2_scheme)) -> str:
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.fluorimeter_start(erase)
return "OK"
@app.post("/fluorimeter/stop")
async def fluorimeter_stop(token: str = Depends(oauth2_scheme)) -> str:
auth.check_jwt_rw(cfg, auth.parse_token(token))
daq.fluorimeter_stop()
return "OK"
@app.get("/fluorimeter/status")
async def fluorimeter_status(token: str = Depends(oauth2_scheme)) -> int | None:
auth.check_jwt_ro(cfg, auth.parse_token(token))
return daq.fluorimeter_status()
@app.get("/fluorimeter/data")
async def fluorimeter_data(token: str = Depends(oauth2_scheme)) -> list[int] | None:
auth.check_jwt_ro(cfg, auth.parse_token(token))
return daq.fluorimeter_data()
@app.get("/fluorimeter/background")
async def fluorimeter_background(token: str = Depends(oauth2_scheme)) -> list[int] | None:
auth.check_jwt_ro(cfg, auth.parse_token(token))
return daq.fluorimeter_background()
# Optional: SSE stream for live spectra while acquiring
async def fluorimeter_stream() -> AsyncGenerator[str, None]:
logger.debug("start fluorimeter stream...")
try:
while True:
s = daq.fluorimeter_status()
d = daq.fluorimeter_data()
b = daq.fluorimeter_background()
# Ensure JSON-serializable lists; avoid numpy truthiness
status = "stopped" if s == 0 else "running"
logger.debug(f"fluorimeter status: {status}. got data from fluorimeter: {d[0]} and background: {b[0]}")
data = d.tolist() if hasattr(d, "tolist") else (list(d) if isinstance(d, (tuple, list)) else ([] if d is None else [d]))
bkg = b.tolist() if hasattr(b, "tolist") else (list(b) if isinstance(b, (tuple, list)) else ([] if b is None else [b]))
logger.debug(f"fluorimeter data: {data[0]}. fluorimeter background: {bkg[0]}")
payload = json.dumps({"status": s, "data": data, "background": bkg}, separators=(",", ":"))
yield f"data: {payload}\n\n"
if s == 0:
await asyncio.sleep(0.2)
logger.debug("fluorimeter stream stopped")
break
await asyncio.sleep(0.2)
except asyncio.CancelledError as e:
logger.error(f">>> Fluorimeter stream cancelled: {e}")
return
@app.get("/sse/fluorimeter")
async def sse_fluorimeter(token: str = Depends(oauth2_scheme)):
auth.check_jwt_ro(cfg, auth.parse_token(token))
try:
daq.fluorimeter_start(erase=False)
except Exception:
# Ignore if already running or start not needed
pass
return StreamingResponse(
fluorimeter_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control"
}
)
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(message)s",
"use_colors": True,
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
},
"loggers": {
"uvicorn": {
"handlers": ["default"],
"level": "WARNING",
},
"uvicorn.access": {
"handlers": ["default"],
"level": "WARNING",
},
},
}
urllib3.disable_warnings()
def main():
# Remove in production!
urllib3.disable_warnings()
# Run the application using uvicorn
uvicorn.run("aaredaq.server:app", host="0.0.0.0", port=5210, workers=4, log_config=LOGGING_CONFIG)
if __name__ == "__main__":
#start_image_stats_receiver(zmq_url="tcp://129.129.110.12:9089")
main()
#stop_image_stats_receiver()