# Original Design

:::{note}
This page was the initial plan for what PVI would do. The produce features have now
been removed and only direct conversion to a Device representation and then
formatting of UIs is supported now. This page is kept now to record the design in
case full integration into areaDetector is revisited in the future.
:::

```{eval-rst}
.. digraph:: pvi_flowchart

    bgcolor=transparent
    rankdir=LR
    node [fontname=Arial fontsize=10 shape=box style=filled fillcolor="#8BC4E9"]
    edge [fontname=Arial fontsize=10 arrowhead=vee]

    {   rank=same;
        "pilatus.pvi.yaml"
        "pilatus.cpp"
        "pilatus.h"
    }

    PVI [shape=doublecircle]
    "pilatus.local.yaml" -> PVI
    "pilatus.pvi.yaml" -> PVI
    PVI -> "pilatus_parameters.h"
    PVI -> "pilatus_parameters.template"
    PVI -> "pilatus_parameters.opi"
    PVI -> "pilatus_parameters.adl"
    PVI -> "pilatus_parameters.edl"
    PVI -> "pilatus_parameters.csv"
    "pilatus_parameters.template" -> "pilatus.template" [label="included in"]
    "pilatus_parameters.h" -> "pilatus.h" [label="included in"]
    "pilatus.cpp" -> "libPilatus.so"
    "pilatus.h" -> "libPilatus.so"
    "pilatus_parameters.csv" -> "pilatus.rst" [label="included in"]
    "pilatus_parameters.adl" -> "pilatus.adl" [label="linked from"]
    "pilatus_parameters.edl" -> "pilatus.edl" [label="linked from"]
    "pilatus_parameters.opi" -> "pilatus.opi" [label="linked from"]
```

```{eval-rst}
.. list-table:: Aims of PVI
    :widths: 20, 80
    :header-rows: 1

    * - Aim
      - Description
    * - Reduce boilerplate
      - At the moment you can write a simple asyn port driver in code, but
        there is a lot of boilerplate to connect it to the outside world.
        The createParam section, the database template records, and the
        lowest level screens are all quite repetitive and each layer looks
        like it could be autogenerated without much extra information
    * - Reduce copy paste errors
      - At the moment it is easy to create screens and database templates
        via copy and paste, but not changing a record name or parameter leads
        to hard to track down errors
    * - Support site specific styles for screens
      - Each site has their own style for screens, and many sites have their
        own site specific display manager. Rather than start with one display
        manager and convert, PVI takes a cut down Channel description (just a
        type, pv and widget), and lets the site specific template generate the
        screen according to local styles

```

## How it works

The YAML file contains information about each asyn parameter that will be
exposed by the driver, it's name, type, description, initial value, which record
type it uses, whether it is writeable or read only, which widget should be used,
etc. PVI reads these and passes them to Producer that creates intermediate Record,
Channel and AsynParam objects. These are passed to a site specific Formatter which
takes the tree of intermediate objects and writes a parameter CPP file, database
template, and site specific screens to disk.

### YAML file

The YAML file is formed of a number of sections:

```{eval-rst}
.. list-table::
    :widths: 20, 80
    :header-rows: 1

    * - Section
      - Description
    * - includes
      - The YAML files to use as base classes for superclasses
    * - local
      - A local override YAML file for site specific changes
    * - producer
      - Producer that knows how to create Records and Channels from the Components
    * - formatter
      - Site specific Formatter which can format the output files
    * - components
      - Tree of Components for each logical asyn parameter arranged in logical
        GUI groups
```

The Components are created from the YAML file with local overrides (also incorporating
the base classes for screens). These are passed to the Producer which produces
AsynParameters, Records and Channels. These are then passed to the Formatter which
outputs them to file:

```{eval-rst}
.. digraph:: pvi_products

    bgcolor=transparent
    node [fontname=Arial fontsize=10 shape=box style=filled fillcolor="#8BC4E9"]
    edge [fontname=Arial fontsize=10 arrowhead=vee]

    Intermediate [label="[Record(),\n Channel(),\n AsynParameter()]"]
    Products [label="Template\nScreens\nDriver Params\nDocumentation"]

    {rank=same; Components -> Producer -> Intermediate -> Formatter -> Products}
```

Here's a cut down pilatus.yaml file that might describe a parameter in a
detector:

```{literalinclude} ../snippets/pilatusDetector.pvi.producer.yaml
:language: yaml
```

### Screen files

The intermediate objects are a number of Channel instances. These contain basic
types (like Combo, TextInput, TextUpdate, LED, Group) and some creation hints
(like label, grouping, description, display_form), but no X, Y, Width,
Height or colour information. They may represent either a single widget or pair
of demand/readback widgets.

The site-specific Formatter consumes these Channel objects, then produces a screen
with style, sizing and layout that can be customized to the site. This means
that the default layout (big screen with lots of widgets arranged in group
boxes) could be produced for one site, then another site could make lots of
little screens with one group per screen. Styling is also covered, so the
blue/grey MEDM screens and green/grey EDM screens can be customized to fit
the site style guide.

### HTML Documentation

The Parameter and record sections of the existing documentation could be
reproduced, in tabular form as a csv file that can be included in rst docs:

```{eval-rst}
.. csv-table:: Pilatus Parameters
   :file: ../snippets/pilatusParameters.csv
   :widths: 15, 10, 8, 25, 25, 10, 60
   :header-rows: 1

```

## Questions

I am fairly happy with the scheme set out above, but there are a lot of
implementation questions. Here are the most pressing:

### One-time generation and checked into source control or generated by Makefile?

The process would probably be:

- If pvi cli tool available, build products as part of make
- Check in products to source control
- End users will only regenerate build products if pvi tool installed

ADGenICam would be supported by building a GenICamProducer which took no
components, just a path to a GenICam XML file

### Which screen tools to support?

I suggest creating adl and edl files initially, following the example of
makeAdl.py in ADGenICam, then expanding to support opi, bob and ui files
natively. This would avoid needing screen converters installed

## Drivers

The generated header file contains the string parameters, and defines the parameters to
make the interface. In this example we have a header file pilatusDetectorParamSet.h:

```{literalinclude} ../snippets/pilatusDetectorParamSet.h
:language: cpp
```

The existing pilatus.cpp is then modified to remove these parameters definitions and
use the param set API.

## Database Template File

According to the demand and readback properties of the component, the following
records are created:

```{literalinclude} ../snippets/pilatusDetectorParameters.template
```

The top level pilatus.template includes this file, as well as records that
provide logic (for things like the arrayRate and EPICSShutter in areaDetector).

## UI

Finally, UI elements can be generated for each component for multiple graphical
applications. For example, the following EDM screen is generated:

```{image} ../images/pilatus_edl.png
:align: center
:width: 50%
```

This can serve as a low level overview of the entire system, as well as a convenient
pallette for constructing higher level, more structured screens.

## Ongoing Development

Once a module is working with PVI (either an existing module after the YAML is created
from the templates using the one time generation script, or a newly written module) it
will then be necessary to update the YAML file in future development. Here is an
example of necessary changes to add a new parameter, with and without
PVI:

### With PVI

Update YAML file:

```YAML
- type: AsynFloat64
  name: DelayTime
  description: Delay in seconds between the external trigger and the start of image acquisition
  role: Setting
  initial: 0
  record_fields:
    PREC: 3
    EGU: s
```

And then run pvi (or possibly just make, if it is integrated into a Makefile). It can
then be shared with other sites who can generate their own required files.

### Without PVI

Update template:

```cpp
# Delay time in External Trigger mode.
record(ao, "$(P)$(R)DelayTime")
{
    field(PINI, "YES")
    field(DTYP, "asynFloat64")
    field(OUT,  "@asyn($(PORT),$(ADDR),$(TIMEOUT))DELAY_TIME")
    field(EGU,  "s")
    field(VAL,  "0")
    field(PREC, "6")
}

record(ai, "$(P)$(R)DelayTime_RBV")
{
    field(DTYP, "asynFloat64")
    field(INP,  "@asyn($(PORT),$(ADDR),$(TIMEOUT))DELAY_TIME")
    field(EGU,  "s")
    field(PREC, "6")
    field(SCAN, "I/O Intr")
}
```

Update header file:

```cpp
...
#define PilatusDelayTimeString "DELAY_TIME"
...
createParam(PilatusDelayTimeString, asynParamFloat64, &PilatusDelayTime);
...
int PilatusDelayTime;
...
```

Update docs:

```rst
* - Delay in seconds between the external trigger and the start of image acquisition
  - DELAY_TIME
  - $(P)$(R)DelayTime
  - ao
```

Update screens (of course, this will actually involve editing with a graphical
interface):

```javascript
"text update" {
    object {
        x=604
        y=146
        width=80
        height=18
    }
    monitor {
        chan="$(P)$(R)DelayTime_RBV"
        clr=54
        bclr=4
    }
    align="horiz. centered"
    limits {
    }
}
"text entry" {
    object {
        x=540
        y=145
        width=59
        height=20
    }
    control {
        chan="$(P)$(R)DelayTime"
        clr=14
        bclr=51
    }
    limits {
    }
}
text {
    object {
        x=435
        y=145
        width=100
        height=20
    }
    "basic attribute" {
        clr=14
    }
    textix="Delay time"
    align="horiz. right"
}
```

Then either add equivalent changes to other screen types or use autoconvert, if
available, and add any site specific details to any of these files (such as autosave
and archiver tags).

## Class Hierarchy

Drivers will access their parameters via a param set, either using inheritance or
composition. The class hierarchy for param sets mirrors the drivers. Each of the 'base'
classes (classes not instantiated directly) has-a paramSet containing its parameters in
addition to its parent class(es). The most derived classes inherit their param sets
so that they have direct access to all parameter indexes (and so that the source code
does not have to change to insert ``paramSet->`` to access them).

There is a new method ``asynPortDriver::createParams`` that iterates the member vector
of ``asynParamSet`` storing parameter definitions and calls
``asynPortDriver::createParam`` (no 's') on each of them. If the vector is empty
(i.e. if it only has the default ``asynParamSet`` and not a specific implementation)
it has no effect. This means ``asynPortDriver`` can be inherited from as before with
no change.

Virtual inheritance is required for two reasons. Primarily, it ensures only a single
instance of ``asynParamSet`` is created and it is shared throughout the class
hierarchy to ensure asynPortDriver can find the child parameters. It also means that
the most derived class must call the constructors for all virtual base classes before
the non-virtual base classes. This means the constructors are called in the correct
order such that when the asynPortDriver constructor the ``asynParamSet``
``parameterDefinitions`` is fully populated when ``createParams`` is called.

### Change Summary

> - asyn
>   : - Created ``asynParamSet``
>     - New overloaded asynPortDriver constructor that takes an ``asynParamSet*``
>       and calls createParams()
> - ADCore
>   : - ``asynNDArrayDriver`` parameters split into ``asynNDArrayDriverParamSet``
>       : Constructor updated to take an ``asynNDArrayDriverParamSet*``.
>         Updated to access parameters via ``paramSet->``
>     - ``ADDriver`` parameters split into ``ADDriverParamSet``
>       : Constructor updated to take an ``ADDriverParamSet*``.
>         Updated to access parameters via ``paramSet->``
>     - ``NDPluginDriver`` inherits ``asynNDArrayDriverParamSet`` in addition to ``asynNDArrayDriver``
>       : Updated to access parameters via ``paramSet->``.
>         Child classes work with no changes
>     - Some trivial updates to the tests
> - ADSimDetector
>   : - ``simDetector`` parameters split into ``simDetectorParamSet``
>     - ``simDetector`` inherits from ``simDetectorParamSet`` in addition to
>       ``ADDriver``
>     - Can access parameters as before
> - ADPilatus
>   : - Equivalent to ADSimDetector changes
> - motor
>   : - ``asynMotorController`` parameters split into ``asynMotorControllerParamSet``
>     - Updated to access parameters via ``paramSet->``
> - pmac
>   : - ``pmacController`` parameters split into ``pmacControllerParamSet``
>     - ``pmacCSController`` same
>     - Each inherit from their own param set (which inherits
>       ``asynMotorControllerParamSet``) in addition to ``asynMotorController``
>     - Can access parameters as before

### Caveats

There are some changes that are unavoidable without inserting edge cases into the
generation logic and making the YAML schema more complicated. Some examples are:

> 1. The first param index used for calling base class methods is inconsistently
>    named, so we will have to agree a consistent way to generate them and make them
>    the same.
> 2. Any readback parameters will have an \_RBV suffix added. Some existing readbacks
>    do not have this, e.g. Armed in pilatusDetector.
> 3. FIRST_DRIVER_PARAM needs to be defined in the main header file based on the
>    FIRST_DRIVER_PARAM_INDEX defined in the param set header file, appending
>    ``paramSet->`` or not depending on whether it inherits the param set or not. (This
>    could possibly be handled in a better way by adding more logic to the
>    ``asynParamSet`` - see [Possible Further Work])
> 4. Asyn parameter names will be the same as the name of the index variable. The
>    value can be overridden to define drvInfo for drvUserCreate dynamic parameters.

### Next Steps

A script is in development to perform the generation of an initial YAML file from a
template. The idea being that after this point, everything is generated from the YAML
file and any changes are made there and everything regenerated. New drivers can use
a YAML file from the start.

The current hierarchy (base classes have-a param set while the most derived class
is-a param set) is inconsistent and confusing, but it has the benefit that people are
not forced to change drivers inheriting from these base classes to access parameters
via ``paramSet->``, they just need to inherit the parent param set. It also means that
the most derived classes cannot be inherited from alongside an extended param set
(this is not necessarily a problem, but it seems like an unnecessary restriction).
It would be clearer to remove this and require all child classes to use the param set
explicitly, removing the need for virtual inheritance. This would just need an addition
to the extern C calls to create the param set and pass it into the constructor of the
driver. Another option is to create a class that inherits from the driver and the param
set, with no additional logic. This could then instantiate the param set and pass it to
the driver constructor. Either solution would resolve caveat 3, because all classes
will access FIRST_DRIVER_PARAM via ``paramSet->``.

### Possible Further Work

Adopting this framework could make it easier to make other improvements to the C++ code,
such as:

> - Move some asynPortDriver functionality into ``asynParamSet`` / ``asynParam`` (See ADEiger eigerParam)
>   : - Reduce the size of ``asynPortDriver``
>     - Possibility of typed subclasses of ``asynParam`` to reduce if statements for
>       handling the many ``asynParamType`` values in slightly different ways
> - Simplify the sharing of file writing functionality between some drivers
>   : - ADPilatus and NDPluginFile-derived classes - Parameters could be split out
>       of ``asynNDArrayDriver`` into a ``FileWriterParamSet``, which could then be
>       included via composition only where required, rather than every driver and
>       plugin under ``asynNDArrayDriver`` having these parameters.

[yaml]: https://en.wikipedia.org/wiki/YAML
