- [The Module View](#the-module-view)
  - [Templates](#templates)
    - [Module View (`modxUI.xml`)](#module-view-modxuixml)
    - [SFP Module (sfpModule.xml)](#sfp-module-sfpmodulexml)
    - [Baseband Tab (`baseband.xml`)](#baseband-tab-basebandxml)
    - [Adding a new Module](#adding-a-new-module)
    - [Adding a new Common Tab](#adding-a-new-common-tab)
    - [Multi-Select Tabs](#multi-select-tabs)
      - [Updating Scratch Parameters](#updating-scratch-parameters)
    - [Non-Standard Widget Usage](#non-standard-widget-usage)
      - [Event Handlers](#event-handlers)
  - [Selection](#selection)
    - [Selection from Picture](#selection-from-picture)
    - [Selection from Table](#selection-from-table)
  - [Updating the Page](#updating-the-page)
    - [IDs and OIDs](#ids-and-oids)
    - [IO Based Page Updates](#io-based-page-updates)
    - [Directionality](#directionality)
  - [General Support](#general-support)
    - [Basic Requirements](#basic-requirements)
    - [Alarms and Overlays](#alarms-and-overlays)
  - [Re-Enabling Mandatory Module Upgrades](#re-enabling-mandatory-module-upgrades)

# The Module View

The module view exists as a way for us to display data relevant to the modules in a MODX card. The specific data that must be displayed is highly dependent on the type of module being selected, which means the Module View must be able to adapt the display as needed to ensure that the information on the screen always maps to the user's selection.

Key features that enable this are as follows:
1. Module and Tab Templates
    - To reduce the need for copy/pasting code and help provide separation between custom code for each module we define modules and tabs separately, then use the <include src="*"/> tag to import these layouts as needed.
    - Some Restrictions apply! Skim through [Templates](#templates) and [Updating the Page](#updating-the-page) before creating a new template
2. Table + Page division
    - View is divided into a condensed table on the left, then a larger tabbed page on the right
    - Table displays all ports in the card that belong to modules of the selected type.
        - See [Selection](#selection) for more information on how the table content gets updated and what happens when a user selects a row from the table
    - Page contains all XML layouts for all module types, logic exists in `moduleLibrary.js` for showing and hiding these as needed. 
        - See [Updating the Page](#updating-the-page) for more information on how to make use of the mechanism for updating parameters using ids and oids.
        - See also [Non-Standard Widget Usage](#non-standard-widget-usage) for more information on how to handle complex widgets in the XML

------------------------------------------------------------------------------------------------

## Templates
The module view is created using a nested structure of imported XML pages. Example Structure is as follows:

*Note: only displaying minimum information to illustrate how the information is broken down. See existing modules for more detailed examples, particularly with respect to layout, [ids, and oids](#ids-and-oids)*

### Module View (`modxUI.xml`)
```xml 
<abs id="moduleUI">
    <ogscript handles="onload">
        // Define shared tabs and simple modules
        defineTab("baseband", 
            ["ABC.id", "ABC.video", "ABC.audio"], // ids updated when selecting anything
            [], // ids that are updated only when selecting an input
            []); // ids that are updated only when selecting an output 
        defineModule("none", []);
        defineModule("sdi", ["baseband"]);
    </ogscript>
    <abs id="module.none" immediate="true" visible="true">
        <label id="module.label" name="EMPTY MODULE" style="txt-align:center;font:bold"/>
    </abs>
    <abs id="module.sdi" immediate="true" visible="true">
        <include src="/oglml/moduleView/modules/sdiModule.xml"/>
    </abs>
    <abs id="module.sfp" immediate="true" visible="true" >
        <include src="/oglml/moduleView/modules/sfpModule.xml"/>
    </abs>
</abs>
```

### SFP Module (sfpModule.xml)
```xml
<abs>
    <meta>
    <!-- Any custom code needed here -->
    </meta>
    <ogscript handles="onload">
        defineTab("sfpStats", 
            ["ABC.sfp.temperature", "ABC.sfp.voltage", "ABC.sfp.vendor"], 
            [],
            []);
        defineModule("sfp", "sfpStats", "baseband");
    </ogscript>
    <tab tabposition="west">
        <abs id="basebandTab" name="Port (Video)">
            <include src="/oglml/moduleView/tabs/baseband.xml"/>
        </abs>
        <abs id="sfpStatsTab" name="SFP Statistics">
            <table id="sfpStats">
			<tr><label name="Temperature" id="ABC.sfp.temperature"/></tr>
			<tr><label name="Voltage" id="ABC.sfp.voltage"/></tr>
			<tr><label name="Vendor PN" id="ABC.sfp.vendor"/></tr>
            </table>
        </abs>
        <!-- Any tabs specific to this module can be laid out here -->
    </tab>
</abs>
```
### Baseband Tab (`baseband.xml`)
```xml
<abs id="basebandTabTemplate">
	<table id="baseband.in" visible="true">
			<tr><label name="Physical Address" id="ABC.id"/></tr>
			<tr><label name="Video" id="ABC.video"/></tr>
			<tr><label name="Audio" id="ABC.audio"/></tr>
	</table>
    <table id="baseband.out" visible="false">
			<tr><label name="Physical Address" id="ABC.id" /></tr>
			<tr><label name="Video" id="ABC.video"/></tr>
			<tr><label name="Audio" id="ABC.audio"/></tr>
	</table>
</abs>

```

In addition to declaring tabs in this way, the IDs used in a tab must be registered and sent to the `moduleLibrary` for use in updating the page. The intended convention at the time of writing this is for generic tab ids (i.e. those used in the baseband tab) to be registered during the startup steps in `modxUI.xml`, then for specific tab ids to be added on using the `defineTab` and `defineModule` methods found in `moduleLibrary.js` FROM THE LAYOUT FILE FOR THAT SPECIFIC MODULE. 

### Adding a new Module
Likely the most common operation when modifying the UI will be adding a new module. This is done by creating a new XML file in the modules folder, and adding a `defineModule` call during an onload handler. After that the structure of the module is up to the developer. 

A system is in place now to handle automatically updating the right hand page in response to selection changes which operates by updating OIDs in the XML based on the currently selected port and the IDs that are registered for the selected module type. If there is additional logic that is required, i.e. sending a ZMQ message in response to an OID changing, then any event handlers must be triggered from `<someModule>.xml` to prevent [collisions](#template-restrictions).

Any common tabs used by the module (i.e. the card, module, and port(video) tabs) should be imported using their respective xml file links. This is to ensure that the common tabs are only defined once and that the same tab is used across all relevant modules. 

Tabs specific to your module can be defined as you desire but will only be able to make use of the existing page update mechanism if they follow a similar ID/OID structure as defined in [IDs and OIDs](#ids-and-oids). Each specific tab is only ever imported once alongside the module it is associated with, therefore any additional logic stored in the xml file will only be running once.

### Adding a new Common Tab
As mentioned above, common tabs as they are now should be "dumb". They serve as passthroughs for whatever OIDs are defined on the page to serve as a landing page for that information and to allow the user to edit anything that can be edited. Any complex logic stored in this page will run multiple times, once for each module that imports it. This is not necessarily going to break the UI, but it can cause unexpected behavior if not accounted for. Furthermore having the same code update read and write the same OIDs and params multiple times is just unnecessary in most cases.

If defining a common tab, define all IDs in the onload handler for the `modxUI.xml` file alongside the baseband tab. This will ensure that all IDs for this tab are updated whenever a module containing that tab is used. 

### Multi-Select Tabs
Multi-Select feature for the Module View adds an automatic system for swapping out tab OIDs with a temporary "scratch" oid, which allows values to be "blanked out" when multiple ports are selected. To enable or disable this for a tab or for more information, refer to the `defineTab` method in `moduleLibrary.js` and the `updatePageContent` method in `moduleLibrary.js`.

In the following example a tab is defined that will make use of the scratch OIDs. This is determined by the boolean value passed in at the end.

```js
        defineTab(
            "module", 
            [
                "module_port.X.id",
                "module_port.X.type",
                "module_port.X.serial_number",
                "module_port.X.fw_version",
                "module_port.X.part_number",
            ],[],[], true
        );
```

If scratch OIDs are not desired, then this value must be set to false.

#### Updating Scratch Parameters

When you add the scratch OIDs for your new tab, you need to make sure that the scratch params get updated properly when the backend OIDs get updated. This should mainly be a concern if the OIDs you are adding include port indexing relative to the selected module. An example of this would be the SFP Module data, it follows the format of `module_port.X.module_sfp_data.Y.<parameter>` where X is the module index and Y is the port index. 

In `moduleLibrary.js` you need to make the following changes:
1. At the top of the file, add the regex for your the section of OID for your module data. It should only capture the port index relative to the module. This will be compared later to the selected ports to determine if the scratch OIDs need to be updated.
```js
const modSfpDataOidRegex = /module_sfp_data\.([0-3])\.(.*)/; // Example for SFP module
```
2. In the function `SetupModuleView`, add a listener for your new OIDs.
```js
// add mid level module_port listeners
keyString = 'Board' + slotNum + mPortListOidName.MODULE + modIdx + "<module_data>";
if ( !(keyString in mModxCardListenerList) ) {
    var param = params.getParam(modulePortOid + ".module_data", 0);
    mModxCardListenerParamList[keyString] = param;
    mModxCardListenerList[keyString] = mModxCardListenerParamList[keyString].addSubParamListener(null, OnGenericModuleParamChange, null);
} 
```
3. Add a case for your OIDs in the `OnGenericModuleParamChange` function to use the regex for your module data OID. The port index will be extracted later. Also generate the ID list for your module. This will filter out all of the OIDs that are not relevant to your module.
```js
if (module_data_oid.includes("<module_data>")){
    module_data_regex = modSfpDataOidRegex;
    ids = ids.concat(mAllTabs["<module_tab1>"]["all"], mAllTabs["<module_tab2>"]["all"], ...);
}
```
### Non-Standard Widget Usage
As briefly touched on in the [IDs and OIDs](#ids-and-oids) section, the OID associated with a param on startup is crucial for determining how the UI will display everything. If improperly defined, or if the data they point to is not instantiated in the params object in Go, then those invalid UI elements will not appear in the UI at all even after the page is updated to point at new OIDs. The params.frameslots object is instantiated on startup and the MODX specific fields only appear if a MODX card is in a given slot so we cannot guarantee that any hardcoded oid pointing to the actual paths will be correct on startup. 

To get around this we use locally defined parameters in `modxUI.xml` of the same widget type as the desired OIDs to ensure that the UI is able to render correctly at startup. In order to make your specific fields work you must either re-use an existing param defined in the `LocalModuleParams` tag in `modxUI.xml`, or add a new param to that list if your desired widget is not supported. Please view `baseband.xml` for an example of how these local OID parameters can be used.

If you need to define a new widget that is not already defined in `LocalModuleParams` then you will need to compare the Go structure of your module to the widget options defined in ogp.go and the list in appendix A of the [Dashboard Development Guide](https://documentation.rossvideo.com/files/Manuals/Facility%20Control%20Systems/DashBoard/DashBoard_CustomPanel_Development_Guide_(8351DR-007).pdf)(around page 288) to learn your widget's string name or integer id. Once you have this information you can create a new param in `LocalModuleParams`, provide it with a dummy constraint if applicable, and use that param in your XML file.

#### Event Handlers
When adding event handlers to a template, it is important to remember that the event handlers are not unique to the template. This means that if a template has an onload handler, then every instance of that template will have the onload handler triggered. This can lead to unexpected behavior if not accounted for.

as an example, if in the baseband tab the following tab was added:
```xml
<ogscript handles="onload">ogscript.debug("hello world once");</ogscript>
```
Then when opening the page in dashboard the openGear debug page would display this in the output;
```
hello world once
hello world once
```
This is because the onload handler is being triggered twice, once for the instance in the SFP module, once for the instance in the SDI module.

In contrast, adding that tag only once to the `sdiModule.xml` would result in the following output:
```
hello world once
```
------------------------------------------------------------------------------------------------

## Selection
Two cases for selection: [Selection from Picture](#selection-from-picture) and [Selection from Table](#selection-from-table)

### Selection from Picture
When in the module view, the selection of a port, module, or board, will cause the following steps to occur, resulting in updates to both the table and page displays:
1. The selected module type, board number, and I/O direction will update based on the selection
    - This information is used later to filter the list of all inputs or all outputs on the board down to only those matching the desired module type.
2. The table will update to display all ports that belong to the selected module type
3. The table will reset it's selection to be only the first port in the list
4. The page will update to display the XML layout for the selected module type
5. The page will refresh all OIDs in the XML based on the currently selected port and the current module type. See [Updating the Page](#updating-the-page) for a breakdown of how this step works.

See `setModuleViewOptions` and `setModuleViewData` in `moduleLibrary.js` and their uses in `uiOperations.js` for more information on how to modify selection logic.

### Selection from Table
When in the module view, the selection of a port, module, or board, will cause the following steps to occur:
1. The selected port in the table will update
2. The page will refresh all OIDs in the XML based on the currently selected port and the current module type. See [Updating the Page](#updating-the-page) for more information on this step.

See `onModuleTableSelection` and `updatePageContent` in `moduleLibrary.js` for more information on the flow for updating page content. 

------------------------------------------------------------------------------------------------

## Updating the Page

### IDs and OIDs
As UltriStream and UltriProc, the IDs for each element are translated down the line into a backend OID of the form: 

```
params.frameslots.A.<Rest of the ID>

i.e.:
id: module_port.X.type
 => params.frameslots.A.module_port.X.type
id: module_port.X.modx_port.Y.ports.Z.id
 => params.frameslots.A.module_port.X.modx_port.Y.ports.Z.id
```

These IDs are registered in the `moduleLibrary.js` file, using the defineTab method then used when updating the page content. When updating the page later the ID will be translated into the OID method, then the parameter with that ID will be updated to point at a new object with the provided OID. 

Although the OIDs associated with each parameter are always overridden during the updatePageContent function call the actual values they point to cannot be ignored. An invalid OID will cause that element to not be displayed, even after being overridden during this step. See [Non-Standard Widget Usage](#non-standard-widget-usage) for more specific information on how to handle this if encountered.

### IO Based Page Updates

One slightly hidden feature is IO based page updates. When defining a tab the user assigns it an id. This ID is referenced by modules when defining the tabs they import, but it is also used when the Module Type or I/O direction changes.

During setModuleViewOptions, if any change that would modify the view is detected, then it passes off to the showModulePage method. In this method any UI element with an id of the form:
```
<tab id>.in
or 
<tab id>.out

i.e.
baseband.in
baseband.out
``` 

Will be shown or hidden based on the current I/O direction. No such logic is in place for showing/hiding individual fields as tabs so far are defined with either no preference for direction, or with such strong preference that there is next to no difference between the two.

As an example, `baseband.xml` contains two tables, one for inputs and one for outputs (IDs set as per the above example). When the I/O direction changes from inputs to outputs (or vice versa) the UI for the new direction will be brought up and the old will be hidden.

Multiple UI elements can share the same ID, in this case all elements with the same ID will be updated at the same time.

### Directionality
When defining tabs, three lists of IDs are provided. The "generic", "inputs", and "outputs" lists. These are used during updatePageContent to determine what IDs specifically would need to be updated in a given case. When possible, these should be used when defining tabs that care about when an input vs output is selected as it will allow the page to update only the relevant fields when the I/O direction changes.

------------------------------------------------------------------------------------------------

## General Support

The Module View contains several convenience features from dynamically updating module templates to free switching between module pages in the module view. To enable support for these options, the following steps must be followed:

### Basic Requirements

1. `hyperIO.go`, locate the `moduleTypeNameList` structure, add your module name to this list. It should be organized according to values in `ModxCommonTypes.hpp`.
    ```golang
    var moduleTypeNameMap = []string{
        "NONE",
        "SFP",
        "SDI",
        "NDI",
    }
    ```
2. `hyperIO.go`, locate the `upgradeableModules` structure, add your module name to this list. It can be organized according to values in `ModxCommonTypes.hpp`.
    ```golang
    var upgradeableModules = []string{"NDI"}
    ```
3. `frameUI.oglml` locate the `mHyperIOModuleTypes` dictionary, correlate the name in moduleTypeNameMap to an ID that will be used throughout the UI oids to refer to your module
    ```js
    // All OID string names of HyperIO Modules
    // must match moduleTypeNameMap in hyperIO.go
    const mHyperIOModuleTypes = {
        "NONE": "none",
        "SFP": "sfp",
        "SDI": "sdi",
        "NDI": "ndi"
    }
    ```
4. `frameUI.oglml` locate the `mViewFilterNamesMap` dictionary, add your module name to this list. 
    ```js
    // View filter display names for each option
    const mViewFilterNamesMap = {
        "ip": "IP",
        "baseband": "Baseband",
        "in": "Inputs",
        "out": "Outputs",
        "port": "Ports",
        "channel": "Channels",
        "videoproc": "UltriProc",
        "ultristream": "UltriStream",
        "modx": "IO Module",
        "none": "None",
        "sfp": "SFP",
        "sdi": "SDI"
    }
    ```
5. `uiOperations.js` locate the `onViewFilterModeChange` function, add your name as a case to the end of the switch statement. All module types should be handled by the `onViewFilterModeModuleType` function.
    ```js
    function onViewFilterModeModuleType(moduleType){
        ...
        case mHyperIOModuleTypes.SFP:
        case mHyperIOModuleTypes.SDI:
            onViewFilterModeModuleType(id);
            break;
        default:
    }
    ```

6. `moduleLibrary.js` locate the `moduleHasInputs` and `moduleHasOutputs` functions, add cases for your function if required. All module types should be handled in these functions.

    ```js
    function moduleHasInputs(moduleType) {
        switch (moduleType) {
            case mHyperIOModuleTypes.DPT:
            case mHyperIOModuleTypes.NONE:
                return false;
            default:
                return true;
        }
    }
    ```

7. `moduleLibrary.js` locate the `moduleUsesDirectionalInternalIdSuffix` function, add cases for your module based on the modxTemplate indicator ids.

    ```js
    function moduleUsesDirectionalInternalIDSuffix(moduleType) {
        switch (moduleType) {
            case mHyperIOModuleTypes.SDI:
            case mHyperIOModuleTypes.DPT:
                return true;
            default:
                return false;
        }
    }
    ```

### Alarms and Overlays

1. `hyperIO.go` locate the `getModuleFwMismatchPerTypeHandler` function, add a case for your module type. This function is responsible for fetching the firmware mismatch data for the selected module type.
    ```golang
    func getModuleFwMismatchPerTypeHandler(w http.ResponseWriter, r *http.Request) {
        ...
        
        // NOTE: Following comments must be un-commented when the first module is made available
        var slotData = rawDatabaseModxParam.Value.([]*MODXOgpData)[0].Slots[modxIndex]

        for _, mismatchData := range mismatchJsonData {
            mismatch := moduleFwMismatchType(boolToInt(mismatchData.Mismatch) + 1)

            isUpgradeable := slices.Contains(upgradeableModules, moduleTypeNameMap[mismatchData.Type])
            if !isUpgradeable {
                hyperioLog().Debug().Int("modxIndex", modxIndex).Int("moduleType", mismatchData.Type).Str("moduleTypeString", moduleTypeNameMap[mismatchData.Type]).Msg("getModuleFwMismatchPerTypeHandler - Module Type is not upgradeable")
                continue
            }
            // Set mismatch value, if successful, broadcast to DashBoard
            changed := false
            moduleType := moduleTypeNameMap[mismatchData.Type]
            switch moduleType {
            case "NDI":
            	slotData.Mismatch.NDI = int(mismatch)
            	changed = true
            default:
                hyperioLog().Error().Str("moduleType", moduleType).Msg("setFwMismatchOnSlot - Unrecognized module type")
                return
            }
            if changed {
            	broadcastParamValChange(ogp.JoinOIDs("params.modx.slots", ogp.IntOID(modxIndex), "mismatch", ogp.OID(strings.ToLower(moduleType))), mismatch)
            	// startup check, if we have just received feedback and we know what modules are available, check if we need to start an upgrade
            	if isMandatoryUpgradeMode() {
            		checkAndMaybeStartUpgrades(slotID)
            	}
            }
        }
    ```
2. `hyperIO.go` locate the `getFwMismatchFromSlot` function, add a case for your module type. This function is responsible for fetching the firmware mismatch data for the selected module type.
    ```golang
    func getFwMismatchFromSlot(slot *ModxSlot, moduleType string) moduleFwMismatchType {

        fwMismatch := NotReady

        switch moduleType {
        case "NDI":
            fwMismatch = moduleFwMismatchType(slot.Mismatch.NDI)
        default:
            hyperioLog().Error().Str("slotID", slot.SlotID).Msg("shouldSetModuleWaitingToUpgrade - Invalid Module Type")
        }
        return fwMismatch
    }
    ```

3. `hyperIO.go` restore `fw_mismatch` in `types.modx` and extend it to include your module type. This structure is responsible for storing the firmware mismatch data for the selected module type.

    ```golang
        "_d_fw_mismatch": &ogp.Param{
            OID:      "fw_mismatch",
            Name:     "FW FwMismatch",
            ReadOnly: ogp.ReadWrite,
            Type:     ogp.Struct,
            Value:    ogp.Params{
                "_d_ndi": &ogp.Param{ // Template for future modules
                    OID:   "ndi",
                    Name:  "NDI FwMismatch",
                    Type:  ogp.Int16,
                    Value: 0,
                },
            },
        },
    ...
    ```
## Re-Enabling Mandatory Module Upgrades

Before initial release of NDI we were encountering many issues with Mandatory upgrades. As a result, the mandatory upgrade feature was disabled.
When we are ready to re-enable the feature, perform the following actions:

For both `hyperIO.go` and `module.xml`:
1. Search for sections organized like so:
    - Go example:
        ```golang
        ...
        // Commented code
        // Commented code
        // HYPCOD-912...
        Uncommented code
        Uncommented code
        ```
    - XML example:
        ```xml
        ...
        <!--
        Commented code block
        -->
        <!-- HYPCOD-912... -->
        <Uncommented code block>
        ```
2. Scan the commented code, delete the corresponding lines after the HYPCOD-912 tag
3. Delete the commented code and the HYPCOD-912 tag