Skip to content
Will Rogers edited this page May 8, 2020 · 11 revisions

In the terminology of EPICS user interfaces, macros are simple string substitutions. At runtime a map from macro name to value is available (the macro map); any PV that contains a macro key should be replaced by one using the value.

If the dictionary is:

macros = {
    MACRO1: SUFFIX
}

and a PV has the name PREFIX:${MACRO1} it should resolve to PREFIX:SUFFIX.

We need to have dynamically-updating macros so that a PV in a display may change its resolved PV at runtime if the macro map changes.

An additional requirement: if a component registers its interest in an unresolved PV name (such as PREFIX:${MACRO1}, at runtime it should have access to the resolved PV name(PREFIX:SUFFIX) as well as the updates associated with that PV. This is so that the user can see what PV is actually being displayed.

Properties as macros

In Phoebus, the value of any property (e.g. x: 200) may be used in any other property of type string. If you try text: $(text) you will get a warning of a recursive definition. This is overridden by any macros defined by other means.

Special macros

CS-Studio provides three special macros:

  • $(DID) - display ID
  • $(LCID) - linking container ID (apparently not in Phoebus)
  • $(DNAME) - display name

These can't be overridden by any other macros.

Changing the values of macros

EDM has a special button (Menu Mux) that allows setting macros based on a choice. It actually uses different things called 'symbols' but they are substituted into strings in the same way as macros.

It has an even more special button (Menu Mux PV) that allows setting the symbols to the value of a PV.

Both of these capabilities are desirable, but probably not by the same mechanism.

The macro hierarchy

The above refers to global macros. However, the real situation is a little more complicated. In practice there are different scopes of macros, increasing in priority:

  • values of properties on that widget (e.g. pv_name)
  • global macros
  • macros defined at launch of the screen (or container widget)
  • macros hard-coded into the screen
  • special macros $(DID) $(DNAME) and $(LCID)

What this means is that each widget has its own effective macros, determined by merging the macros appropriately at load time.

Implementation

See also Redux.

The following two points are challenging:

  • The component is created with the unresolved PV name, but the middleware requires the resolved PV name in order to create the correct subscription.
  • The component doesn't know the resolved PV name, but needs it at runtime.

Since each widget has an effective macro dictionary, the easiest way to implement this is to resolve macros at render time. This also handles the above two points because the component then has access to the resolved PV name. If this proves to be too slow, we will have to investigate caching the resolved macros.

The implementation is as follows:

  • there is a global macro map held in the store
  • container widgets may define their own macro maps; these take precedence
  • container widgets provide macros via MacroContext objects. The MacroContext object also contains a function that allows child components to update macros of a parent
  • at render time the macro map for each widget is assembled from global macros, the containing macro context and the widget's own props. These macros are resolved on all values of type string in props
  • an additional property rawPvName is added to props

This logic is in the useMacros.ts file, which contains the useMacros hook, as well as each container widget.

We are providing the ${DID} macro, but not yet ${DNAME} or ${LCID}.

Not yet implemented

  • The ${DNAME} and ${LCID} macros
  • Allowing PV values to be used as macro values (as in the EDM MenuMuxPV widget)
  • Ensuring that nested macros are correctly resolved i.e. if B=C, pvName is A${B} and tooltip is PV: ${pvName}, ensuring that tooltip is PV: AC.