feat: migrate shifttool python service

This commit is contained in:
Benjamin Labrecque
2026-07-16 16:09:21 +02:00
parent 4e27b6fe7a
commit 07f68e6634
3 changed files with 234 additions and 57 deletions
@@ -6,6 +6,7 @@ requires-python = "==3.10.*"
dependencies = [
"agebd",
]
# TODO: elog
[dependency-groups]
dev = [
@@ -5,9 +5,9 @@ from agebd.runner import CallbackRunner
from agebd.utils import init_logging
from agebd_shifttool import PVs, Service
# TODO: remove: cicd test 10
def main(
log_level: LogLevel = typer.Option(
LogLevel.INFO,
@@ -1,12 +1,17 @@
import locale
import time
from datetime import datetime
from time import time
from epics import dbr
import elog
from agebd.pv import LocalPVLink
from agebd.pv import get_pv_class
from agebd.service.base import BaseService
from agebd.service.pvs import BasePVs
from agebd.utils import printgetversion
from agebd.pv import get_pv_class
# Setze die Locale auf Deutsch (z.B. 'de_DE' oder 'de_CH' für Schweiz)
locale.setlocale(locale.LC_TIME, "de_CH.UTF-8") # oder 'de_CH.UTF-8'
__version__ = printgetversion(__file__)
@@ -14,36 +19,147 @@ PV = get_pv_class()
class PVs(BasePVs):
# Option 1:
## Service specific PVs from dedicated IOC
my_pv1 = PV("AGEBD-SHIFTTOOL:AO") # TODO: is this a good PV to use?
req_MakeElog = PV("AGEOP-SHIFTTOOL:REQ-SHIFT-ENTRY.VAL")
req_UpdateTeam = PV("AGEOP-SHIFTTOOL:REQ-UPDATE-TEAM.VAL")
## Service specific PVs from other IOCs
# ...
shift = PV("AGEOP-SHIFTTOOL:TXT-SHIFT.VAL")
crew = PV("AGEOP-SHIFTTOOL:TXT-SHIFT-CREW.VAL")
operators = [
PV("AGEOP-SHIFTTOOL:TXT-OPERATOR-01.VAL"),
PV("AGEOP-SHIFTTOOL:TXT-OPERATOR-02.VAL"),
PV("AGEOP-SHIFTTOOL:TXT-OPERATOR-03.VAL"),
PV("AGEOP-SHIFTTOOL:TXT-OPERATOR-04.VAL"),
PV("AGEOP-SHIFTTOOL:TXT-OPERATOR-05.VAL"),
]
links = {
"Problem-01": [
PV("AGEOP-SHIFTTOOL:LINK-Problem-01.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Problem&pAutor=Rast%2C%20Streich%2C%20Weber&pEffekt=Downtime",
],
"Problem-02": [
PV("AGEOP-SHIFTTOOL:LINK-Problem-02.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Problem&pAutor=Rast%2C%20Streich%2C%20Weber&pEffekt=Beamdrop",
],
"Problem-03": [
PV("AGEOP-SHIFTTOOL:LINK-Problem-03.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Problem&pAutor=Rast%2C%20Streich%2C%20Weber&pEffekt=Lifetime",
],
"Problem-04": [
PV("AGEOP-SHIFTTOOL:LINK-Problem-04.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Problem&pAutor=Rast%2C%20Streich%2C%20Weber&pEffekt=Beamsize",
],
"Problem-05": [
PV("AGEOP-SHIFTTOOL:LINK-Problem-05.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Problem&pAutor=Rast%2C%20Streich%2C%20Weber&pEffekt=keiner",
],
"Problem-06": [
PV("AGEOP-SHIFTTOOL:LINK-Problem-06.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Problem&pAutor=Rast%2C%20Streich%2C%20Weber&pEffekt=anderer",
],
"Optimierung-01": [
PV("AGEOP-SHIFTTOOL:LINK-Optimierung-01.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Tuning&pAutor=Rast%2C%20Streich%2C%20Weber&pTitel=Optimierung%20Transfer",
],
"Optimierung-02": [
PV("AGEOP-SHIFTTOOL:LINK-Optimierung-02.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Tuning&pAutor=Rast%2C%20Streich%2C%20Weber&pTitel=Optimierung%20Tau%2FYsig",
],
"Optimierung-03": [
PV("AGEOP-SHIFTTOOL:LINK-Optimierung-03.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Tuning&pAutor=Rast%2C%20Streich%2C%20Weber&pTitel=Optimierung",
],
"Optimierung-04": [
PV("AGEOP-SHIFTTOOL:LINK-Optimierung-04.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Tuning&pAutor=Rast%2C%20Streich%2C%20Weber&pTitel=Optimierung",
],
"Optimierung-05": [
PV("AGEOP-SHIFTTOOL:LINK-Optimierung-05.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Tuning&pAutor=Rast%2C%20Streich%2C%20Weber&pTitel=Optimierung",
],
"Optimierung-06": [
PV("AGEOP-SHIFTTOOL:LINK-Optimierung-06.VAL"),
"https://elog-gfa.psi.ch/sls/?cmd=Neu&pEintrag=Tuning&pAutor=Rast%2C%20Streich%2C%20Weber&pTitel=Optimierung",
],
"Piquet": [
PV("AGEOP-SHIFTTOOL:LINK-Piquet.VAL"),
],
"Info": [
PV("AGEOP-SHIFTTOOL:LINK-Info.VAL"),
],
"AktuellesProtokoll": [
PV("AGEOP-SHIFTTOOL:LINK-AktuellesProtokoll.VAL"),
],
"AndereEintraege-01": [
PV("AGEOP-SHIFTTOOL:LINK-AndereEintraege-01.VAL"),
],
"AndereEintraege-02": [
PV("AGEOP-SHIFTTOOL:LINK-AndereEintraege-02.VAL"),
],
"AndereEintraege-03": [
PV("AGEOP-SHIFTTOOL:LINK-AndereEintraege-03.VAL"),
],
"ListenAnzeigen-01": [
PV("AGEOP-SHIFTTOOL:LINK-ListenAnzeigen-01.VAL"),
],
"ListenAnzeigen-02": [
PV("AGEOP-SHIFTTOOL:LINK-ListenAnzeigen-02.VAL"),
],
"ListenAnzeigen-03": [
PV("AGEOP-SHIFTTOOL:LINK-ListenAnzeigen-03.VAL"),
],
"ListenAnzeigen-04": [
PV("AGEOP-SHIFTTOOL:LINK-ListenAnzeigen-04.VAL"),
],
"PiquetWoche": [
PV("AGEOP-SHIFTTOOL:LINK-PiquetWoche.VAL"),
],
"OffeneAuftraege": [
PV("AGEOP-SHIFTTOOL:LINK-OffeneAuftraege.VAL"),
],
}
minisave = [
PV("AGEBD-PARAMS:LIFETIME.VAL"),
PV("AGEBD-DBPM3CURR:CURRENT-AVG.VAL"),
PV("AGEBD-PARAMS:INJECTION-RATE.VAL"),
PV("AGEBD-DBPM3CURR:ACCUMULATED-BEAM-DOSE.VAL"),
PV("AGEOP-CPCL-TRANSMISSION:ICT-BRTL_2_BPM-RING.VAL"),
PV("ARSGE-CECL-FOFB1:X-RMS-UM.VAL"),
PV("ARSGE-CECL-FOFB1:Y-RMS-UM.VAL"),
PV("X01DD-FE-ABS1:STATUS.VAL"),
PV("X08DB-FE-ABS1:STATUS.VAL"),
PV("X01DA-FE-ABS1:STATUS.VAL"),
PV("X01DA-OP-PSYS:SH-A-OPEN.VAL"),
PV("X02DA-FE-ABS1:STATUS.VAL"),
PV("X02DA-OP-PSYS:SH-A-OPEN.VAL"),
PV("X06DA-FE-ABS1:STATUS.VAL"),
PV("X06DA-OP-PSYS:SH-A-OPEN.VAL"),
PV("X07DA-FE-ABS1:STATUS.VAL"),
PV("X07D-OP-PSYS:SH-A-OPEN.VAL"),
PV("X07DB-FE-ABS1:STATUS.VAL"),
PV("X07D-OP-PSYS:SH-B-OPEN.VAL"),
PV("X10DA-FE-ABS1:STATUS.VAL"),
PV("X10DA-OP-PSYS:SH-A-OPEN.VAL"),
PV("X04SA-UIND:GAP-READ.VAL"),
PV("X04SA-UIND:SCTRL.VAL"),
PV("X06SA-UIND:GAP-READ.VAL"),
PV("X06SA-UIND:SCTRL.VAL"),
PV("X09LB-UIND:GAP-READ.VAL"),
PV("X09LB-UIND:SCTRL.VAL"),
PV("X10SA-UIND:GAP-READ.VAL"),
PV("X10SA-UIND:SCTRL.VAL"),
PV("X12SA-UIND:GAP-READ.VAL"),
PV("X12SA-UIND:SCTRL.VAL"),
]
NShiftTasks = PV("AGEOP-SHIFTTOOL:CNT-TODO.VAL")
def __init__(self, service_name: str, pv_factory=PV):
super().__init__(service_name, pv_factory)
PV = pv_factory
# TODO: can we do this cleaner? Maybe mechanism for
# settings initial values in dev mode (e.g. ini file)
if isinstance(self.onoff, LocalPVLink):
self.onoff.value = True
# Option 2:
## Service specific PVs from dedicated IOC
self.my_pv2 = PV(
"AGEBD-SHIFTTOOL:BO"
) # TODO: is this a good PV to use?
## Service specific PVs from other IOCs
# ...
# TODO: class attribute names usually lowercase
# usually it is advisable to trigger the mainloop of a service on changes of certain PVs:
self.CallbackPV = PV(
"AGEBD-SHIFTTOOL:CALLBACK", auto_monitor=dbr.DBE_VALUE
) # TODO: ok?
self.CallbackPV = PV("AGEOP-SHIFTTOOL:REQ-CALC.VAL")
class Service(BaseService[PVs]):
@@ -56,38 +172,98 @@ class Service(BaseService[PVs]):
):
super().__init__(name, pvs, version, sleep_interval)
# TODO: What is this for?
self.path = "sls/bd/bin"
# FACILITY = SLS
self.logbook = elog.open(
"https://elog-gfa.psi.ch/SLS/",
user="robot",
password="robot",
use_ssl=True,
)
self.path = "/sls/op/data/applications/minisave/"
def minisave(self):
# table overview of current machine and beamline parameters
return
def update(self):
# when callback is fired, this code will be executed
pvs = self.pvs
if self.CallbackFired:
#######################################################################################
#######################################################################################
# here goes all the code that your service
# is supposed to be running when the callback is fired
time.sleep(1)
#######################################################################################
#######################################################################################
self.req_UpdateTeam = pvs.req_UpdateTeam.get()
self.req_MakeElog = pvs.req_MakeElog.get()
print(self.req_UpdateTeam, self.req_MakeElog)
# if shift crew update request detected update current shift crew
if self.req_UpdateTeam or self.req_MakeElog:
pvs.req_UpdateTeam.put(0)
print("Team aktualisieren")
self.logtxt = []
self.logtxt.append("Eingetragene Mannschaft:")
self.date = datetime.now()
self.hour = int(self.date.strftime("%H"))
# Format the date
self.day_abbr = self.date.strftime(
"%a"
) # Abbreviated weekday (e.g., "Mi" for Mittwoch)
self.date_str = self.date.strftime(
"%d-%b-%Y"
) # Day-Month-Year (e.g., "17-Jun-2026")
self.week_num = self.date.isocalendar()[1] # ISO calendar week number (e.g., 25)
self.timestamp = f"{self.day_abbr} {self.date_str} KW{self.week_num}"
if self.hour > 4 and self.hour < 8:
self.shift = "Früh-Schicht"
elif self.hour > 12 and self.hour < 16:
self.shift = "Spät-Schicht"
elif self.hour > 20:
self.shift = "Nacht-Schicht"
else:
self.shift = "Verschlafen"
self.shift += ", " + self.timestamp
pvs.shift.put(self.shift)
self.operators = []
for op in pvs.operators:
op = op.get(as_string=True)
if len(op) > 1:
self.operators.append(op)
print(self.operators)
self.crew = ", ".join(self.operators)
pvs.crew.put(self.crew)
self.logtxt.append(self.crew)
print(self.crew)
# if shift entry request detected and current shift entry not yet made, make it
if self.req_MakeElog:
pvs.req_MakeElog.put(0)
print("Schicht eintragen")
# Write to elog
self.logtxt = "".join(self.logtxt)
self.dict_of_attributes = {
"Wann": time(),
"Autor": self.crew,
"Eintrag": "Schicht-Übergabe",
"Titel": self.shift,
"Encoding": "html",
}
print(" ...elog entry prepared...")
self.msg_id = self.logbook.post(
self.logtxt,
attributes=self.dict_of_attributes,
suppress_email_notification=True,
attribute_as_param="value",
)
print(" ...elog entry submitted...")
# callback done
self.CallbackFired = 0
#######################################################################################
#######################################################################################
# here goes all the code that your service
# is supposed to be running in any case, e.g.,
# update some pvs/vals
my_pv1_random_val = str(time.clock_gettime_ns(0))[-1]
self.pvs.my_pv1.put(my_pv1_random_val)
# self.pvs.my_pv2.put("MY_PV2 value")
# get some pvs/vals
self.val1 = self.pvs.my_pv1.get()
self.val2 = self.pvs.my_pv2.get()
# maybe sleep to limit the update loop execution rate
time.sleep(1)
#######################################################################################
#######################################################################################