From c67bcd579d3f2875f0f1e6dbbd4dd6baff98957c Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Fri, 12 Dec 2025 15:51:09 -1000 Subject: [PATCH 01/14] first commit that adds epics implementation --- examples/EPICS.py | 211 ++++++++++++ examples/fakedcs_cake_config | 261 +++++++++++++++ examples/fakedcs_cake_config.json | 515 ++++++++++++++++++++++++++++++ examples/parse_cake_config.py | 152 +++++++++ sbin/mkd | 2 +- tests/mktl | 1 + 6 files changed, 1141 insertions(+), 1 deletion(-) create mode 100644 examples/EPICS.py create mode 100644 examples/fakedcs_cake_config create mode 100644 examples/fakedcs_cake_config.json create mode 100644 examples/parse_cake_config.py create mode 120000 tests/mktl diff --git a/examples/EPICS.py b/examples/EPICS.py new file mode 100644 index 00000000..3c2b84ee --- /dev/null +++ b/examples/EPICS.py @@ -0,0 +1,211 @@ +""" This is an implementation of a EPICS proxy, enabling full mKTL commands + to a EPICS channel. +""" + +import mktl +import epics +from itertools import product +import pdb + + +class Daemon(mktl.Daemon): + + def __init__(self, store, alias=None, *args, **kwargs): + + # Generate the configuration matching this KTL service. Since this + # configuration is not in the default location it must be declared + # prior to initializing the Daemon. + # store is the mktl store name for the epics object (i.e. k1:mySystem:myDevice) + # alias is the name mKTL can use if you don't want to use pvname (i.e. myDevice) + mktl.Daemon.__init__(self, store, alias, *args, **kwargs) + + def setup(self): + """ The only reason this method exists is to create a EPICS.Item + instance for each and every channel being proxied by this + daemon, as opposed to letting them be the default mktl.Item. + """ + config = self.config[self.uuid] + items = config['items'] + keys = items.keys() + + for key in keys: + self.add_item(Item, key) + + + def setup_final(self): + """ This is the last step before broadcasts go out. This is the + right time to fire up monitoring of all EPICS PVs. + """ + pass + + +# end of class Store + + + +class Item(mktl.Item): + + def __init__(self, *args, **kwargs): + mktl.Item.__init__(self, *args, **kwargs) + + # We want the EPICS channels to be the sole source of broadcast + # events. mKTL defaults to publishing a new value when a SET operation + # completes successfully; this attribute inhibits that behavior. + self.pvname = self.full_key.replace('.', ':') + service = "".join(self.full_key.split('.')[:-1]) + self.pvname = service + ':' + self.key.upper() + + self.publish_on_set = False + + @classmethod + def convert_string_to_binary(cls, value, bits='08b'): + """ Convert a string to 8 bit binary value + """ + return ''.join(f'{byte:08b}' for byte in value.encode('utf-8')) + # return int(''.join(format(ord(char), bits) for char in value), 2) + + + def publish_broadcast(self, ): + """ This method is registered as a KTL callback; take any/all KTL + broadcast events and publish them as mKTL events. + """ + pv = epics.PV(self.pvname) + slice = pv.get_with_metadata(as_string=True) + timestamp = slice.get('timestamp') + value = slice.get('value') + bvalue = self.convert_string_to_binary(value) + + self.publish(bvalue, timestamp) + + + def perform_get(self): + """ Wrap an incoming GET request to a Epics channel get. This method + is only invoked on synchronous GET requests, normally it would + also be invoked when local polling occurs, but this wrapper + relies on epics callbacks to receive asynchronous broadcasts + (see :func:`publish_broadcast`). + """ + pv = epics.PV(self.pvname) + resp = pv.get_with_metadata(as_string=True) # get the value and metadata + timestamp = resp.get('timestamp') + value = resp.get('value') + bvalue = self.convert_string_to_binary(value) + payload = mktl.Payload(bvalue, timestamp) + return payload + + + def perform_set(self, new_value): + """ Wrap an incoming SET request to a Epics channel put. This method + is expected to block until completion of the request. The values + presented at this level are the equivalent of the KTL binary + value, and this needs to be asserted explicitly at the KTL level + to ensure they are interpreted (or not interpreted, as the case + may be) properly. + """ + pv = epics.PV(self.key) + pv.put(new_value, wait=True) + +# end of class Item + + +def describeChannel(name): + """ Construct an mKTL configuration block to describe the named Epics channel. + """ + pv = epics.PV(name) + slice = pv.get_with_metadata(as_string=True) # populate metadata + return slice + + +def describePV(pv: epics.PV): + """ Construct an item-specific mKTL configuration block for a single + Epics channel. + """ + + keyword_dict = dict() + + type = pv.type + type = type_mapping[type.upper()] + keyword_dict['type'] = type + enumerators = None + + try: + enumerators = pv['enum_strs'] + except: + pass + + if enumerators: # ignore empty enumerators + rebuilt = dict() + for key in range(len(enumerators)): + enumerator = enumerators[key] + if enumerator == '': + continue + else: + rebuilt[key] = enumerator + + enumerators = rebuilt + + for attribute in ('units', 'info'): + try: + value = getattr(pv, attribute) + except ValueError: + value = None + + if attribute == 'units' and enumerators is not None: + # Keywords with enumerators overload the 'units' string in order + # to provide the enumerators. Including the 0th enumerator again + # here would be a mistake. + value = None + + if value is not None: + if attribute == 'help': + attribute = 'description' + keyword_dict[attribute] = value + + # make range attribute + try: + lower = getattr(pv, 'lower_ctrl_limit') + upper = getattr(pv, 'upper_ctrl_limit') + except ValueError: + pass + else: + keyword_dict['range'] = {"minimum": lower, "maximum": upper} + + for attribute in ('key', 'read_access', 'write_access'): + try: + if attribute == 'key': + value = pv.pvname + else: + value = getattr(pv, attribute) + except ValueError: + value = None + + if value is False: + keyword_dict[attribute] = value + + if enumerators is not None: + keyword_dict['enumerators'] = enumerators + + return keyword_dict + + +# Translate Epics data types to mKTL types. + +type_mapping = dict() +epics_types = ['double', 'float', 'int', 'string', 'short', 'enum', 'char', 'long'] +epics_variants = ['', 'ctrl', 'time'] +for v, t in product(epics_variants, epics_types): + if v == '': + epics_type = t.upper() + else: + epics_type = f'{v.upper()}_{t.upper()}' + if t in ['double', 'float']: + mktl_type = 'numeric' + elif t in ['int', 'char', 'long']: + mktl_type = 'numeric' + elif t == 'string': + mktl_type = 'string' + elif t == 'enum': + mktl_type = 'enumerated' + type_mapping[epics_type] = mktl_type + +# vim: set expandtab tabstop=8 softtabstop=4 shiftwidth=4 autoindent: diff --git a/examples/fakedcs_cake_config b/examples/fakedcs_cake_config new file mode 100644 index 00000000..73d3ff0e --- /dev/null +++ b/examples/fakedcs_cake_config @@ -0,0 +1,261 @@ +# number of CA events to skip per event handled (now defaulted to 0 anyway) +skip_num = 0 + +# The "%" is replaced with the trailing character from the service (if it +# is a digit). This file can therefore be used with dcs0, dcs1, dcs2 etc. + +prefix = fakedcs: +simprefix = fakedcs: + +# use prefix flag ------------------ +# type --------------- | +# asc2bin method ----------- | | +# bin2asc method ------ | | | +# | | | | +# keyword CA name for read CA name for write V V V V +# +# -- in alphabetical order of KTL keyword name -- + + guider-active in {no,yes} + AUTACTIV AUTACTIV AUTACTIV BOO BOO b 1 + + guider-go-flag in "" + AUTGO AUTGO AUTGO STR STR s 1 + + guider-pause in "" + AUTPAUSE AUTPAUSE AUTPAUSE INT INT i 1 + + guider-resume in "" + AUTRESUM AUTRESUM AUTRESUM INT INT i 1 + + guider-resumeX in arcsec + AUTRESX AUTRESX AUTRESX R2A A2R d 1 + + guider-resumeY in arcsec + AUTRESY AUTRESY AUTRESY R2A A2R d 1 + + guider-seqnum in "" + AUTSQNUM AUTSQNUM AUTSQNUM INT INT i 1 + + guider-stop in "" + AUTSTOP AUTSTOP AUTSTOP INT INT i 1 + + guider-Xcent in arcsec + AUTXCENT AUTXCENT AUTXCENT R2A A2R d 1 + + guider-Ycent in arcsec + AUTYCENT AUTYCENT AUTYCENT R2A A2R d 1 + + axes-control-status in {unknown, + not controlling, + halted, + in position, + in limit, + slewing, + acquiring, + tracking} + AXESTAT AXESTAT AXESTAT ENMM ENMM em 1 + + telescope-azimuth in deg + AZ AZ AZ R2D2 D2R d 1 + + current-instrument in "" + CURRINST CURRINST CURRINST STR STR s 1 + + telescope-elevation in deg + EL EL EL R2D2 D2R d 1 + + instrument-name in "" + INSTRUME CURRINST CURRINST STR STR s 1 + + parallactic-angle-astrometric in deg + PARANG PARANG PARANG R2D2 D2R d 1 + + rotator-base-angle in deg + ROTBASE ROTBASE ROTBASE R2D2 D2R d 1 + + rotator-user-destination in deg + ROTDEST ROTDEST ROTDEST R2D D2R d 1 + +# Note: timestamps are not specific to rotator but only use will be for rot? +# Can always define additional keywords connected to these channels + rotator-destination-timestamp1-ut1-slow in "" + ROTDETS1 ROTDETS1 ROTDETS1 UTC DBL d 1 + + rotator-destination-timestamp2-ut1-slow in "" + ROTDETS2 ROTDETS2 ROTDETS2 UTC DBL d 1 + +# Writable so external rotator can set it + rotator-error-string in "" + ROTERRS ROTERRS ROTERRS STR STR s 1 + +# Writable so external rotator can set it + rotator-error-status in "" + ROTERVL ROTERVL ROTERVL INT INT i 1 + + rotator-halt-command in "" + ROTHALT ROTHALT ROTHALTC BOO BOO b 1 + +# Can be used by external rotator to intercept command + rotator-halt-command-complement in "" + ROTHALTC ROTHALTC ROTHALT BOO BOO b 1 + + rotator-initialization-command in "" + ROTINIT ROTINIT ROTINITC BOO BOO b 1 + +# Can be used by external rotator to intercept command + rotator-initialization-command-complement in "" + ROTINITC ROTINITC ROTINIT BOO BOO b 1 + + rotator-tracking-mode in {unknown, + position angle, + vertical angle, + stationary} + ROTMODE ROTMODE ROTMODE ENMM ENMM em 1 + + rotator-new-destination-slow in "" + ROTNEWS ROTNEWS ROTNEWS INT INT i 1 + + rotator-physical-destination in deg + ROTPDEST ROTPDEST ROTPDEST R2D D2R d 1 + + rotator-physical-destination-slow in deg + ROTPDSTS ROTPDSTS ROTPDSTS R2D D2R d 1 + + rotator-user-position in deg + ROTPOSN ROTPOSN ROTPOSN R2D D2R d 1 + + rotator-position-timestamp1-ut1-slow in "" + ROTPOTS1 ROTPOTS1 ROTPOTS1 UTC DBL d 1 + + rotator-position-timestamp2-ut1-slow in "" + ROTPOTS2 ROTPOTS2 ROTPOTS2 UTC DBL d 1 + + rotator-physical-position in deg + ROTPPOSN ROTPPOSN ROTPPOSN R2D D2R d 1 + + rotator-select in {none, + fcass (forward cass), + cass (cassegrain), + lbc (left bent cass), + rbc (right bent cass)} + ROTSEL ROTSEL ROTSEL ENM ENM e 1 + +# This is not a real dcs keyword. The ROTSPEED allows the developer to +# change the commanded speed from the nominal default of 0.1 deg/sec. + + rotator-fake-speed in deg/sec + ROTSPEED ROTSPEED ROTSPEED DBL DBL d 1 + + rotator-servo-error in arcsec + ROTSRVER ROTSRVER ROTSRVER R2A3 A2R d 1 + +# Writable so external rotator can set it + rotator-state in {unknown, + off, + initializing, + standby, + tracking, + fault, + manual, + halted, + disabled, + in position, + slewing, + acquiring, + in limit} + ROTSTAT ROTSTAT ROTSTAT ENMM ENMM em 1 + + rotator-standby-command in "" + ROTSTBY ROTSTBY ROTSTBYC BOO BOO b 1 + +# Can be used by external rotator to intercept command + rotator-standby-command-complement in "" + ROTSTBYC ROTSTBYC ROTSTBY BOO BOO b 1 + +# Writable so external rotator can set it + rotator-state-string in "" + ROTSTST ROTSTST ROTSTST STR STR s 1 + +# rot IOC heartbeat + rotator-current-time in "" + ROTTIME ROTTIME ROTTIME UTC DBL d 1 + + simulating-dcs in {false, true} + SIMULATE SIMULATE SIMULATE BOO BOO b 1 + + tertiary-error-string in "" + TERTERRS TERTERRS TERTERRS STR STR s 1 + + tertiary-error-status in "" + TERTERVL TERTERVL TERTERRS INT INT i 1 + + tertiary-halt-command in "" + TERTHALT TERTHALT TERTHALTC BOO BOO b 1 + + tertiary-halt-command-complement in "" + TERTHALTC TERTHALTC TERTHALT BOO BOO b 1 + + tertiary-initialization-command in "" + TERTINIT TERTINIT TERTINITC BOO BOO b 1 + + tertiary-initialization-command-complement in "" + TERTINITC TERTINITC TERTINIT BOO BOO b 1 + + move-tertiary in no,yes + TERTMOVE TERTMOVE TERTMOVEC BOO BOO b 1 + + move-tertiary-complement in no,yes + TERTMOVEC TERTMOVEC TERTMOVE BOO BOO b 1 + + tertiary-destination in {lnas, + lbc1, + lbc2, + stowed, + rbc2, + rbc1, + rnas, + cass, + mirrorup} + TERTDEST TERTDEST TERTDEST ENM ENM e 1 + + tertiary-position in {lnas, + lbc1, + lbc2, + stowed, + rbc2, + rbc1, + rnas, + unknown, + cass, + mirrorup} + TERTPOSN TERTPOSN TERTPOSN ENM ENM e 1 + + tertiary-state in {unknown, + off, + initializing, + standby, + tracking, + fault, + manual, + halted, + disabled, + in position, + slewing, + acquiring, + in limit} + TERTSTAT TERTSTAT TERTSTAT ENMM ENMM em 1 + + tertiary-standby-command in "" + TERTSTBY TERTSTBY TERTSTBYC BOO BOO b 1 + + tertiary-standby-command-complement in "" + TERTSTBYC TERTSTBYC TERTSTBY BOO BOO b 1 + + tertiary-state-string in "" + TERTSTST TERTSTST TERTSTST STR STR s 1 + +# Note: is not UTC until timConvert dead-band handling is sorted out + coordinated-universal-time in h + UTC UTC UTC UTC DBL d 1 + diff --git a/examples/fakedcs_cake_config.json b/examples/fakedcs_cake_config.json new file mode 100644 index 00000000..f7b60388 --- /dev/null +++ b/examples/fakedcs_cake_config.json @@ -0,0 +1,515 @@ +{ + "autactiv": { + "read_channel": "AUTACTIV", + "write_channel": "AUTACTIV", + "description": "guider-active in {no,yes}", + "units": { + "base": "", + "formatted": "" + } + }, + "autgo": { + "read_channel": "AUTGO", + "write_channel": "AUTGO", + "description": "guider-go-flag in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "autpause": { + "read_channel": "AUTPAUSE", + "write_channel": "AUTPAUSE", + "description": "guider-pause in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "autresum": { + "read_channel": "AUTRESUM", + "write_channel": "AUTRESUM", + "description": "guider-resume in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "autresx": { + "read_channel": "AUTRESX", + "write_channel": "AUTRESX", + "description": "guider-resumeX in arcsec", + "units": { + "base": "arcsec", + "formatted": "arcsec" + } + }, + "autresy": { + "read_channel": "AUTRESY", + "write_channel": "AUTRESY", + "description": "guider-resumeY in arcsec", + "units": { + "base": "arcsec", + "formatted": "arcsec" + } + }, + "autsqnum": { + "read_channel": "AUTSQNUM", + "write_channel": "AUTSQNUM", + "description": "guider-seqnum in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "autstop": { + "read_channel": "AUTSTOP", + "write_channel": "AUTSTOP", + "description": "guider-stop in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "autxcent": { + "read_channel": "AUTXCENT", + "write_channel": "AUTXCENT", + "description": "guider-Xcent in arcsec", + "units": { + "base": "arcsec", + "formatted": "arcsec" + } + }, + "autycent": { + "read_channel": "AUTYCENT", + "write_channel": "AUTYCENT", + "description": "guider-Ycent in arcsec", + "units": { + "base": "arcsec", + "formatted": "arcsec" + } + }, + "axestat": { + "read_channel": "AXESTAT", + "write_channel": "AXESTAT", + "description": "axes-control-status in {unknown, not controlling, halted, in position, in limit, slewing, acquiring, tracking}", + "units": { + "base": "", + "formatted": "" + } + }, + "az": { + "read_channel": "AZ", + "write_channel": "AZ", + "description": "telescope-azimuth in deg", + "units": { + "base": "deg", + "formatted": "deg" + } + }, + "currinst": { + "read_channel": "CURRINST", + "write_channel": "CURRINST", + "description": "current-instrument in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "el": { + "read_channel": "EL", + "write_channel": "EL", + "description": "telescope-elevation in deg", + "units": { + "base": "deg", + "formatted": "deg" + } + }, + "parang": { + "read_channel": "PARANG", + "write_channel": "PARANG", + "description": "parallactic-angle-astrometric in deg", + "units": { + "base": "deg", + "formatted": "deg" + } + }, + "rotbase": { + "read_channel": "ROTBASE", + "write_channel": "ROTBASE", + "description": "rotator-base-angle in deg", + "units": { + "base": "deg", + "formatted": "deg" + } + }, + "rotdest": { + "read_channel": "ROTDEST", + "write_channel": "ROTDEST", + "description": "rotator-user-destination in deg", + "units": { + "base": "deg", + "formatted": "deg" + } + }, + "rotdets1": { + "read_channel": "ROTDETS1", + "write_channel": "ROTDETS1", + "description": "rotator-destination-timestamp1-ut1-slow in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rotdets2": { + "read_channel": "ROTDETS2", + "write_channel": "ROTDETS2", + "description": "rotator-destination-timestamp2-ut1-slow in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "roterrs": { + "read_channel": "ROTERRS", + "write_channel": "ROTERRS", + "description": "rotator-error-string in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rotervl": { + "read_channel": "ROTERVL", + "write_channel": "ROTERVL", + "description": "rotator-error-status in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rothalt": { + "read_channel": "ROTHALT", + "write_channel": "ROTHALTC", + "description": "rotator-halt-command in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rothaltc": { + "read_channel": "ROTHALTC", + "write_channel": "ROTHALT", + "description": "rotator-halt-command-complement in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rotinit": { + "read_channel": "ROTINIT", + "write_channel": "ROTINITC", + "description": "rotator-initialization-command in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rotinitc": { + "read_channel": "ROTINITC", + "write_channel": "ROTINIT", + "description": "rotator-initialization-command-complement in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rotmode": { + "read_channel": "ROTMODE", + "write_channel": "ROTMODE", + "description": "rotator-tracking-mode in {unknown, position angle, vertical angle, stationary}", + "units": { + "base": "", + "formatted": "" + } + }, + "rotnews": { + "read_channel": "ROTNEWS", + "write_channel": "ROTNEWS", + "description": "rotator-new-destination-slow in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rotpdest": { + "read_channel": "ROTPDEST", + "write_channel": "ROTPDEST", + "description": "rotator-physical-destination in deg", + "units": { + "base": "deg", + "formatted": "deg" + } + }, + "rotpdsts": { + "read_channel": "ROTPDSTS", + "write_channel": "ROTPDSTS", + "description": "rotator-physical-destination-slow in deg", + "units": { + "base": "deg", + "formatted": "deg" + } + }, + "rotposn": { + "read_channel": "ROTPOSN", + "write_channel": "ROTPOSN", + "description": "rotator-user-position in deg", + "units": { + "base": "deg", + "formatted": "deg" + } + }, + "rotpots1": { + "read_channel": "ROTPOTS1", + "write_channel": "ROTPOTS1", + "description": "rotator-position-timestamp1-ut1-slow in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rotpots2": { + "read_channel": "ROTPOTS2", + "write_channel": "ROTPOTS2", + "description": "rotator-position-timestamp2-ut1-slow in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rotpposn": { + "read_channel": "ROTPPOSN", + "write_channel": "ROTPPOSN", + "description": "rotator-physical-position in deg", + "units": { + "base": "deg", + "formatted": "deg" + } + }, + "rotsel": { + "read_channel": "ROTSEL", + "write_channel": "ROTSEL", + "description": "rotator-select in {none, fcass (forward cass), cass (cassegrain), lbc (left bent cass), rbc (right bent cass)}", + "units": { + "base": "", + "formatted": "" + } + }, + "rotspeed": { + "read_channel": "ROTSPEED", + "write_channel": "ROTSPEED", + "description": "rotator-fake-speed in deg/sec", + "units": { + "base": "deg/sec", + "formatted": "deg/sec" + } + }, + "rotsrver": { + "read_channel": "ROTSRVER", + "write_channel": "ROTSRVER", + "description": "rotator-servo-error in arcsec", + "units": { + "base": "arcsec", + "formatted": "arcsec" + } + }, + "rotstat": { + "read_channel": "ROTSTAT", + "write_channel": "ROTSTAT", + "description": "rotator-state in {unknown, off, initializing, standby, tracking, fault, manual, halted, disabled, in position, slewing, acquiring, in limit}", + "units": { + "base": "", + "formatted": "" + } + }, + "rotstby": { + "read_channel": "ROTSTBY", + "write_channel": "ROTSTBYC", + "description": "rotator-standby-command in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rotstbyc": { + "read_channel": "ROTSTBYC", + "write_channel": "ROTSTBY", + "description": "rotator-standby-command-complement in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rotstst": { + "read_channel": "ROTSTST", + "write_channel": "ROTSTST", + "description": "rotator-state-string in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "rottime": { + "read_channel": "ROTTIME", + "write_channel": "ROTTIME", + "description": "rotator-current-time in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "simulate": { + "read_channel": "SIMULATE", + "write_channel": "SIMULATE", + "description": "simulating-dcs in {false, true}", + "units": { + "base": "", + "formatted": "" + } + }, + "terterrs": { + "read_channel": "TERTERRS", + "write_channel": "TERTERRS", + "description": "tertiary-error-string in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "tertervl": { + "read_channel": "TERTERVL", + "write_channel": "TERTERRS", + "description": "tertiary-error-status in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "terthalt": { + "read_channel": "TERTHALT", + "write_channel": "TERTHALTC", + "description": "tertiary-halt-command in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "terthaltc": { + "read_channel": "TERTHALTC", + "write_channel": "TERTHALT", + "description": "tertiary-halt-command-complement in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "tertinit": { + "read_channel": "TERTINIT", + "write_channel": "TERTINITC", + "description": "tertiary-initialization-command in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "tertinitc": { + "read_channel": "TERTINITC", + "write_channel": "TERTINIT", + "description": "tertiary-initialization-command-complement in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "tertmove": { + "read_channel": "TERTMOVE", + "write_channel": "TERTMOVEC", + "description": "move-tertiary in no,yes", + "units": { + "base": "no,yes", + "formatted": "no,yes" + } + }, + "tertmovec": { + "read_channel": "TERTMOVEC", + "write_channel": "TERTMOVE", + "description": "move-tertiary-complement in no,yes", + "units": { + "base": "no,yes", + "formatted": "no,yes" + } + }, + "tertdest": { + "read_channel": "TERTDEST", + "write_channel": "TERTDEST", + "description": "tertiary-destination in {lnas, lbc1, lbc2, stowed, rbc2, rbc1, rnas, cass, mirrorup}", + "units": { + "base": "", + "formatted": "" + } + }, + "tertposn": { + "read_channel": "TERTPOSN", + "write_channel": "TERTPOSN", + "description": "tertiary-position in {lnas, lbc1, lbc2, stowed, rbc2, rbc1, rnas, unknown, cass, mirrorup}", + "units": { + "base": "", + "formatted": "" + } + }, + "tertstat": { + "read_channel": "TERTSTAT", + "write_channel": "TERTSTAT", + "description": "tertiary-state in {unknown, off, initializing, standby, tracking, fault, manual, halted, disabled, in position, slewing, acquiring, in limit}", + "units": { + "base": "", + "formatted": "" + } + }, + "tertstby": { + "read_channel": "TERTSTBY", + "write_channel": "TERTSTBYC", + "description": "tertiary-standby-command in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "tertstbyc": { + "read_channel": "TERTSTBYC", + "write_channel": "TERTSTBY", + "description": "tertiary-standby-command-complement in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "tertstst": { + "read_channel": "TERTSTST", + "write_channel": "TERTSTST", + "description": "tertiary-state-string in \"\"", + "units": { + "base": "", + "formatted": "" + } + }, + "utc": { + "read_channel": "UTC", + "write_channel": "UTC", + "description": "coordinated-universal-time in h", + "units": { + "base": "h", + "formatted": "h" + } + } +} diff --git a/examples/parse_cake_config.py b/examples/parse_cake_config.py new file mode 100644 index 00000000..4aee98b0 --- /dev/null +++ b/examples/parse_cake_config.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Parser to convert fakedcs_cake_config file to JSON format. + +The parser extracts keyword definitions from the config file and creates +a JSON object mapping each keyword to its read and write channels. +""" + +import json +import re +from pathlib import Path + + +def parse_cake_config(input_file, output_file=None): + """ + Parse a cake configuration file and convert it to JSON. + + Args: + input_file: Path to the input config file + output_file: Path to the output JSON file (optional) + + Returns: + dict: Parsed configuration as a dictionary + """ + result = {} + + with open(input_file, 'r') as f: + lines = f.readlines() + + i = 0 + while i < len(lines): + line = lines[i].strip() + + # Skip empty lines and comments + if not line or line.startswith('#'): + i += 1 + continue + + # Skip configuration lines like "skip_num = 0", "prefix = fakedcs:" + if '=' in line and not line.startswith(' '): + i += 1 + continue + + # Check if this is a keyword definition line (starts with whitespace and contains "in") + if line and not line.startswith('#') and ' in ' in line: + # This is a keyword definition line - collect potentially multi-line description + description_parts = [line.strip()] + + # Check if there are continuation lines (lines that don't contain mapping info) + j = i + 1 + while j < len(lines): + next_line = lines[j].strip() + # If the next line is empty, a comment, or looks like a mapping line (uppercase keyword), stop + if not next_line or next_line.startswith('#'): + break + # Check if it's a mapping line (starts with uppercase letters and has multiple columns) + parts = next_line.split() + if len(parts) >= 3 and parts[0].isupper(): + # This is the mapping line + break + # Otherwise, it's a continuation of the description + description_parts.append(next_line) + j += 1 + + # Join all description parts + full_description = ' '.join(description_parts) + + # Extract units from the description (after "in") + units = "" + if ' in ' in full_description: + units_part = full_description.split(' in ', 1)[1] + # Remove curly braces and their contents if present + units = re.sub(r'\{[^}]*\}', '', units_part).strip() + # If units is empty or just quotes, set to empty string + if units in ['""', '']: + units = "" + + # Move to the mapping line + i = j + if i < len(lines): + mapping_line = lines[i].strip() + + # Parse the mapping line to extract the three channel names + # Format: KEYWORD READ_CHANNEL WRITE_CHANNEL [other fields...] + parts = mapping_line.split() + + if len(parts) >= 3: + keyword = parts[0].lower() # First column (lowercase) + read_channel = parts[1] # Second column + write_channel = parts[2] # Third column + + result[keyword] = { + "read_channel": read_channel, + "write_channel": write_channel, + "description": full_description, + "units": { + "base": units, + "formatted": units + } + } + + i += 1 + + # Write to output file if specified + if output_file: + with open(output_file, 'w') as f: + json.dump(result, f, indent=2) + print(f"Wrote {len(result)} entries to {output_file}") + + return result + + +def main(): + """Main entry point for the parser.""" + import argparse + + parser = argparse.ArgumentParser( + description='Convert cake config file to JSON format' + ) + parser.add_argument( + 'input_file', + help='Path to the input cake config file' + ) + parser.add_argument( + '-o', '--output', + help='Path to the output JSON file (default: .json)', + default=None + ) + + args = parser.parse_args() + + # Determine output file name + if args.output is None: + input_path = Path(args.input_file) + output_file = input_path.with_suffix('.json') + else: + output_file = args.output + + # Parse and convert + result = parse_cake_config(args.input_file, output_file) + + # Print summary + print(f"\nParsed {len(result)} keywords") + print("\nFirst few entries:") + for i, (key, value) in enumerate(result.items()): + if i >= 3: + break + print(f" {key}: {value}") + + +if __name__ == '__main__': + main() diff --git a/sbin/mkd b/sbin/mkd index 957952d8..dbbc45d4 100755 --- a/sbin/mkd +++ b/sbin/mkd @@ -85,7 +85,7 @@ def load_configuration(store, alias, filename): contents = open(filename, 'r').read() items = mktl.json.loads(contents) - mktl.config.save(store, items, alias) + mktl.config.authoritative(store, alias, items) diff --git a/tests/mktl b/tests/mktl new file mode 120000 index 00000000..e33b42da --- /dev/null +++ b/tests/mktl @@ -0,0 +1 @@ +../src/mktl \ No newline at end of file From f1920b629542093c1170154506493ee7f0aa001d Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Mon, 15 Dec 2025 11:26:10 -1000 Subject: [PATCH 02/14] removing binary conversion function. config parser includes type and enumerations and mask enumerations --- examples/EPICS.py | 13 +- examples/fakedcs_cake_config.json | 249 ++++++++++++++++++++++++------ examples/parse_cake_config.py | 75 +++++++-- 3 files changed, 263 insertions(+), 74 deletions(-) diff --git a/examples/EPICS.py b/examples/EPICS.py index 3c2b84ee..8cca834b 100644 --- a/examples/EPICS.py +++ b/examples/EPICS.py @@ -57,14 +57,6 @@ def __init__(self, *args, **kwargs): self.publish_on_set = False - @classmethod - def convert_string_to_binary(cls, value, bits='08b'): - """ Convert a string to 8 bit binary value - """ - return ''.join(f'{byte:08b}' for byte in value.encode('utf-8')) - # return int(''.join(format(ord(char), bits) for char in value), 2) - - def publish_broadcast(self, ): """ This method is registered as a KTL callback; take any/all KTL broadcast events and publish them as mKTL events. @@ -89,8 +81,7 @@ def perform_get(self): resp = pv.get_with_metadata(as_string=True) # get the value and metadata timestamp = resp.get('timestamp') value = resp.get('value') - bvalue = self.convert_string_to_binary(value) - payload = mktl.Payload(bvalue, timestamp) + payload = mktl.Payload(value, timestamp) return payload @@ -102,7 +93,7 @@ def perform_set(self, new_value): to ensure they are interpreted (or not interpreted, as the case may be) properly. """ - pv = epics.PV(self.key) + pv = epics.PV(self.pvname) pv.put(new_value, wait=True) # end of class Item diff --git a/examples/fakedcs_cake_config.json b/examples/fakedcs_cake_config.json index f7b60388..724ca7fe 100644 --- a/examples/fakedcs_cake_config.json +++ b/examples/fakedcs_cake_config.json @@ -6,6 +6,11 @@ "units": { "base": "", "formatted": "" + }, + "type": "boolean", + "enumerators": { + "0": "no", + "1": "yes" } }, "autgo": { @@ -15,7 +20,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "string" }, "autpause": { "read_channel": "AUTPAUSE", @@ -24,7 +30,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "integer" }, "autresum": { "read_channel": "AUTRESUM", @@ -33,7 +40,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "integer" }, "autresx": { "read_channel": "AUTRESX", @@ -42,7 +50,8 @@ "units": { "base": "arcsec", "formatted": "arcsec" - } + }, + "type": "double" }, "autresy": { "read_channel": "AUTRESY", @@ -51,7 +60,8 @@ "units": { "base": "arcsec", "formatted": "arcsec" - } + }, + "type": "double" }, "autsqnum": { "read_channel": "AUTSQNUM", @@ -60,7 +70,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "integer" }, "autstop": { "read_channel": "AUTSTOP", @@ -69,7 +80,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "integer" }, "autxcent": { "read_channel": "AUTXCENT", @@ -78,7 +90,8 @@ "units": { "base": "arcsec", "formatted": "arcsec" - } + }, + "type": "double" }, "autycent": { "read_channel": "AUTYCENT", @@ -87,7 +100,8 @@ "units": { "base": "arcsec", "formatted": "arcsec" - } + }, + "type": "double" }, "axestat": { "read_channel": "AXESTAT", @@ -96,6 +110,17 @@ "units": { "base": "", "formatted": "" + }, + "type": "enumerated", + "enumerators": { + "0": "unknown", + "1": "not controlling", + "2": "halted", + "4": "in position", + "8": "in limit", + "16": "slewing", + "32": "acquiring", + "64": "tracking" } }, "az": { @@ -105,7 +130,8 @@ "units": { "base": "deg", "formatted": "deg" - } + }, + "type": "double" }, "currinst": { "read_channel": "CURRINST", @@ -114,7 +140,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "string" }, "el": { "read_channel": "EL", @@ -123,7 +150,18 @@ "units": { "base": "deg", "formatted": "deg" - } + }, + "type": "double" + }, + "instrume": { + "read_channel": "CURRINST", + "write_channel": "CURRINST", + "description": "instrument-name in \"\"", + "units": { + "base": "", + "formatted": "" + }, + "type": "string" }, "parang": { "read_channel": "PARANG", @@ -132,7 +170,8 @@ "units": { "base": "deg", "formatted": "deg" - } + }, + "type": "double" }, "rotbase": { "read_channel": "ROTBASE", @@ -141,7 +180,8 @@ "units": { "base": "deg", "formatted": "deg" - } + }, + "type": "double" }, "rotdest": { "read_channel": "ROTDEST", @@ -150,7 +190,8 @@ "units": { "base": "deg", "formatted": "deg" - } + }, + "type": "double" }, "rotdets1": { "read_channel": "ROTDETS1", @@ -159,7 +200,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "double" }, "rotdets2": { "read_channel": "ROTDETS2", @@ -168,7 +210,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "double" }, "roterrs": { "read_channel": "ROTERRS", @@ -177,7 +220,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "string" }, "rotervl": { "read_channel": "ROTERVL", @@ -186,7 +230,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "integer" }, "rothalt": { "read_channel": "ROTHALT", @@ -195,7 +240,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "rothaltc": { "read_channel": "ROTHALTC", @@ -204,7 +250,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "rotinit": { "read_channel": "ROTINIT", @@ -213,7 +260,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "rotinitc": { "read_channel": "ROTINITC", @@ -222,7 +270,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "rotmode": { "read_channel": "ROTMODE", @@ -231,6 +280,13 @@ "units": { "base": "", "formatted": "" + }, + "type": "enumerated", + "enumerators": { + "0": "unknown", + "1": "position angle", + "2": "vertical angle", + "4": "stationary" } }, "rotnews": { @@ -240,7 +296,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "integer" }, "rotpdest": { "read_channel": "ROTPDEST", @@ -249,7 +306,8 @@ "units": { "base": "deg", "formatted": "deg" - } + }, + "type": "double" }, "rotpdsts": { "read_channel": "ROTPDSTS", @@ -258,7 +316,8 @@ "units": { "base": "deg", "formatted": "deg" - } + }, + "type": "double" }, "rotposn": { "read_channel": "ROTPOSN", @@ -267,7 +326,8 @@ "units": { "base": "deg", "formatted": "deg" - } + }, + "type": "double" }, "rotpots1": { "read_channel": "ROTPOTS1", @@ -276,7 +336,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "double" }, "rotpots2": { "read_channel": "ROTPOTS2", @@ -285,7 +346,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "double" }, "rotpposn": { "read_channel": "ROTPPOSN", @@ -294,7 +356,8 @@ "units": { "base": "deg", "formatted": "deg" - } + }, + "type": "double" }, "rotsel": { "read_channel": "ROTSEL", @@ -303,6 +366,14 @@ "units": { "base": "", "formatted": "" + }, + "type": "enumerated", + "enumerators": { + "0": "none", + "1": "fcass (forward cass)", + "2": "cass (cassegrain)", + "3": "lbc (left bent cass)", + "4": "rbc (right bent cass)" } }, "rotspeed": { @@ -312,7 +383,8 @@ "units": { "base": "deg/sec", "formatted": "deg/sec" - } + }, + "type": "double" }, "rotsrver": { "read_channel": "ROTSRVER", @@ -321,7 +393,8 @@ "units": { "base": "arcsec", "formatted": "arcsec" - } + }, + "type": "double" }, "rotstat": { "read_channel": "ROTSTAT", @@ -330,6 +403,22 @@ "units": { "base": "", "formatted": "" + }, + "type": "enumerated", + "enumerators": { + "0": "unknown", + "1": "off", + "2": "initializing", + "4": "standby", + "8": "tracking", + "16": "fault", + "32": "manual", + "64": "halted", + "128": "disabled", + "256": "in position", + "512": "slewing", + "1024": "acquiring", + "2048": "in limit" } }, "rotstby": { @@ -339,7 +428,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "rotstbyc": { "read_channel": "ROTSTBYC", @@ -348,7 +438,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "rotstst": { "read_channel": "ROTSTST", @@ -357,7 +448,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "string" }, "rottime": { "read_channel": "ROTTIME", @@ -366,7 +458,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "double" }, "simulate": { "read_channel": "SIMULATE", @@ -375,6 +468,11 @@ "units": { "base": "", "formatted": "" + }, + "type": "boolean", + "enumerators": { + "0": "false", + "1": "true" } }, "terterrs": { @@ -384,7 +482,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "string" }, "tertervl": { "read_channel": "TERTERVL", @@ -393,7 +492,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "integer" }, "terthalt": { "read_channel": "TERTHALT", @@ -402,7 +502,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "terthaltc": { "read_channel": "TERTHALTC", @@ -411,7 +512,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "tertinit": { "read_channel": "TERTINIT", @@ -420,7 +522,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "tertinitc": { "read_channel": "TERTINITC", @@ -429,7 +532,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "tertmove": { "read_channel": "TERTMOVE", @@ -438,7 +542,8 @@ "units": { "base": "no,yes", "formatted": "no,yes" - } + }, + "type": "boolean" }, "tertmovec": { "read_channel": "TERTMOVEC", @@ -447,7 +552,8 @@ "units": { "base": "no,yes", "formatted": "no,yes" - } + }, + "type": "boolean" }, "tertdest": { "read_channel": "TERTDEST", @@ -456,6 +562,18 @@ "units": { "base": "", "formatted": "" + }, + "type": "enumerated", + "enumerators": { + "0": "lnas", + "1": "lbc1", + "2": "lbc2", + "3": "stowed", + "4": "rbc2", + "5": "rbc1", + "6": "rnas", + "7": "cass", + "8": "mirrorup" } }, "tertposn": { @@ -465,6 +583,19 @@ "units": { "base": "", "formatted": "" + }, + "type": "enumerated", + "enumerators": { + "0": "lnas", + "1": "lbc1", + "2": "lbc2", + "3": "stowed", + "4": "rbc2", + "5": "rbc1", + "6": "rnas", + "7": "unknown", + "8": "cass", + "9": "mirrorup" } }, "tertstat": { @@ -474,6 +605,22 @@ "units": { "base": "", "formatted": "" + }, + "type": "enumerated", + "enumerators": { + "0": "unknown", + "1": "off", + "2": "initializing", + "4": "standby", + "8": "tracking", + "16": "fault", + "32": "manual", + "64": "halted", + "128": "disabled", + "256": "in position", + "512": "slewing", + "1024": "acquiring", + "2048": "in limit" } }, "tertstby": { @@ -483,7 +630,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "tertstbyc": { "read_channel": "TERTSTBYC", @@ -492,7 +640,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "boolean" }, "tertstst": { "read_channel": "TERTSTST", @@ -501,7 +650,8 @@ "units": { "base": "", "formatted": "" - } + }, + "type": "string" }, "utc": { "read_channel": "UTC", @@ -510,6 +660,7 @@ "units": { "base": "h", "formatted": "h" - } + }, + "type": "double" } -} +} \ No newline at end of file diff --git a/examples/parse_cake_config.py b/examples/parse_cake_config.py index 4aee98b0..f23fbed7 100644 --- a/examples/parse_cake_config.py +++ b/examples/parse_cake_config.py @@ -65,23 +65,13 @@ def parse_cake_config(input_file, output_file=None): # Join all description parts full_description = ' '.join(description_parts) - # Extract units from the description (after "in") - units = "" - if ' in ' in full_description: - units_part = full_description.split(' in ', 1)[1] - # Remove curly braces and their contents if present - units = re.sub(r'\{[^}]*\}', '', units_part).strip() - # If units is empty or just quotes, set to empty string - if units in ['""', '']: - units = "" - - # Move to the mapping line + # Move to the mapping line first to check if it's ENMM type i = j if i < len(lines): mapping_line = lines[i].strip() # Parse the mapping line to extract the three channel names - # Format: KEYWORD READ_CHANNEL WRITE_CHANNEL [other fields...] + # Format: KEYWORD READ_CHANNEL WRITE_CHANNEL COL4 COL5 TYPE ... parts = mapping_line.split() if len(parts) >= 3: @@ -89,15 +79,72 @@ def parse_cake_config(input_file, output_file=None): read_channel = parts[1] # Second column write_channel = parts[2] # Third column - result[keyword] = { + # Check if column 4 (index 3) is ENMM to determine bitmask enumeration + is_bitmask = len(parts) >= 4 and parts[3] == 'ENMM' + + # Extract enumerators if present (inside curly braces) + enumerators = {} + enum_match = re.search(r'\{([^}]+)\}', full_description) + if enum_match: + enum_content = enum_match.group(1) + # Split by comma and clean up whitespace + enum_values = [v.strip() for v in enum_content.split(',')] + + if is_bitmask: + # For ENMM: use powers of 2 (0, 1, 2, 4, 8, 16, ...) + # First value is 0, second is 1, then powers of 2 + for idx, val in enumerate(enum_values): + if idx == 0: + key = "0" + elif idx == 1: + key = "1" + else: + key = str(2 ** (idx - 1)) + enumerators[key] = val + else: + # For ENM: use sequential indices (0, 1, 2, 3, ...) + enumerators = {str(i): val for i, val in enumerate(enum_values)} + + # Extract units from the description (after "in") + units = "" + if ' in ' in full_description: + units_part = full_description.split(' in ', 1)[1] + # Remove curly braces and their contents if present + units = re.sub(r'\{[^}]*\}', '', units_part).strip() + # If units is empty or just quotes, set to empty string + if units in ['""', '']: + units = "" + + # Extract type from 6th column (index 5) if available + type_code = parts[5] if len(parts) >= 6 else "" + + # Map type codes to type names + type_map = { + 'b': 'boolean', + 'i': 'integer', + 'd': 'double', + 'em': 'enumerated', + 'e': 'enumerated', + 's': 'string' + } + data_type = type_map.get(type_code, type_code) + + entry = { "read_channel": read_channel, "write_channel": write_channel, "description": full_description, "units": { "base": units, "formatted": units - } + }, + "type": data_type } + + # Add enumerators only if they exist + if enumerators: + entry["enumerators"] = enumerators + + result[keyword] = entry i += 1 From 1169f16c3e31365a2e7d2201c8b6fb67d7e1de25 Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Tue, 16 Dec 2025 11:09:43 -1000 Subject: [PATCH 03/14] publish broadcast now works --- examples/EPICS.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/examples/EPICS.py b/examples/EPICS.py index 8cca834b..a7ed5e18 100644 --- a/examples/EPICS.py +++ b/examples/EPICS.py @@ -36,7 +36,14 @@ def setup_final(self): """ This is the last step before broadcasts go out. This is the right time to fire up monitoring of all EPICS PVs. """ - pass + config = self.config[self.uuid] + items = config['items'] + for key in items.keys(): + item = self.store[key] + if isinstance(item, Item): + pvname = config['store'] + ':' + key.upper() + pv = epics.PV(pvname) + pv.add_callback(item.publish_broadcast) # end of class Store @@ -57,17 +64,29 @@ def __init__(self, *args, **kwargs): self.publish_on_set = False - def publish_broadcast(self, ): - """ This method is registered as a KTL callback; take any/all KTL + def publish_broadcast(self, *args, **kwargs): + """ This method is registered as an EPICS callback; take any/all EPICS broadcast events and publish them as mKTL events. """ + try: + timestamp = kwargs['timestamp'] + value = kwargs['value'] + except KeyError: + return + self.publish(value, timestamp) + + def _get_pv_with_metadata(self): + """ Return the EPICS PV object associated with this item. + """ pv = epics.PV(self.pvname) - slice = pv.get_with_metadata(as_string=True) - timestamp = slice.get('timestamp') - value = slice.get('value') - bvalue = self.convert_string_to_binary(value) - - self.publish(bvalue, timestamp) + resp = None + tries = 0 + while resp is None: # try up to 5 times to get a valid response + resp = pv.get_with_metadata(as_string=True) # get the value and metadata + tries += 1 + if tries >= 5: + raise RuntimeError(f"Could not get metadata for PV {self.pvname}") + return resp def perform_get(self): @@ -77,8 +96,7 @@ def perform_get(self): relies on epics callbacks to receive asynchronous broadcasts (see :func:`publish_broadcast`). """ - pv = epics.PV(self.pvname) - resp = pv.get_with_metadata(as_string=True) # get the value and metadata + resp = self._get_pv_with_metadata() timestamp = resp.get('timestamp') value = resp.get('value') payload = mktl.Payload(value, timestamp) From 07224f70a6d7f74cd3808900b1b7b298c457d0a8 Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Mon, 5 Jan 2026 13:47:11 -1000 Subject: [PATCH 04/14] remove non-existant channel instrume --- examples/EPICS.py | 3 +-- examples/fakedcs_cake_config.json | 10 ---------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/examples/EPICS.py b/examples/EPICS.py index a7ed5e18..1f982466 100644 --- a/examples/EPICS.py +++ b/examples/EPICS.py @@ -12,7 +12,7 @@ class Daemon(mktl.Daemon): def __init__(self, store, alias=None, *args, **kwargs): - # Generate the configuration matching this KTL service. Since this + # Generate the configuration matching this set of EPICS channels. Since this # configuration is not in the default location it must be declared # prior to initializing the Daemon. # store is the mktl store name for the epics object (i.e. k1:mySystem:myDevice) @@ -61,7 +61,6 @@ def __init__(self, *args, **kwargs): self.pvname = self.full_key.replace('.', ':') service = "".join(self.full_key.split('.')[:-1]) self.pvname = service + ':' + self.key.upper() - self.publish_on_set = False def publish_broadcast(self, *args, **kwargs): diff --git a/examples/fakedcs_cake_config.json b/examples/fakedcs_cake_config.json index 724ca7fe..fd78166f 100644 --- a/examples/fakedcs_cake_config.json +++ b/examples/fakedcs_cake_config.json @@ -153,16 +153,6 @@ }, "type": "double" }, - "instrume": { - "read_channel": "CURRINST", - "write_channel": "CURRINST", - "description": "instrument-name in \"\"", - "units": { - "base": "", - "formatted": "" - }, - "type": "string" - }, "parang": { "read_channel": "PARANG", "write_channel": "PARANG", From 067d4260b639c3c2e92ce3cbe4140399e3355e28 Mon Sep 17 00:00:00 2001 From: tylertucker202 Date: Tue, 13 Jan 2026 09:55:54 -1000 Subject: [PATCH 05/14] unformatted values now "" --- examples/fakedcs_cake_config.json | 116 +++++++++++++++--------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/examples/fakedcs_cake_config.json b/examples/fakedcs_cake_config.json index fd78166f..70c03fa5 100644 --- a/examples/fakedcs_cake_config.json +++ b/examples/fakedcs_cake_config.json @@ -4,7 +4,7 @@ "write_channel": "AUTACTIV", "description": "guider-active in {no,yes}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean", @@ -18,7 +18,7 @@ "write_channel": "AUTGO", "description": "guider-go-flag in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -28,7 +28,7 @@ "write_channel": "AUTPAUSE", "description": "guider-pause in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -38,7 +38,7 @@ "write_channel": "AUTRESUM", "description": "guider-resume in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -48,7 +48,7 @@ "write_channel": "AUTRESX", "description": "guider-resumeX in arcsec", "units": { - "base": "arcsec", + "": "arcsec", "formatted": "arcsec" }, "type": "double" @@ -58,7 +58,7 @@ "write_channel": "AUTRESY", "description": "guider-resumeY in arcsec", "units": { - "base": "arcsec", + "": "arcsec", "formatted": "arcsec" }, "type": "double" @@ -68,7 +68,7 @@ "write_channel": "AUTSQNUM", "description": "guider-seqnum in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -78,7 +78,7 @@ "write_channel": "AUTSTOP", "description": "guider-stop in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -88,7 +88,7 @@ "write_channel": "AUTXCENT", "description": "guider-Xcent in arcsec", "units": { - "base": "arcsec", + "": "arcsec", "formatted": "arcsec" }, "type": "double" @@ -98,7 +98,7 @@ "write_channel": "AUTYCENT", "description": "guider-Ycent in arcsec", "units": { - "base": "arcsec", + "": "arcsec", "formatted": "arcsec" }, "type": "double" @@ -108,7 +108,7 @@ "write_channel": "AXESTAT", "description": "axes-control-status in {unknown, not controlling, halted, in position, in limit, slewing, acquiring, tracking}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -128,7 +128,7 @@ "write_channel": "AZ", "description": "telescope-azimuth in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -138,7 +138,7 @@ "write_channel": "CURRINST", "description": "current-instrument in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -148,7 +148,7 @@ "write_channel": "EL", "description": "telescope-elevation in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -158,7 +158,7 @@ "write_channel": "PARANG", "description": "parallactic-angle-astrometric in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -168,7 +168,7 @@ "write_channel": "ROTBASE", "description": "rotator-base-angle in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -178,7 +178,7 @@ "write_channel": "ROTDEST", "description": "rotator-user-destination in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -188,7 +188,7 @@ "write_channel": "ROTDETS1", "description": "rotator-destination-timestamp1-ut1-slow in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "double" @@ -198,7 +198,7 @@ "write_channel": "ROTDETS2", "description": "rotator-destination-timestamp2-ut1-slow in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "double" @@ -208,7 +208,7 @@ "write_channel": "ROTERRS", "description": "rotator-error-string in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -218,7 +218,7 @@ "write_channel": "ROTERVL", "description": "rotator-error-status in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -228,7 +228,7 @@ "write_channel": "ROTHALTC", "description": "rotator-halt-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -238,7 +238,7 @@ "write_channel": "ROTHALT", "description": "rotator-halt-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -248,7 +248,7 @@ "write_channel": "ROTINITC", "description": "rotator-initialization-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -258,7 +258,7 @@ "write_channel": "ROTINIT", "description": "rotator-initialization-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -268,7 +268,7 @@ "write_channel": "ROTMODE", "description": "rotator-tracking-mode in {unknown, position angle, vertical angle, stationary}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -284,7 +284,7 @@ "write_channel": "ROTNEWS", "description": "rotator-new-destination-slow in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -294,7 +294,7 @@ "write_channel": "ROTPDEST", "description": "rotator-physical-destination in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -304,7 +304,7 @@ "write_channel": "ROTPDSTS", "description": "rotator-physical-destination-slow in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -314,7 +314,7 @@ "write_channel": "ROTPOSN", "description": "rotator-user-position in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -324,7 +324,7 @@ "write_channel": "ROTPOTS1", "description": "rotator-position-timestamp1-ut1-slow in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "double" @@ -334,7 +334,7 @@ "write_channel": "ROTPOTS2", "description": "rotator-position-timestamp2-ut1-slow in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "double" @@ -344,7 +344,7 @@ "write_channel": "ROTPPOSN", "description": "rotator-physical-position in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -354,7 +354,7 @@ "write_channel": "ROTSEL", "description": "rotator-select in {none, fcass (forward cass), cass (cassegrain), lbc (left bent cass), rbc (right bent cass)}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -371,7 +371,7 @@ "write_channel": "ROTSPEED", "description": "rotator-fake-speed in deg/sec", "units": { - "base": "deg/sec", + "": "deg/sec", "formatted": "deg/sec" }, "type": "double" @@ -381,7 +381,7 @@ "write_channel": "ROTSRVER", "description": "rotator-servo-error in arcsec", "units": { - "base": "arcsec", + "": "arcsec", "formatted": "arcsec" }, "type": "double" @@ -391,7 +391,7 @@ "write_channel": "ROTSTAT", "description": "rotator-state in {unknown, off, initializing, standby, tracking, fault, manual, halted, disabled, in position, slewing, acquiring, in limit}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -416,7 +416,7 @@ "write_channel": "ROTSTBYC", "description": "rotator-standby-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -426,7 +426,7 @@ "write_channel": "ROTSTBY", "description": "rotator-standby-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -436,7 +436,7 @@ "write_channel": "ROTSTST", "description": "rotator-state-string in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -446,7 +446,7 @@ "write_channel": "ROTTIME", "description": "rotator-current-time in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "double" @@ -456,7 +456,7 @@ "write_channel": "SIMULATE", "description": "simulating-dcs in {false, true}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean", @@ -470,7 +470,7 @@ "write_channel": "TERTERRS", "description": "tertiary-error-string in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -480,7 +480,7 @@ "write_channel": "TERTERRS", "description": "tertiary-error-status in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -490,7 +490,7 @@ "write_channel": "TERTHALTC", "description": "tertiary-halt-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -500,7 +500,7 @@ "write_channel": "TERTHALT", "description": "tertiary-halt-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -510,7 +510,7 @@ "write_channel": "TERTINITC", "description": "tertiary-initialization-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -520,7 +520,7 @@ "write_channel": "TERTINIT", "description": "tertiary-initialization-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -530,7 +530,7 @@ "write_channel": "TERTMOVEC", "description": "move-tertiary in no,yes", "units": { - "base": "no,yes", + "": "no,yes", "formatted": "no,yes" }, "type": "boolean" @@ -540,7 +540,7 @@ "write_channel": "TERTMOVE", "description": "move-tertiary-complement in no,yes", "units": { - "base": "no,yes", + "": "no,yes", "formatted": "no,yes" }, "type": "boolean" @@ -550,7 +550,7 @@ "write_channel": "TERTDEST", "description": "tertiary-destination in {lnas, lbc1, lbc2, stowed, rbc2, rbc1, rnas, cass, mirrorup}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -571,7 +571,7 @@ "write_channel": "TERTPOSN", "description": "tertiary-position in {lnas, lbc1, lbc2, stowed, rbc2, rbc1, rnas, unknown, cass, mirrorup}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -593,7 +593,7 @@ "write_channel": "TERTSTAT", "description": "tertiary-state in {unknown, off, initializing, standby, tracking, fault, manual, halted, disabled, in position, slewing, acquiring, in limit}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -618,7 +618,7 @@ "write_channel": "TERTSTBYC", "description": "tertiary-standby-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -628,7 +628,7 @@ "write_channel": "TERTSTBY", "description": "tertiary-standby-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -638,7 +638,7 @@ "write_channel": "TERTSTST", "description": "tertiary-state-string in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -648,9 +648,9 @@ "write_channel": "UTC", "description": "coordinated-universal-time in h", "units": { - "base": "h", + "": "h", "formatted": "h" }, "type": "double" } -} \ No newline at end of file +} From b2dc310e9f0c1c75146b78eb2a9f3608692617e5 Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Tue, 13 Jan 2026 13:04:10 -1000 Subject: [PATCH 06/14] base is now blank --- examples/fakedcs_cake_config.json | 114 +++++++++++++++--------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/examples/fakedcs_cake_config.json b/examples/fakedcs_cake_config.json index fd78166f..d832841f 100644 --- a/examples/fakedcs_cake_config.json +++ b/examples/fakedcs_cake_config.json @@ -4,7 +4,7 @@ "write_channel": "AUTACTIV", "description": "guider-active in {no,yes}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean", @@ -18,7 +18,7 @@ "write_channel": "AUTGO", "description": "guider-go-flag in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -28,7 +28,7 @@ "write_channel": "AUTPAUSE", "description": "guider-pause in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -38,7 +38,7 @@ "write_channel": "AUTRESUM", "description": "guider-resume in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -48,7 +48,7 @@ "write_channel": "AUTRESX", "description": "guider-resumeX in arcsec", "units": { - "base": "arcsec", + "": "arcsec", "formatted": "arcsec" }, "type": "double" @@ -58,7 +58,7 @@ "write_channel": "AUTRESY", "description": "guider-resumeY in arcsec", "units": { - "base": "arcsec", + "": "arcsec", "formatted": "arcsec" }, "type": "double" @@ -68,7 +68,7 @@ "write_channel": "AUTSQNUM", "description": "guider-seqnum in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -78,7 +78,7 @@ "write_channel": "AUTSTOP", "description": "guider-stop in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -88,7 +88,7 @@ "write_channel": "AUTXCENT", "description": "guider-Xcent in arcsec", "units": { - "base": "arcsec", + "": "arcsec", "formatted": "arcsec" }, "type": "double" @@ -98,7 +98,7 @@ "write_channel": "AUTYCENT", "description": "guider-Ycent in arcsec", "units": { - "base": "arcsec", + "": "arcsec", "formatted": "arcsec" }, "type": "double" @@ -108,7 +108,7 @@ "write_channel": "AXESTAT", "description": "axes-control-status in {unknown, not controlling, halted, in position, in limit, slewing, acquiring, tracking}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -128,7 +128,7 @@ "write_channel": "AZ", "description": "telescope-azimuth in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -138,7 +138,7 @@ "write_channel": "CURRINST", "description": "current-instrument in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -148,7 +148,7 @@ "write_channel": "EL", "description": "telescope-elevation in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -158,7 +158,7 @@ "write_channel": "PARANG", "description": "parallactic-angle-astrometric in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -168,7 +168,7 @@ "write_channel": "ROTBASE", "description": "rotator-base-angle in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -178,7 +178,7 @@ "write_channel": "ROTDEST", "description": "rotator-user-destination in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -188,7 +188,7 @@ "write_channel": "ROTDETS1", "description": "rotator-destination-timestamp1-ut1-slow in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "double" @@ -198,7 +198,7 @@ "write_channel": "ROTDETS2", "description": "rotator-destination-timestamp2-ut1-slow in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "double" @@ -208,7 +208,7 @@ "write_channel": "ROTERRS", "description": "rotator-error-string in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -218,7 +218,7 @@ "write_channel": "ROTERVL", "description": "rotator-error-status in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -228,7 +228,7 @@ "write_channel": "ROTHALTC", "description": "rotator-halt-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -238,7 +238,7 @@ "write_channel": "ROTHALT", "description": "rotator-halt-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -248,7 +248,7 @@ "write_channel": "ROTINITC", "description": "rotator-initialization-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -258,7 +258,7 @@ "write_channel": "ROTINIT", "description": "rotator-initialization-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -268,7 +268,7 @@ "write_channel": "ROTMODE", "description": "rotator-tracking-mode in {unknown, position angle, vertical angle, stationary}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -284,7 +284,7 @@ "write_channel": "ROTNEWS", "description": "rotator-new-destination-slow in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -294,7 +294,7 @@ "write_channel": "ROTPDEST", "description": "rotator-physical-destination in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -304,7 +304,7 @@ "write_channel": "ROTPDSTS", "description": "rotator-physical-destination-slow in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -314,7 +314,7 @@ "write_channel": "ROTPOSN", "description": "rotator-user-position in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -324,7 +324,7 @@ "write_channel": "ROTPOTS1", "description": "rotator-position-timestamp1-ut1-slow in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "double" @@ -334,7 +334,7 @@ "write_channel": "ROTPOTS2", "description": "rotator-position-timestamp2-ut1-slow in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "double" @@ -344,7 +344,7 @@ "write_channel": "ROTPPOSN", "description": "rotator-physical-position in deg", "units": { - "base": "deg", + "": "deg", "formatted": "deg" }, "type": "double" @@ -354,7 +354,7 @@ "write_channel": "ROTSEL", "description": "rotator-select in {none, fcass (forward cass), cass (cassegrain), lbc (left bent cass), rbc (right bent cass)}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -371,7 +371,7 @@ "write_channel": "ROTSPEED", "description": "rotator-fake-speed in deg/sec", "units": { - "base": "deg/sec", + "": "deg/sec", "formatted": "deg/sec" }, "type": "double" @@ -381,7 +381,7 @@ "write_channel": "ROTSRVER", "description": "rotator-servo-error in arcsec", "units": { - "base": "arcsec", + "": "arcsec", "formatted": "arcsec" }, "type": "double" @@ -391,7 +391,7 @@ "write_channel": "ROTSTAT", "description": "rotator-state in {unknown, off, initializing, standby, tracking, fault, manual, halted, disabled, in position, slewing, acquiring, in limit}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -416,7 +416,7 @@ "write_channel": "ROTSTBYC", "description": "rotator-standby-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -426,7 +426,7 @@ "write_channel": "ROTSTBY", "description": "rotator-standby-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -436,7 +436,7 @@ "write_channel": "ROTSTST", "description": "rotator-state-string in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -446,7 +446,7 @@ "write_channel": "ROTTIME", "description": "rotator-current-time in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "double" @@ -456,7 +456,7 @@ "write_channel": "SIMULATE", "description": "simulating-dcs in {false, true}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean", @@ -470,7 +470,7 @@ "write_channel": "TERTERRS", "description": "tertiary-error-string in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -480,7 +480,7 @@ "write_channel": "TERTERRS", "description": "tertiary-error-status in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "integer" @@ -490,7 +490,7 @@ "write_channel": "TERTHALTC", "description": "tertiary-halt-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -500,7 +500,7 @@ "write_channel": "TERTHALT", "description": "tertiary-halt-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -510,7 +510,7 @@ "write_channel": "TERTINITC", "description": "tertiary-initialization-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -520,7 +520,7 @@ "write_channel": "TERTINIT", "description": "tertiary-initialization-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -530,7 +530,7 @@ "write_channel": "TERTMOVEC", "description": "move-tertiary in no,yes", "units": { - "base": "no,yes", + "": "no,yes", "formatted": "no,yes" }, "type": "boolean" @@ -540,7 +540,7 @@ "write_channel": "TERTMOVE", "description": "move-tertiary-complement in no,yes", "units": { - "base": "no,yes", + "": "no,yes", "formatted": "no,yes" }, "type": "boolean" @@ -550,7 +550,7 @@ "write_channel": "TERTDEST", "description": "tertiary-destination in {lnas, lbc1, lbc2, stowed, rbc2, rbc1, rnas, cass, mirrorup}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -571,7 +571,7 @@ "write_channel": "TERTPOSN", "description": "tertiary-position in {lnas, lbc1, lbc2, stowed, rbc2, rbc1, rnas, unknown, cass, mirrorup}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -593,7 +593,7 @@ "write_channel": "TERTSTAT", "description": "tertiary-state in {unknown, off, initializing, standby, tracking, fault, manual, halted, disabled, in position, slewing, acquiring, in limit}", "units": { - "base": "", + "": "", "formatted": "" }, "type": "enumerated", @@ -618,7 +618,7 @@ "write_channel": "TERTSTBYC", "description": "tertiary-standby-command in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -628,7 +628,7 @@ "write_channel": "TERTSTBY", "description": "tertiary-standby-command-complement in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "boolean" @@ -638,7 +638,7 @@ "write_channel": "TERTSTST", "description": "tertiary-state-string in \"\"", "units": { - "base": "", + "": "", "formatted": "" }, "type": "string" @@ -648,7 +648,7 @@ "write_channel": "UTC", "description": "coordinated-universal-time in h", "units": { - "base": "h", + "": "h", "formatted": "h" }, "type": "double" From c1bb1d589a1ea227ef6ae50565f40ca6cc2ceaf2 Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Tue, 13 Jan 2026 13:34:12 -1000 Subject: [PATCH 07/14] let key case match what is in config --- examples/EPICS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/EPICS.py b/examples/EPICS.py index 1f982466..1d6dfe45 100644 --- a/examples/EPICS.py +++ b/examples/EPICS.py @@ -41,7 +41,7 @@ def setup_final(self): for key in items.keys(): item = self.store[key] if isinstance(item, Item): - pvname = config['store'] + ':' + key.upper() + pvname = config['store'] + ':' + key pv = epics.PV(pvname) pv.add_callback(item.publish_broadcast) From 8f2a7564b0a8c880dc19a517286ea3c04adf131e Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Tue, 13 Jan 2026 13:35:18 -1000 Subject: [PATCH 08/14] let key case match what is in config --- examples/EPICS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/EPICS.py b/examples/EPICS.py index 1d6dfe45..deb906e0 100644 --- a/examples/EPICS.py +++ b/examples/EPICS.py @@ -60,7 +60,7 @@ def __init__(self, *args, **kwargs): # completes successfully; this attribute inhibits that behavior. self.pvname = self.full_key.replace('.', ':') service = "".join(self.full_key.split('.')[:-1]) - self.pvname = service + ':' + self.key.upper() + self.pvname = service + ':' + self.key self.publish_on_set = False def publish_broadcast(self, *args, **kwargs): From 693fa9ca0b6630659c636170cd45e1e981f46280 Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Tue, 13 Jan 2026 13:43:58 -1000 Subject: [PATCH 09/14] epics using read_channel to read pv --- examples/EPICS.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/examples/EPICS.py b/examples/EPICS.py index deb906e0..956ce5fe 100644 --- a/examples/EPICS.py +++ b/examples/EPICS.py @@ -41,7 +41,7 @@ def setup_final(self): for key in items.keys(): item = self.store[key] if isinstance(item, Item): - pvname = config['store'] + ':' + key + pvname = self.config[self.uuid]['items'][key]['read_channel'] pv = epics.PV(pvname) pv.add_callback(item.publish_broadcast) @@ -58,9 +58,6 @@ def __init__(self, *args, **kwargs): # We want the EPICS channels to be the sole source of broadcast # events. mKTL defaults to publishing a new value when a SET operation # completes successfully; this attribute inhibits that behavior. - self.pvname = self.full_key.replace('.', ':') - service = "".join(self.full_key.split('.')[:-1]) - self.pvname = service + ':' + self.key self.publish_on_set = False def publish_broadcast(self, *args, **kwargs): @@ -77,14 +74,14 @@ def publish_broadcast(self, *args, **kwargs): def _get_pv_with_metadata(self): """ Return the EPICS PV object associated with this item. """ - pv = epics.PV(self.pvname) + pv = epics.PV(self.config['read_channel']) resp = None tries = 0 while resp is None: # try up to 5 times to get a valid response resp = pv.get_with_metadata(as_string=True) # get the value and metadata tries += 1 if tries >= 5: - raise RuntimeError(f"Could not get metadata for PV {self.pvname}") + raise RuntimeError(f"Could not get metadata for PV {self.config['read_channel']}") return resp @@ -110,7 +107,7 @@ def perform_set(self, new_value): to ensure they are interpreted (or not interpreted, as the case may be) properly. """ - pv = epics.PV(self.pvname) + pv = epics.PV(self.config['write_channel']) pv.put(new_value, wait=True) # end of class Item @@ -181,7 +178,7 @@ def describePV(pv: epics.PV): for attribute in ('key', 'read_access', 'write_access'): try: if attribute == 'key': - value = pv.pvname + value = pv.config['read_channel'] else: value = getattr(pv, attribute) except ValueError: From 8daeac1e76202a53a080357fcc97826031cc4f67 Mon Sep 17 00:00:00 2001 From: tylertucker202 Date: Wed, 14 Jan 2026 14:04:43 -1000 Subject: [PATCH 10/14] post review: need to test --- examples/EPICS.py | 65 +++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/examples/EPICS.py b/examples/EPICS.py index 956ce5fe..5bbec65b 100644 --- a/examples/EPICS.py +++ b/examples/EPICS.py @@ -4,7 +4,7 @@ import mktl import epics -from itertools import product +import itertools import pdb @@ -24,9 +24,7 @@ def setup(self): instance for each and every channel being proxied by this daemon, as opposed to letting them be the default mktl.Item. """ - config = self.config[self.uuid] - items = config['items'] - keys = items.keys() + keys = self.config.keys(authoritative=True) for key in keys: self.add_item(Item, key) @@ -36,14 +34,14 @@ def setup_final(self): """ This is the last step before broadcasts go out. This is the right time to fire up monitoring of all EPICS PVs. """ - config = self.config[self.uuid] - items = config['items'] - for key in items.keys(): + keys = self.config.keys(authoritative=True) + for key in keys: + if key.startswith('_'): + continue item = self.store[key] - if isinstance(item, Item): - pvname = self.config[self.uuid]['items'][key]['read_channel'] - pv = epics.PV(pvname) - pv.add_callback(item.publish_broadcast) + pvname = self.config[key]['read_channel'] + pv = epics.PV(pvname) + pv.add_callback(item.publish_broadcast) # end of class Store @@ -63,13 +61,25 @@ def __init__(self, *args, **kwargs): def publish_broadcast(self, *args, **kwargs): """ This method is registered as an EPICS callback; take any/all EPICS broadcast events and publish them as mKTL events. + + TODO: add an epics callback example. See pyepics for example...etc. """ - try: - timestamp = kwargs['timestamp'] + try: value = kwargs['value'] except KeyError: return - self.publish(value, timestamp) + timestamp = self._get_timestamp(kwargs.get('timestamp')) + self.publish(value, timestamp) # Publish will pick up the timestamp value if it is None. + + def _get_timestamp(self, timestamp, minlim=915184800): + """TODO: ADD ME + """ + if timestamp is None: + return None + elif timestamp < minlim: # this is unix timestamp for 1999-01-01 + return None + else: + return timestamp def _get_pv_with_metadata(self): """ Return the EPICS PV object associated with this item. @@ -92,8 +102,9 @@ def perform_get(self): relies on epics callbacks to receive asynchronous broadcasts (see :func:`publish_broadcast`). """ + #TODO: check if readable from config. resp = self._get_pv_with_metadata() - timestamp = resp.get('timestamp') + timestamp = _get_timestamp(resp.get('timestamp')) value = resp.get('value') payload = mktl.Payload(value, timestamp) return payload @@ -107,7 +118,8 @@ def perform_set(self, new_value): to ensure they are interpreted (or not interpreted, as the case may be) properly. """ - pv = epics.PV(self.config['write_channel']) + #TODO: check if writable from config. + pv = epics.PV(self.config['write_channel']) #TODO: can we just have a channel? read/write is on anoyther layer pv.put(new_value, wait=True) # end of class Item @@ -126,11 +138,11 @@ def describePV(pv: epics.PV): Epics channel. """ - keyword_dict = dict() + channel_dict = dict() type = pv.type type = type_mapping[type.upper()] - keyword_dict['type'] = type + channel_dict['type'] = type enumerators = None try: @@ -164,7 +176,7 @@ def describePV(pv: epics.PV): if value is not None: if attribute == 'help': attribute = 'description' - keyword_dict[attribute] = value + channel_dict[attribute] = value # make range attribute try: @@ -173,7 +185,7 @@ def describePV(pv: epics.PV): except ValueError: pass else: - keyword_dict['range'] = {"minimum": lower, "maximum": upper} + channel_dict['range'] = {"minimum": lower, "maximum": upper} for attribute in ('key', 'read_access', 'write_access'): try: @@ -185,27 +197,26 @@ def describePV(pv: epics.PV): value = None if value is False: - keyword_dict[attribute] = value + channel_dict[attribute] = value if enumerators is not None: - keyword_dict['enumerators'] = enumerators + channel_dict['enumerators'] = enumerators - return keyword_dict + return channel_dict # Translate Epics data types to mKTL types. type_mapping = dict() epics_types = ['double', 'float', 'int', 'string', 'short', 'enum', 'char', 'long'] +numeric_types = set(('double', 'float', 'short', 'int', 'char', 'long')) epics_variants = ['', 'ctrl', 'time'] -for v, t in product(epics_variants, epics_types): +for v, t in intertools.product(epics_variants, epics_types): if v == '': epics_type = t.upper() else: epics_type = f'{v.upper()}_{t.upper()}' - if t in ['double', 'float']: - mktl_type = 'numeric' - elif t in ['int', 'char', 'long']: + if t in numeric_types: mktl_type = 'numeric' elif t == 'string': mktl_type = 'string' From ae26b963d997cdfc7fbf526db12137559fead25b Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Wed, 21 Jan 2026 16:00:23 -1000 Subject: [PATCH 11/14] post meeting changes. config file format changed --- examples/EPICS.py | 36 +- examples/fakedcs_cake_config | 3 - examples/fakedcs_cake_config.json | 529 +++++++++++++++++------------- examples/parse_cake_config.py | 319 ++++++++++++++++-- 4 files changed, 609 insertions(+), 278 deletions(-) diff --git a/examples/EPICS.py b/examples/EPICS.py index 5bbec65b..477de2cc 100644 --- a/examples/EPICS.py +++ b/examples/EPICS.py @@ -5,7 +5,6 @@ import mktl import epics import itertools -import pdb class Daemon(mktl.Daemon): @@ -36,10 +35,10 @@ def setup_final(self): """ keys = self.config.keys(authoritative=True) for key in keys: - if key.startswith('_'): + if key.startswith('_'): # may not need this anymore continue item = self.store[key] - pvname = self.config[key]['read_channel'] + pvname = self.config[key]['channel'] pv = epics.PV(pvname) pv.add_callback(item.publish_broadcast) @@ -62,7 +61,15 @@ def publish_broadcast(self, *args, **kwargs): """ This method is registered as an EPICS callback; take any/all EPICS broadcast events and publish them as mKTL events. - TODO: add an epics callback example. See pyepics for example...etc. + Epics callback functions are called with several keyword arguments. + you should always include **kwargs in your callback definition to + capture all of them. Here are some example callback arguments: + pvname : Name of the PV that triggered the callback + value : Current value of the PV + type: : the Python type for the data + units : string for PV units + See https://pyepics.github.io/pyepics/pv.html#user-supplied-callback-functions + for an exhaustive list of callback arguments. """ try: value = kwargs['value'] @@ -72,7 +79,8 @@ def publish_broadcast(self, *args, **kwargs): self.publish(value, timestamp) # Publish will pick up the timestamp value if it is None. def _get_timestamp(self, timestamp, minlim=915184800): - """TODO: ADD ME + """Check if there is a timestamp. If it is None or before 1999-01-01, + return None to let mKTL handle it. Otherwise return the timestamp. """ if timestamp is None: return None @@ -84,14 +92,14 @@ def _get_timestamp(self, timestamp, minlim=915184800): def _get_pv_with_metadata(self): """ Return the EPICS PV object associated with this item. """ - pv = epics.PV(self.config['read_channel']) + pv = epics.PV(self.config['channel']) resp = None tries = 0 while resp is None: # try up to 5 times to get a valid response resp = pv.get_with_metadata(as_string=True) # get the value and metadata tries += 1 if tries >= 5: - raise RuntimeError(f"Could not get metadata for PV {self.config['read_channel']}") + raise RuntimeError(f"Could not get metadata for PV {self.config['channel']}") return resp @@ -102,9 +110,10 @@ def perform_get(self): relies on epics callbacks to receive asynchronous broadcasts (see :func:`publish_broadcast`). """ - #TODO: check if readable from config. + if 'channel' not in self.config.keys(): + return None resp = self._get_pv_with_metadata() - timestamp = _get_timestamp(resp.get('timestamp')) + timestamp = self._get_timestamp(resp.get('timestamp')) value = resp.get('value') payload = mktl.Payload(value, timestamp) return payload @@ -118,8 +127,9 @@ def perform_set(self, new_value): to ensure they are interpreted (or not interpreted, as the case may be) properly. """ - #TODO: check if writable from config. - pv = epics.PV(self.config['write_channel']) #TODO: can we just have a channel? read/write is on anoyther layer + if not self.config.get('settable'): + return None + pv = epics.PV(self.config['channel']) pv.put(new_value, wait=True) # end of class Item @@ -190,7 +200,7 @@ def describePV(pv: epics.PV): for attribute in ('key', 'read_access', 'write_access'): try: if attribute == 'key': - value = pv.config['read_channel'] + value = pv.config['channel'] else: value = getattr(pv, attribute) except ValueError: @@ -211,7 +221,7 @@ def describePV(pv: epics.PV): epics_types = ['double', 'float', 'int', 'string', 'short', 'enum', 'char', 'long'] numeric_types = set(('double', 'float', 'short', 'int', 'char', 'long')) epics_variants = ['', 'ctrl', 'time'] -for v, t in intertools.product(epics_variants, epics_types): +for v, t in itertools.product(epics_variants, epics_types): if v == '': epics_type = t.upper() else: diff --git a/examples/fakedcs_cake_config b/examples/fakedcs_cake_config index 73d3ff0e..2bdf0270 100644 --- a/examples/fakedcs_cake_config +++ b/examples/fakedcs_cake_config @@ -65,9 +65,6 @@ simprefix = fakedcs: telescope-elevation in deg EL EL EL R2D2 D2R d 1 - instrument-name in "" - INSTRUME CURRINST CURRINST STR STR s 1 - parallactic-angle-astrometric in deg PARANG PARANG PARANG R2D2 D2R d 1 diff --git a/examples/fakedcs_cake_config.json b/examples/fakedcs_cake_config.json index 70c03fa5..a85bb460 100644 --- a/examples/fakedcs_cake_config.json +++ b/examples/fakedcs_cake_config.json @@ -1,10 +1,11 @@ { - "autactiv": { - "read_channel": "AUTACTIV", - "write_channel": "AUTACTIV", + "fakedcs:AUTACTIV": { + "channel": "fakedcs:AUTACTIV", + "gettable": true, + "settable": true, "description": "guider-active in {no,yes}", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean", @@ -13,102 +14,112 @@ "1": "yes" } }, - "autgo": { - "read_channel": "AUTGO", - "write_channel": "AUTGO", + "fakedcs:AUTGO": { + "channel": "fakedcs:AUTGO", + "gettable": true, + "settable": true, "description": "guider-go-flag in \"\"", "units": { - "": "", + "": "string", "formatted": "" }, "type": "string" }, - "autpause": { - "read_channel": "AUTPAUSE", - "write_channel": "AUTPAUSE", + "fakedcs:AUTPAUSE": { + "channel": "fakedcs:AUTPAUSE", + "gettable": true, + "settable": true, "description": "guider-pause in \"\"", "units": { - "": "", + "": "integer", "formatted": "" }, "type": "integer" }, - "autresum": { - "read_channel": "AUTRESUM", - "write_channel": "AUTRESUM", + "fakedcs:AUTRESUM": { + "channel": "fakedcs:AUTRESUM", + "gettable": true, + "settable": true, "description": "guider-resume in \"\"", "units": { - "": "", + "": "integer", "formatted": "" }, "type": "integer" }, - "autresx": { - "read_channel": "AUTRESX", - "write_channel": "AUTRESX", + "fakedcs:AUTRESX": { + "channel": "fakedcs:AUTRESX", + "gettable": true, + "settable": true, "description": "guider-resumeX in arcsec", "units": { - "": "arcsec", + "": "Radians", "formatted": "arcsec" }, "type": "double" }, - "autresy": { - "read_channel": "AUTRESY", - "write_channel": "AUTRESY", + "fakedcs:AUTRESY": { + "channel": "fakedcs:AUTRESY", + "gettable": true, + "settable": true, "description": "guider-resumeY in arcsec", "units": { - "": "arcsec", + "": "Radians", "formatted": "arcsec" }, "type": "double" }, - "autsqnum": { - "read_channel": "AUTSQNUM", - "write_channel": "AUTSQNUM", + "fakedcs:AUTSQNUM": { + "channel": "fakedcs:AUTSQNUM", + "gettable": true, + "settable": true, "description": "guider-seqnum in \"\"", "units": { - "": "", + "": "integer", "formatted": "" }, "type": "integer" }, - "autstop": { - "read_channel": "AUTSTOP", - "write_channel": "AUTSTOP", + "fakedcs:AUTSTOP": { + "channel": "fakedcs:AUTSTOP", + "gettable": true, + "settable": true, "description": "guider-stop in \"\"", "units": { - "": "", + "": "integer", "formatted": "" }, "type": "integer" }, - "autxcent": { - "read_channel": "AUTXCENT", - "write_channel": "AUTXCENT", + "fakedcs:AUTXCENT": { + "channel": "fakedcs:AUTXCENT", + "gettable": true, + "settable": true, "description": "guider-Xcent in arcsec", "units": { - "": "arcsec", + "": "Radians", "formatted": "arcsec" }, "type": "double" }, - "autycent": { - "read_channel": "AUTYCENT", - "write_channel": "AUTYCENT", + "fakedcs:AUTYCENT": { + "channel": "fakedcs:AUTYCENT", + "gettable": true, + "settable": true, "description": "guider-Ycent in arcsec", "units": { - "": "arcsec", + "": "Radians", "formatted": "arcsec" }, "type": "double" }, - "axestat": { - "read_channel": "AXESTAT", - "write_channel": "AXESTAT", + "fakedcs:AXESTAT": { + "channel": "fakedcs:AXESTAT", + "gettable": true, + "settable": true, "description": "axes-control-status in {unknown, not controlling, halted, in position, in limit, slewing, acquiring, tracking}", "units": { - "": "", + "": "binary", "formatted": "" }, "type": "enumerated", @@ -123,152 +134,167 @@ "64": "tracking" } }, - "az": { - "read_channel": "AZ", - "write_channel": "AZ", + "fakedcs:AZ": { + "channel": "fakedcs:AZ", + "gettable": true, + "settable": true, "description": "telescope-azimuth in deg", "units": { - "": "deg", + "": "Radians", "formatted": "deg" }, "type": "double" }, - "currinst": { - "read_channel": "CURRINST", - "write_channel": "CURRINST", + "fakedcs:CURRINST": { + "channel": "fakedcs:CURRINST", + "gettable": true, + "settable": true, "description": "current-instrument in \"\"", "units": { - "": "", + "": "string", "formatted": "" }, "type": "string" }, - "el": { - "read_channel": "EL", - "write_channel": "EL", + "fakedcs:EL": { + "channel": "fakedcs:EL", + "gettable": true, + "settable": true, "description": "telescope-elevation in deg", "units": { - "": "deg", + "": "Radians", "formatted": "deg" }, "type": "double" }, - "parang": { - "read_channel": "PARANG", - "write_channel": "PARANG", + "fakedcs:PARANG": { + "channel": "fakedcs:PARANG", + "gettable": true, + "settable": true, "description": "parallactic-angle-astrometric in deg", "units": { - "": "deg", + "": "Radians", "formatted": "deg" }, "type": "double" }, - "rotbase": { - "read_channel": "ROTBASE", - "write_channel": "ROTBASE", + "fakedcs:ROTBASE": { + "channel": "fakedcs:ROTBASE", + "gettable": true, + "settable": true, "description": "rotator-base-angle in deg", "units": { - "": "deg", + "": "Radians", "formatted": "deg" }, "type": "double" }, - "rotdest": { - "read_channel": "ROTDEST", - "write_channel": "ROTDEST", + "fakedcs:ROTDEST": { + "channel": "fakedcs:ROTDEST", + "gettable": true, + "settable": true, "description": "rotator-user-destination in deg", "units": { - "": "deg", + "": "Radians", "formatted": "deg" }, "type": "double" }, - "rotdets1": { - "read_channel": "ROTDETS1", - "write_channel": "ROTDETS1", + "fakedcs:ROTDETS1": { + "channel": "fakedcs:ROTDETS1", + "gettable": true, + "settable": true, "description": "rotator-destination-timestamp1-ut1-slow in \"\"", "units": { - "": "", + "": "Seconds", "formatted": "" }, "type": "double" }, - "rotdets2": { - "read_channel": "ROTDETS2", - "write_channel": "ROTDETS2", + "fakedcs:ROTDETS2": { + "channel": "fakedcs:ROTDETS2", + "gettable": true, + "settable": true, "description": "rotator-destination-timestamp2-ut1-slow in \"\"", "units": { - "": "", + "": "Seconds", "formatted": "" }, "type": "double" }, - "roterrs": { - "read_channel": "ROTERRS", - "write_channel": "ROTERRS", + "fakedcs:ROTERRS": { + "channel": "fakedcs:ROTERRS", + "gettable": true, + "settable": true, "description": "rotator-error-string in \"\"", "units": { - "": "", + "": "string", "formatted": "" }, "type": "string" }, - "rotervl": { - "read_channel": "ROTERVL", - "write_channel": "ROTERVL", + "fakedcs:ROTERVL": { + "channel": "fakedcs:ROTERVL", + "gettable": true, + "settable": true, "description": "rotator-error-status in \"\"", "units": { - "": "", + "": "integer", "formatted": "" }, "type": "integer" }, - "rothalt": { - "read_channel": "ROTHALT", - "write_channel": "ROTHALTC", + "fakedcs:ROTHALT": { + "channel": "fakedcs:ROTHALT", + "gettable": true, + "settable": true, "description": "rotator-halt-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "rothaltc": { - "read_channel": "ROTHALTC", - "write_channel": "ROTHALT", - "description": "rotator-halt-command-complement in \"\"", + "fakedcs:ROTHALTC": { + "channel": "fakedcs:ROTHALTC", + "gettable": true, + "settable": true, + "description": "rotator-halt-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "rotinit": { - "read_channel": "ROTINIT", - "write_channel": "ROTINITC", + "fakedcs:ROTINIT": { + "channel": "fakedcs:ROTINIT", + "gettable": true, + "settable": true, "description": "rotator-initialization-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "rotinitc": { - "read_channel": "ROTINITC", - "write_channel": "ROTINIT", - "description": "rotator-initialization-command-complement in \"\"", + "fakedcs:ROTINITC": { + "channel": "fakedcs:ROTINITC", + "gettable": true, + "settable": true, + "description": "rotator-initialization-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "rotmode": { - "read_channel": "ROTMODE", - "write_channel": "ROTMODE", + "fakedcs:ROTMODE": { + "channel": "fakedcs:ROTMODE", + "gettable": true, + "settable": true, "description": "rotator-tracking-mode in {unknown, position angle, vertical angle, stationary}", "units": { - "": "", + "": "binary", "formatted": "" }, "type": "enumerated", @@ -279,82 +305,90 @@ "4": "stationary" } }, - "rotnews": { - "read_channel": "ROTNEWS", - "write_channel": "ROTNEWS", + "fakedcs:ROTNEWS": { + "channel": "fakedcs:ROTNEWS", + "gettable": true, + "settable": true, "description": "rotator-new-destination-slow in \"\"", "units": { - "": "", + "": "integer", "formatted": "" }, "type": "integer" }, - "rotpdest": { - "read_channel": "ROTPDEST", - "write_channel": "ROTPDEST", + "fakedcs:ROTPDEST": { + "channel": "fakedcs:ROTPDEST", + "gettable": true, + "settable": true, "description": "rotator-physical-destination in deg", "units": { - "": "deg", + "": "Radians", "formatted": "deg" }, "type": "double" }, - "rotpdsts": { - "read_channel": "ROTPDSTS", - "write_channel": "ROTPDSTS", + "fakedcs:ROTPDSTS": { + "channel": "fakedcs:ROTPDSTS", + "gettable": true, + "settable": true, "description": "rotator-physical-destination-slow in deg", "units": { - "": "deg", + "": "Radians", "formatted": "deg" }, "type": "double" }, - "rotposn": { - "read_channel": "ROTPOSN", - "write_channel": "ROTPOSN", + "fakedcs:ROTPOSN": { + "channel": "fakedcs:ROTPOSN", + "gettable": true, + "settable": true, "description": "rotator-user-position in deg", "units": { - "": "deg", + "": "Radians", "formatted": "deg" }, "type": "double" }, - "rotpots1": { - "read_channel": "ROTPOTS1", - "write_channel": "ROTPOTS1", + "fakedcs:ROTPOTS1": { + "channel": "fakedcs:ROTPOTS1", + "gettable": true, + "settable": true, "description": "rotator-position-timestamp1-ut1-slow in \"\"", "units": { - "": "", + "": "Seconds", "formatted": "" }, "type": "double" }, - "rotpots2": { - "read_channel": "ROTPOTS2", - "write_channel": "ROTPOTS2", + "fakedcs:ROTPOTS2": { + "channel": "fakedcs:ROTPOTS2", + "gettable": true, + "settable": true, "description": "rotator-position-timestamp2-ut1-slow in \"\"", "units": { - "": "", + "": "Seconds", "formatted": "" }, "type": "double" }, - "rotpposn": { - "read_channel": "ROTPPOSN", - "write_channel": "ROTPPOSN", + "fakedcs:ROTPPOSN": { + "channel": "fakedcs:ROTPPOSN", + "gettable": true, + "settable": true, "description": "rotator-physical-position in deg", "units": { - "": "deg", + "": "Radians", "formatted": "deg" }, "type": "double" }, - "rotsel": { - "read_channel": "ROTSEL", - "write_channel": "ROTSEL", + "fakedcs:ROTSEL": { + "channel": "fakedcs:ROTSEL", + "gettable": true, + "settable": true, "description": "rotator-select in {none, fcass (forward cass), cass (cassegrain), lbc (left bent cass), rbc (right bent cass)}", "units": { - "": "", + "": "binary", "formatted": "" }, "type": "enumerated", @@ -366,32 +400,35 @@ "4": "rbc (right bent cass)" } }, - "rotspeed": { - "read_channel": "ROTSPEED", - "write_channel": "ROTSPEED", + "fakedcs:ROTSPEED": { + "channel": "fakedcs:ROTSPEED", + "gettable": true, + "settable": true, "description": "rotator-fake-speed in deg/sec", "units": { - "": "deg/sec", + "": "double", "formatted": "deg/sec" }, "type": "double" }, - "rotsrver": { - "read_channel": "ROTSRVER", - "write_channel": "ROTSRVER", + "fakedcs:ROTSRVER": { + "channel": "fakedcs:ROTSRVER", + "gettable": true, + "settable": true, "description": "rotator-servo-error in arcsec", "units": { - "": "arcsec", + "": "Radians", "formatted": "arcsec" }, "type": "double" }, - "rotstat": { - "read_channel": "ROTSTAT", - "write_channel": "ROTSTAT", + "fakedcs:ROTSTAT": { + "channel": "fakedcs:ROTSTAT", + "gettable": true, + "settable": true, "description": "rotator-state in {unknown, off, initializing, standby, tracking, fault, manual, halted, disabled, in position, slewing, acquiring, in limit}", "units": { - "": "", + "": "binary", "formatted": "" }, "type": "enumerated", @@ -411,52 +448,57 @@ "2048": "in limit" } }, - "rotstby": { - "read_channel": "ROTSTBY", - "write_channel": "ROTSTBYC", + "fakedcs:ROTSTBY": { + "channel": "fakedcs:ROTSTBY", + "gettable": true, + "settable": true, "description": "rotator-standby-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "rotstbyc": { - "read_channel": "ROTSTBYC", - "write_channel": "ROTSTBY", - "description": "rotator-standby-command-complement in \"\"", + "fakedcs:ROTSTBYC": { + "channel": "fakedcs:ROTSTBYC", + "gettable": true, + "settable": true, + "description": "rotator-standby-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "rotstst": { - "read_channel": "ROTSTST", - "write_channel": "ROTSTST", + "fakedcs:ROTSTST": { + "channel": "fakedcs:ROTSTST", + "gettable": true, + "settable": true, "description": "rotator-state-string in \"\"", "units": { - "": "", + "": "string", "formatted": "" }, "type": "string" }, - "rottime": { - "read_channel": "ROTTIME", - "write_channel": "ROTTIME", + "fakedcs:ROTTIME": { + "channel": "fakedcs:ROTTIME", + "gettable": true, + "settable": true, "description": "rotator-current-time in \"\"", "units": { - "": "", + "": "Seconds", "formatted": "" }, "type": "double" }, - "simulate": { - "read_channel": "SIMULATE", - "write_channel": "SIMULATE", + "fakedcs:SIMULATE": { + "channel": "fakedcs:SIMULATE", + "gettable": true, + "settable": true, "description": "simulating-dcs in {false, true}", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean", @@ -465,92 +507,101 @@ "1": "true" } }, - "terterrs": { - "read_channel": "TERTERRS", - "write_channel": "TERTERRS", + "fakedcs:TERTERRS": { + "channel": "fakedcs:TERTERRS", + "gettable": true, + "settable": true, "description": "tertiary-error-string in \"\"", "units": { - "": "", + "": "string", "formatted": "" }, "type": "string" }, - "tertervl": { - "read_channel": "TERTERVL", - "write_channel": "TERTERRS", + "fakedcs:TERTERVL": { + "channel": "fakedcs:TERTERVL", + "gettable": true, + "settable": false, "description": "tertiary-error-status in \"\"", "units": { - "": "", + "": "integer", "formatted": "" }, "type": "integer" }, - "terthalt": { - "read_channel": "TERTHALT", - "write_channel": "TERTHALTC", + "fakedcs:TERTHALT": { + "channel": "fakedcs:TERTHALT", + "gettable": true, + "settable": true, "description": "tertiary-halt-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "terthaltc": { - "read_channel": "TERTHALTC", - "write_channel": "TERTHALT", - "description": "tertiary-halt-command-complement in \"\"", + "fakedcs:TERTHALTC": { + "channel": "fakedcs:TERTHALTC", + "gettable": true, + "settable": true, + "description": "tertiary-halt-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "tertinit": { - "read_channel": "TERTINIT", - "write_channel": "TERTINITC", + "fakedcs:TERTINIT": { + "channel": "fakedcs:TERTINIT", + "gettable": true, + "settable": true, "description": "tertiary-initialization-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "tertinitc": { - "read_channel": "TERTINITC", - "write_channel": "TERTINIT", - "description": "tertiary-initialization-command-complement in \"\"", + "fakedcs:TERTINITC": { + "channel": "fakedcs:TERTINITC", + "gettable": true, + "settable": true, + "description": "tertiary-initialization-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "tertmove": { - "read_channel": "TERTMOVE", - "write_channel": "TERTMOVEC", + "fakedcs:TERTMOVE": { + "channel": "fakedcs:TERTMOVE", + "gettable": true, + "settable": true, "description": "move-tertiary in no,yes", "units": { - "": "no,yes", + "": "boolean", "formatted": "no,yes" }, "type": "boolean" }, - "tertmovec": { - "read_channel": "TERTMOVEC", - "write_channel": "TERTMOVE", - "description": "move-tertiary-complement in no,yes", + "fakedcs:TERTMOVEC": { + "channel": "fakedcs:TERTMOVEC", + "gettable": true, + "settable": true, + "description": "move-tertiary in no,yes", "units": { - "": "no,yes", + "": "boolean", "formatted": "no,yes" }, "type": "boolean" }, - "tertdest": { - "read_channel": "TERTDEST", - "write_channel": "TERTDEST", + "fakedcs:TERTDEST": { + "channel": "fakedcs:TERTDEST", + "gettable": true, + "settable": true, "description": "tertiary-destination in {lnas, lbc1, lbc2, stowed, rbc2, rbc1, rnas, cass, mirrorup}", "units": { - "": "", + "": "binary", "formatted": "" }, "type": "enumerated", @@ -566,12 +617,13 @@ "8": "mirrorup" } }, - "tertposn": { - "read_channel": "TERTPOSN", - "write_channel": "TERTPOSN", + "fakedcs:TERTPOSN": { + "channel": "fakedcs:TERTPOSN", + "gettable": true, + "settable": true, "description": "tertiary-position in {lnas, lbc1, lbc2, stowed, rbc2, rbc1, rnas, unknown, cass, mirrorup}", "units": { - "": "", + "": "binary", "formatted": "" }, "type": "enumerated", @@ -588,12 +640,13 @@ "9": "mirrorup" } }, - "tertstat": { - "read_channel": "TERTSTAT", - "write_channel": "TERTSTAT", + "fakedcs:TERTSTAT": { + "channel": "fakedcs:TERTSTAT", + "gettable": true, + "settable": true, "description": "tertiary-state in {unknown, off, initializing, standby, tracking, fault, manual, halted, disabled, in position, slewing, acquiring, in limit}", "units": { - "": "", + "": "binary", "formatted": "" }, "type": "enumerated", @@ -613,44 +666,48 @@ "2048": "in limit" } }, - "tertstby": { - "read_channel": "TERTSTBY", - "write_channel": "TERTSTBYC", + "fakedcs:TERTSTBY": { + "channel": "fakedcs:TERTSTBY", + "gettable": true, + "settable": true, "description": "tertiary-standby-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "tertstbyc": { - "read_channel": "TERTSTBYC", - "write_channel": "TERTSTBY", - "description": "tertiary-standby-command-complement in \"\"", + "fakedcs:TERTSTBYC": { + "channel": "fakedcs:TERTSTBYC", + "gettable": true, + "settable": true, + "description": "tertiary-standby-command in \"\"", "units": { - "": "", + "": "boolean", "formatted": "" }, "type": "boolean" }, - "tertstst": { - "read_channel": "TERTSTST", - "write_channel": "TERTSTST", + "fakedcs:TERTSTST": { + "channel": "fakedcs:TERTSTST", + "gettable": true, + "settable": true, "description": "tertiary-state-string in \"\"", "units": { - "": "", + "": "string", "formatted": "" }, "type": "string" }, - "utc": { - "read_channel": "UTC", - "write_channel": "UTC", + "fakedcs:UTC": { + "channel": "fakedcs:UTC", + "gettable": true, + "settable": true, "description": "coordinated-universal-time in h", "units": { - "": "h", + "": "Seconds", "formatted": "h" }, "type": "double" } -} +} \ No newline at end of file diff --git a/examples/parse_cake_config.py b/examples/parse_cake_config.py index f23fbed7..9a18ae35 100644 --- a/examples/parse_cake_config.py +++ b/examples/parse_cake_config.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """ -Parser to convert fakedcs_cake_config file to JSON format. +Parser to convert cake files to mktl JSON config format. -The parser extracts keyword definitions from the config file and creates +The parser extracts keyword definitions from the cake file and creates a JSON object mapping each keyword to its read and write channels. """ @@ -11,6 +11,185 @@ from pathlib import Path +CAKE_BIN2ASCII_METHODS = bin2asc_mapping = { + "R2D": { + "function": "r2d_bin2asc", + "description": "Radians to degrees", + "bin_units": "Radians" + }, + "R2D1": { + "function": "r2d_bin2asc", + "description": "Radians to degrees", + "bin_units": "Radians" + }, + "R2D2": { + "function": "r2d_bin2asc", + "description": "Radians to degrees", + "bin_units": "Radians" + }, + "RXD": { + "function": "rxd_bin2asc", + "description": "Radians to DMS", + "bin_units": "Radians" + }, + "RXH": { + "function": "rxh_bin2asc", + "description": "Radians to HMS", + "bin_units": "Radians" + }, + "RXH1": { + "function": "rxh1_bin2asc", + "description": "Radians to HMS with sign", + "bin_units": "Radians" + }, + "A2R": { + "function": "a2r_bin2asc", + "description": "Arcseconds to radians", + "bin_units": "Arcseconds" + }, + "M2A": { + "function": "m2a_bin2asc", + "description": "Meters to arcseconds (assumes f/15)", + "bin_units": "Meters" + }, + "M2A3": { + "function": "m2a_bin2asc", + "description": "Meters to arcseconds (assumes f/15)", + "bin_units": "Meters" + }, + "R2A": { + "function": "r2a_bin2asc", + "description": "Radians to arcseconds", + "bin_units": "Radians" + }, + "R2A2": { + "function": "r2a_bin2asc", + "description": "Radians to arcseconds", + "bin_units": "Radians" + }, + "R2A3": { + "function": "r2a_bin2asc", + "description": "Radians to arcseconds", + "bin_units": "Radians" + }, + "R2A4": { + "function": "r2a_bin2asc", + "description": "Radians to arcseconds", + "bin_units": "Radians" + }, + "R2S": { + "function": "r2s_bin2asc", + "description": "Radians to seconds of time", + "bin_units": "Radians" + }, + "R2S4": { + "function": "r2s_bin2asc", + "description": "Radians to seconds of time", + "bin_units": "Radians" + }, + "INT": { + "function": "int_bin2asc", + "description": "convert integer", + "bin_units": "integer" + }, + "BOO": { + "function": "boo_bin2asc", + "description": "convert boolean", + "bin_units": "boolean" + }, + "FLT": { + "function": "flt_bin2asc", + "description": "convert float", + "bin_units": "float" + }, + "DBL": { + "function": "dbl_bin2asc", + "description": "convert double", + "bin_units": "double" + }, + "DBL1": { + "function": "dbl_bin2asc", + "description": "convert double", + "bin_units": "double" + }, + "DBL2": { + "function": "dbl_bin2asc", + "description": "convert double", + "bin_units": "double" + }, + "DBL3": { + "function": "dbl_bin2asc", + "description": "convert double", + "bin_units": "double" + }, + "DBL4": { + "function": "dbl_bin2asc", + "description": "convert double", + "bin_units": "double" + }, + "M2CM": { + "function": "m2cm_bin2asc", + "description": "convert meters to centimeters", + "bin_units": "meters" + }, + "M2MM": { + "function": "m2mm_bin2asc", + "description": "convert meters to millimeters", + "bin_units": "meters" + }, + "M2UM": { + "function": "m2um_bin2asc", + "description": "convert meters to microns", + "bin_units": "meters" + }, + "STR": { + "function": "str_bin2asc", + "description": "convert string", + "bin_units": "string" + }, + "UTC": { + "function": "utc_bin2asc", + "description": "Seconds since 1970 to UT time of day (hh:mm:ss.ss)", + "bin_units": "Seconds" + }, + "DATE": { + "function": "date_bin2asc", + "description": "Seconds since 1970 to UT date (yyyy-mm-dd)", + "bin_units": "Seconds" + }, + "NOP": { + "function": "nop_bin2asc", + "description": "No conversion", + "bin_units": "" + }, + "ENM": { + "function": "enm_bin2asc", + "description": "convert binary to enumeration string", + "bin_units": "binary" + }, + "ENMM": { + "function": "enmm_bin2asc", + "description": "converts a single binary bit to the enumeration", + "bin_units": "binary" + }, + "MASK": { + "function": "mask_bin2asc", + "description": "convert binary to multiple bit mask", + "bin_units": "binary" + }, + "DT1": { + "function": "dt1_bin2asc", + "description": "rad-per-sec-to_s/hr", + "bin_units": "rad-per-sec" + }, + "DT2": { + "function": "dt2_bin2asc", + "description": "rad-per-sec-to-arcsec/hr", + "bin_units": "rad-per-sec" + } +} + + def parse_cake_config(input_file, output_file=None): """ Parse a cake configuration file and convert it to JSON. @@ -23,6 +202,7 @@ def parse_cake_config(input_file, output_file=None): dict: Parsed configuration as a dictionary """ result = {} + prefix = "" with open(input_file, 'r') as f: lines = f.readlines() @@ -30,6 +210,12 @@ def parse_cake_config(input_file, output_file=None): i = 0 while i < len(lines): line = lines[i].strip() + + # find prefix line + if line.startswith('prefix = '): + prefix = line.split('=', 1)[1].strip() + i += 1 + continue # Skip empty lines and comments if not line or line.startswith('#'): @@ -67,17 +253,19 @@ def parse_cake_config(input_file, output_file=None): # Move to the mapping line first to check if it's ENMM type i = j + if i < len(lines): mapping_line = lines[i].strip() # Parse the mapping line to extract the three channel names - # Format: KEYWORD READ_CHANNEL WRITE_CHANNEL COL4 COL5 TYPE ... + # Format: KEYWORD READ_CHANNEL WRITE_CHANNEL BIN2ASCII_method ASCII2BIN_method TYPE ... parts = mapping_line.split() if len(parts) >= 3: keyword = parts[0].lower() # First column (lowercase) - read_channel = parts[1] # Second column - write_channel = parts[2] # Third column + + read_channel = prefix + parts[1] # Second column + write_channel = prefix + parts[2] # Third column # Check if column 4 (index 3) is ENMM to determine bitmask enumeration is_bitmask = len(parts) >= 4 and parts[3] == 'ENMM' @@ -106,14 +294,20 @@ def parse_cake_config(input_file, output_file=None): enumerators = {str(i): val for i, val in enumerate(enum_values)} # Extract units from the description (after "in") - units = "" + f_units = "" if ' in ' in full_description: units_part = full_description.split(' in ', 1)[1] # Remove curly braces and their contents if present - units = re.sub(r'\{[^}]*\}', '', units_part).strip() + f_units = re.sub(r'\{[^}]*\}', '', units_part).strip() # If units is empty or just quotes, set to empty string - if units in ['""', '']: - units = "" + if f_units in ['""', '']: + f_units = "" + + bin2ascii_method = parts[3] if len(parts) >= 4 else "" + bin_units = "" + if bin2ascii_method in CAKE_BIN2ASCII_METHODS: + bin_units = CAKE_BIN2ASCII_METHODS[bin2ascii_method]['bin_units'] + # Extract type from 6th column (index 5) if available type_code = parts[5] if len(parts) >= 6 else "" @@ -129,32 +323,105 @@ def parse_cake_config(input_file, output_file=None): } data_type = type_map.get(type_code, type_code) - entry = { - "read_channel": read_channel, - "write_channel": write_channel, - "description": full_description, - "units": { - "base": units, - "formatted": units - }, - "type": data_type - } + # Determine if we need to create separate entries + has_read = read_channel and read_channel.strip() + has_write = write_channel and write_channel.strip() + same_channel = has_read and has_write and read_channel == write_channel - # Add enumerators only if they exist - if enumerators: - entry["enumerators"] = enumerators + if same_channel: + # Same read/write channel - create one entry + entry = { + "channel": read_channel, + "gettable": True, + "settable": True, + "description": full_description, + "units": { + "": bin_units, + "formatted": f_units + }, + "type": data_type + } + + # Add enumerators only if they exist + if enumerators: + entry["enumerators"] = enumerators + + result[keyword] = entry - result[keyword] = entry + else: + # Different read/write channels - create separate entries + if has_read: + read_entry = { + "channel": read_channel, + "gettable": True, + "settable": False, + "description": full_description, + "units": { + "": bin_units, + "formatted": f_units + }, + "type": data_type + } + + # Add enumerators only if they exist + if enumerators: + read_entry["enumerators"] = enumerators + + result[f"{keyword}:read"] = read_entry + + if has_write: + write_entry = { + "channel": write_channel, + "gettable": False, + "settable": True, + "description": full_description, + "units": { + "": bin_units, + "formatted": f_units + }, + "type": data_type + } + + # Add enumerators only if they exist + if enumerators: + write_entry["enumerators"] = enumerators + + result[f"{keyword}:write"] = write_entry i += 1 - + + # Now we are going to merge duplicate channels (same channel name) into single entries. + # The name will be the channel name, and gettable/settable will be True if any of the duplicates had it True. + + merged_result = {} + channels = [x['channel'] for x in result.values()] + for key, entry in result.items(): + channel = entry['channel'] + if channels.count(channel) > 1: + # Duplcate channel found - merge entries + if channel in merged_result: # already merged + continue + # Find all entries with this channel + same_channel_entries = [v for v in result.values() if v['channel'] == channel] + merged_entry = { + "channel": channel, + "gettable": any(e.get('gettable', False) for e in same_channel_entries), + "settable": any(e.get('settable', False) for e in same_channel_entries), + "description": same_channel_entries[0]['description'], # take from first entry + "units": same_channel_entries[0]['units'], # take from first entry + "type": same_channel_entries[0]['type'] # take from first entry + } + merged_result[channel] = merged_entry + else: # unique channel, just copy over + merged_result[channel] = entry + # Write to output file if specified if output_file: with open(output_file, 'w') as f: - json.dump(result, f, indent=2) + json.dump(merged_result, f, indent=2) print(f"Wrote {len(result)} entries to {output_file}") - return result + return merged_result def main(): From 6e72b527ea912b0ca3ca47017f9313b68f24e35d Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Thu, 22 Jan 2026 10:46:00 -1000 Subject: [PATCH 12/14] fixed typo in help text --- bin/mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/mk b/bin/mk index 7b57a5a3..1f699c48 100755 --- a/bin/mk +++ b/bin/mk @@ -115,7 +115,7 @@ def parse_command_line(): parser.add_argument('-u', '--units', default=None, - help='Use the requested units for getting or setting tem values. Note that this option is applied to all requests, and is not practical when interacting with multiple items.') + help='Use the requested units for getting or setting item values. Note that this option is applied to all requests, and is not practical when interacting with multiple items.') parser.add_argument('type', choices=('get', 'set', 'watch', 'describe', 'discover', 'list'), From 4d353e267745d15a61042cd0fc4151b6806617e5 Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Thu, 22 Jan 2026 10:47:35 -1000 Subject: [PATCH 13/14] typo in docuementation --- doc/mk.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/mk.txt b/doc/mk.txt index 9e1c265a..debaa700 100644 --- a/doc/mk.txt +++ b/doc/mk.txt @@ -41,7 +41,7 @@ options: --no-timestamp Omit the timestamp from the output of 'watch' requests. The default is to include the timestamp for 'watch' requests, and omit it from 'get' requests. - -u, --units UNITS Use the requested units for getting or setting tem + -u, --units UNITS Use the requested units for getting or setting item values. Note that this option is applied to all requests, and is not practical when interacting with multiple items. From 5142539c1846207f9557bc189f8bff899881f7f0 Mon Sep 17 00:00:00 2001 From: Tyler Tucker Date: Thu, 22 Jan 2026 14:36:24 -1000 Subject: [PATCH 14/14] order matters --- sbin/mkd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbin/mkd b/sbin/mkd index cdc69240..ee508253 100755 --- a/sbin/mkd +++ b/sbin/mkd @@ -83,7 +83,7 @@ def load_configuration(store, alias, filename): contents = open(filename, 'r').read() items = mktl.json.loads(contents) - mktl.config.authoritative(store, items, alias) + mktl.config.authoritative(store, alias, items)