various fixes, state as of 2026-05-18

instead of removing setup files, they are changed to lowlevel,
so no longer visible, but not creating any errors when loaded
This commit is contained in:
2026-05-19 11:47:01 +02:00
parent f14f1a7414
commit 9eaf6e7ff2
3 changed files with 147 additions and 111 deletions
+132 -93
View File
@@ -39,9 +39,12 @@ from frappy_sinq.secop.devices import SecNodeDevice, SecopDevice, DefunctDevice,
from nicos.core.utils import createThread
from nicos.utils.comparestrings import compare
from nicos.devices.secop.devices import get_attaching_devices
from nicos.utils import loggers
from nicos.utils import loggers, printTable
from nicos.services.daemon.script import ScriptRequest
from nicos.commands import helparglist, usercommand
from nicos.commands.basic import RemoveSetup, AddSetup
from linsetools.frappy import FrappyControl
from linsetools.base import write_content
SETUP_TEMPLATE = """description = '%(desc)s'
@@ -134,8 +137,7 @@ def shorten(value):
return result[:-1] if result.endswith(',') else result
def get_meanings(modname, moddesc):
meaning = moddesc['properties'].get('meaning')
def get_meanings(modname, meaning, hastarget):
result = {}
if meaning:
meaning_name, importance = meaning
@@ -146,7 +148,7 @@ def get_meanings(modname, moddesc):
if meaning_name == 'temperature_regulation':
# add temperature_regulation to temperature list, with very low importance
result.setdefault('temperature', []).append((importance - 100, modname))
elif meaning_name == 'temperature' and moddesc['parameters'].get('target'):
elif meaning_name == 'temperature' and hastarget:
result.setdefault('temperature_regulation', []).append((importance, modname))
return result
@@ -171,12 +173,13 @@ class FrappyManager(Readable):
_box2uri = None
_uri2box = None
_secnodes = None # a dict like Secnodes instance
_do_cleanup = False
def doInit(self, mode):
if SETUPDIR not in session._setup_paths:
session.log.error('can not use frappy as %r is not in the setup_subdirs', SETUPDIR)
expt = session.experiment
if 'persistent_environment' in expt.parameters:
if 'persistent_environment' in expt.parameters and mode != SIMULATION:
expt.persistent_environment = [v for k, v in MEANINGS.items() if k not in SKIP_ENV]
else:
cls = type(expt)
@@ -189,6 +192,10 @@ class FrappyManager(Readable):
self._secnodes = secnodes_from_string(value)
self._value = shorten(value)
def _update_secnodes(self, secnodes):
self._secnodes = secnodes
self.secnodes = secnodes_to_string(self._secnodes)
def doRead(self, maxage=0):
return self._value
@@ -205,10 +212,6 @@ class FrappyManager(Readable):
secnodes = secnodes_from_string(self.secnodes)
else:
secnodes = {k: v for k, v in self._secnodes.items() if k in self._running or not v.startswith('localhost:')}
for name, uri in self._secnodes:
if name not in self._running and uri.startswith('localhost.'):
continue
secnodes[name] = uri
if check_connection:
for name, uri in list(secnodes.items()):
if not uri.startswith('localhost:'):
@@ -219,12 +222,7 @@ class FrappyManager(Readable):
secopclient.disconnect()
self._box2uri[name] = uri
self._uri2box[uri] = name
self._secnodes = secnodes
self.secnodes = secnodes_to_string(secnodes)
self._value = shorten(self.secnodes)
def doPoll(self, maxage=0):
self.doRead()
self._update_secnodes(secnodes)
def doStatus(self, maxage=0):
return status.OK, ''
@@ -259,87 +257,110 @@ class FrappyManager(Readable):
self._check_secnodes(False)
fc = self._frappy_control
# determine which servers to stop/start
tostop = []
tostart = {}
# determine which nodes to add (and start) or remove (and stop)
to_remove = {}
to_add = {}
main_item = None
stick_item = None
for name, uri in self._secnodes:
for name, uri in self._secnodes.items():
if isinstance(uri, Main):
main_item = name
elif isinstance(uri, Stick):
stick_item = name
elif not isinstance(uri, Addons):
self.log.warning('%r is no service', uri)
if main is not None:
if main_item:
tostop.append(main_item)
self._secnodes.pop(main_item)
to_remove[main_item] = self._secnodes.pop(main_item)
if main:
tostart[main] = Main
# auto stick:
if main_item != main and stick is None:
try:
cfg = f'{main}stick'
service, cfgfile = fc.cfg_file(None, 'stick', cfg)
if service == 'stick':
stick = cfg
except FileNotFoundError:
pass
to_add[main] = Main
if main != main_item:
if main in self._secnodes:
to_remove[main] = self._secnodes.pop(main)
# auto stick:
if stick is None:
try:
cfg = f'{main}stick'
service, cfgfile = fc.cfg_file(None, 'stick', cfg)
if service == 'stick':
stick = cfg
except FileNotFoundError:
pass
if stick is not None:
if stick_item:
tostop.append(stick_item)
self._secnodes.pop(stick_item)
to_remove[stick_item] = self._secnodes.pop(stick_item)
if stick:
tostart[stick] = Stick
to_add[stick] = Stick
if stick in self._secnodes:
to_remove[stick] = self._secnodes.pop(stick)
for arglist in (addons,) + extra:
if arglist == '':
tostop.extend(self._secnodes)
to_remove.update(self._secnodes)
self._secnodes.clear()
elif arglist is not None:
# allow legacy '<cfg1>,<cfg2>'
for arg in arglist.split(','):
if arg:
tostart[arg] = None
to_add[arg] = Addons
prev = self._secnodes.get(arg)
if isinstance(prev, (Main, Stick)):
to_remove[arg] = self._secnodes.pop(arg)
toremove = []
for cfg in self._running:
if cfg is None:
continue
# TODO: when using nodename for setup, this may not work here ...
setup = self.get_setup_name(cfg)
if setup in session.loaded_setups:
toremove.append(setup)
if ':' in cfg:
continue
if toremove:
RemoveSetup(*toremove)
for cfg in tostop:
fc.stop(cfg)
if cfg not in tostart:
fc.delete_frappy_service(cfg)
# remove_setups = {self.get_setup_name(cfg) for cfg in to_remove} & set(session.loaded_setups)
# if remove_setups:
# RemoveSetup(*remove_setups)
for cfg in to_remove:
secnode_dev = session.devices.get(f'secnode_{cfg}')
if secnode_dev:
secnode_dev.doShutdown()
for cfg, uri in to_remove.items():
if uri.startswith('localhost:'):
fc.stop(cfg)
fc.delete_frappy_service(cfg)
toadd = []
add_setups = []
# add and start new servers
for arg, service in tostart.items():
uri, name = self.get_uri_name(arg, service)
for arg, servicecls in to_add.items():
uri, name = self.get_uri_name(arg, servicecls)
cfginfo = ''
if isinstance(uri, Service):
if uri.startswith('localhost:'): # isinstance(uri, Service):
port = fc.get_port(uri.name)
uri = fc.add_frappy_service(uri.name, name, port, session.log)
uri = servicecls(fc.add_frappy_service(servicecls.name, name, port, session.log))
cfginfo = f' ({name})'
fc.start(name)
session.log.info('wait for startup of %r%s', uri, cfginfo)
session.log.info('wait for startup of %r%s', uri, cfginfo)
else:
session.log.info('connect to %r%s', uri, cfginfo)
secopclient = self.connect_secnode(uri)
if secopclient:
self.write_setup_file(secopclient, uri, name, arg)
secopclient.disconnect()
toadd.append(self.get_setup_name(name))
add_setups.append(self.get_setup_name(name))
self._secnodes[name] = uri
else:
self._secnodes.pop(name, None)
session.log.exception('cannot connect to %r%s', uri, cfginfo)
self._update_secnodes(self._secnodes)
remove_setups = {self.get_setup_name(cfg) for cfg in to_remove} & set(session.loaded_setups)
noadd_set = session.loaded_setups - set(remove_setups)
add_setups = [v for v in add_setups if v not in noadd_set]
if remove_setups:
RemoveSetup(*remove_setups)
if add_setups:
AddSetup(*add_setups)
session.readSetups()
if toadd:
AddSetup(*toadd)
self._running = self._frappy_control.running()
return self.read()
unused_setups = {f for f in Path(SETUPDIR).glob('se_*.py')
if f.stem not in session.loaded_setups and f.stem[3:] not in self._secnodes}
if unused_setups:
for setup_file in unused_setups:
content = setup_file.read_text()
newcontent = re.sub("\ngroup = '(optional|plugplay)'", "\ngroup = 'lowlevel'", content)
if newcontent != content:
self.log.info('hide unused setup %s', setup_file.stem)
write_content(setup_file, newcontent, group='+rw')
session.readSetups()
return self.read(0)
def get_setup_name(self, cfg):
cfg = cfg.replace(':', '_')
@@ -359,30 +380,17 @@ class FrappyManager(Readable):
depending on their meaning
"""
modules = secopclient.modules
result = {} # dict <meaning name> of list of (<importance>, <target>)
meaning_info = {} # dict <meaning name> of list of (<importance>, <target>)
device_mapping = {}
reserved_names = {v.lower() for v in MEANINGS.values()}
for modname, moddesc in modules.items():
if modname.lower() in reserved_names:
if self.prefix == '' and modname.lower() in reserved_names:
device_mapping[modname] = {'name': f'{modname}_'}
# meanings = self.get_meanings(modname, moddesc)
meaning = moddesc['properties'].get('meaning')
if meaning:
meaning_name, importance = meaning
if meaning_name not in MEANINGS:
self.log.warning('%s: meaning %r is unknown', modname, meaning_name)
continue
result.setdefault(meaning_name, []).append((importance, modname))
if meaning_name == 'temperature_regulation':
# add temperature_regulation to temperature list, with very low importance
result.setdefault('temperature', []).append((importance - 100, modname))
elif meaning_name == 'temperature' and moddesc['parameters'].get('target'):
result.setdefault('temperature_regulation', []).append((importance, modname))
meaning_info.update(get_meanings(modname, moddesc['properties'].get('meaning'),
'target' in moddesc['parameters']))
envlist = []
alias_config = {}
for meaning_name, info in result.items():
for meaning_name, info in meaning_info.items():
importance, modname = sorted(info)[-1]
devname = self._get_device_name(device_mapping, modname)
target = MEANINGS.get(meaning_name)
@@ -395,16 +403,15 @@ class FrappyManager(Readable):
def write_setup_file(self, secopclient, uri, name, origname):
setup = self.get_setup_name(name)
setup_file = Path(SETUPDIR) / f'{setup}.py'
if name != origname:
# sanitize name: replace sequences of non-alphanumeric characters by a single '_'
# also prefix '_' when starting with a digit
# and remove trailing .psi.ch
name = IDSUB.sub(secopclient.nodename.replace('.psi.ch', ''), '_')
self._box2uri[name] = uri
envlist, alias_config, devmap = self.node_setup_info(secopclient)
nodeargs = f'device_mapping={devmap!r}' if devmap else ''
additions = []
if envlist:
aliassetups = [f'{k}_alias' for k in envlist]
# additions.append(f'includes = {aliassetups!r}')
if alias_config:
additions.append(f'alias_config = {alias_config!r}')
desc = secopclient.properties.get('description') or name
@@ -415,18 +422,24 @@ class FrappyManager(Readable):
'uri': repr(uri),
'nodeargs': nodeargs,
'additions': '\n'.join(additions),}
setup_file.write_text(setup_content)
prev_mode = setup_file.stat().st_mode
if not prev_mode & 0x20:
setup_file.chmod(prev_mode | 0x20)
write_content(Path(SETUPDIR) / f'{setup}.py', setup_content, group='+rw')
return envlist
def adjustEnvironment(self):
if self._mode == SIMULATION:
return
self._do_cleanup = True
session.readSetups()
# user = type('User', (), {'name': 'ghost'})()
# session.daemon_device._controller.new_request(ScriptRequest('frappy.cleanup_setups()', '', user))
samenvlist = []
for meaning, devname in MEANINGS.items():
dev = session.devices.get(devname)
if dev:
try:
dev.read(0)
except Exception as e:
self.log.info('error %r when reading %s', e, dev)
if meaning in SKIP_ENV:
continue
if dev:
@@ -450,6 +463,7 @@ class FrappyManager(Readable):
elif devname not in previous:
envlist.append(devname)
expt.setEnvironment(envlist)
self.read(0)
def cleanup_defunct():
@@ -499,13 +513,15 @@ class FrappyNode(SecNodeDevice):
def createDevices(self):
super().createDevices()
self.log.info('--- create devices ---')
secnode = self._attached_secnode
for devname, (_, devcfg) in self.setup_info.items():
meanings = get_meanings(devcfg['secop_module'], devcfg['secop_properties'])
params_cfg = devcfg['params_cfg']
meanings = get_meanings(
devcfg['secop_module'],
devcfg['secop_properties'].get('meaning'),
'target' in params_cfg)
for meaning in meanings:
alias = MEANINGS.get(meaning)
if alias and alias not in session.devices:
self.log.info('create alias %r for %s', alias, meaning)
# alias is not yet created
devcfg = ('nicos.core.device.DeviceAlias', {'descripton': meaning})
session.configured_devices[alias] = devcfg
@@ -514,7 +530,6 @@ class FrappyNode(SecNodeDevice):
elif not alias:
self.log.info('do not know meaning %s', meaning)
if self.param_category:
params_cfg = devcfg['params_cfg']
dev = session.devices[devname]
for pname, pargs in params_cfg.items():
pinfo = dev.parameters[pname]
@@ -582,3 +597,27 @@ class NullDevice(Moveable):
def doStart(self, target):
self.log.warning('disabled, cannot not move')
@usercommand
@helparglist('')
def frappy_list(service=None):
"""list available configuration files"""
fc = FrappyControl('this')
content = []
def prt(line):
content.append(line)
if service is None:
prt('Available configuration files')
prt('')
prt('Hint: if no config file can be found which matches your needs exactly')
prt('make a copy of an existing one, and change the description accordingly')
prt('')
prt('Usage (default argument "main"):')
prt('')
printTable(['command'], [['frappy_list(%r)' % s] for s in fc.services], prt)
fc.listcfg(service or 'main', prt)
session.log.info('\n%s', '\n'.join(content))
+14 -17
View File
@@ -82,6 +82,8 @@ except ImportError:
class NicosSecopClient(SecopClient):
SPECIAL_NAMES = {'target', 'status', 'stop', 'pollinterval', 'reset'}
MANGLED = re.compile(r'(.*)(_*)$')
forbidden_names = set()
def internalize_name(self, name):
"""name mangling
@@ -93,22 +95,18 @@ class NicosSecopClient(SecopClient):
"""
if name in self.SPECIAL_NAMES:
return name
name = super().internalize_name(name).lower()
prefix, sep, postfix = name.partition('_')
if prefix not in dir(SecopMoveable):
return name
# mangle names matching NICOS device attributes
# info -> info_, info_ -> info_1, info_1 -> info_2, ...
if not sep:
if not self.forbidden_names:
self.forbidden_names.update(k.lower() for k in dir(SecopMoveable))
name = super().internalize_name(name)
match = self.MANGLED.match(name)
if match:
basename, postfix = match.groups()
else:
basename, postfix = name, ''
if basename.lower() in self.forbidden_names:
# mangle names matching NICOS device attributes
# info -> info_, info_ -> info__, ...
return name + '_'
if not postfix:
return name + '_1'
try:
num = int(postfix)
if str(num) == postfix:
return '%s_%d' % (prefix, num + 1)
except ValueError:
pass
return name
@@ -330,8 +328,7 @@ class SecNodeDevice(Readable):
def get_setup_info(self):
if self._mode == SIMULATION:
db = session._getSyncDb()
# we might do here in addition:
session.simulationSync(db)
# db = session.simulation_db
return db.get('%s/setup_info' % self.name.lower())
return self.setup_info
+1 -1
View File
@@ -2,7 +2,7 @@ description = 'sample environment basics'
group = 'optional'
display_order = 50.5
# modules = ['frappy_sinq.commandsnew']
modules = ['frappy_sinq.new']
devices = dict(
frappy = device(