# How to Add Support for New Laser/Stimulus Hardware (ABR + IC)

This page is a step-by-step tutorial for adding support for a new piece of optical (or other) stimulation hardware to FEATHER. It uses a worked, invented example throughout — a hypothetical new laser called **"Laser CoboltMLD520"** — to make each step concrete.

⚠️ The exact laser name, hardware string, and calibration filename patterns below are **illustrative only**, invented for this tutorial. Substitute the exact strings that appear in your own raw data and calibration files.

This tutorial covers the pattern used for **ABR** (`@berabr/stim.m`) and **single-unit** (`@sutr/stim.m`) recordings, which share an identical structure. IC (`icme`) recordings handle calibration slightly differently (calibration is resolved per-recording via `getCalibration`/`calculateCalibration` rather than via a switch-case in `stim.m`) — see Step 3 for the IC-specific part.

---

## Background: how hardware is currently recognised

For ABR and single-unit recordings, `stim.m` inspects the raw data field `Speaker` (fixed-width, padded with trailing spaces by the BERA software) and matches it against a hardcoded `switch`/`case` block:

```matlab
switch strtrim(bera_raw.IsData(1).Speaker)
    case strtrim('avisoft           ')
        ...
    case strtrim('Laser ObisTTL     ')
        ...
    case strtrim('LaserOxxiusMPA542')
        ...
end
```

Each `case` block fills in the same fixed set of fields in `B.Stim`:

| Field | Meaning |
|---|---|
| `anuTag` | ANU's numeric modality tag (`0` = acoustic, `1` = optical, `2` = electric) |
| `modality` | `'Acoustic'`, `'Optical'`, or `'Electric'` |
| `stimulusHardware` | Copy of the raw `Speaker` string |
| `mode` | Raw stimulus mode string |
| `unit` | Unit of the `intensity` field (e.g. `'dB SPL'`, `'mW'`, `'mV'`) |
| `duration`, `intensity`, `repRate` | Per-trace stimulus values, pulled from the corresponding raw fields |

If your hardware isn't in this list, `B.Stim` is left with mostly empty fields, and downstream calibration/threshold functions will fail or silently produce wrong units.

---

## Step-by-step: adding a new laser

### Step a) Find the exact hardware string in the raw data

Before writing any code, find out **exactly** how your new laser identifies itself in the raw BERA/ExpControl data. This must match character-for-character (including trailing spaces, for ABR) — a common source of bugs is guessing this string instead of checking it.

```matlab
enablecache off
B = loadRaw(berabr(ee, SeriesID, D));   % or inspect bera_raw.IsData(1).Speaker directly
disp(['[' B.R(1).Speaker ']'])          % square brackets make trailing spaces visible
```

For our worked example, suppose this prints:

```
[Laser CoboltMLD520]
```

⚠️ **NEEDS INPUT FROM MAINTAINERS:** please confirm whether newly written BERA hardware strings are still fixed-width/padded like the older entries (e.g. `'Laser ObisTTL     '`), or whether newer hardware integrations write untrimmed strings — this affects whether you need to pad your new `case` string to match.

### Step b) Add a new `case` block in `@berabr/stim.m`

Add a new case to the `switch` statement in `@berabr/stim.m`, following the exact pattern of the existing optical-laser cases:

```matlab
case strtrim('Laser CoboltMLD520')
    for ii = 1 : numel(bera_raw.IsData)
        S(ii).anuTag = 1;                                  % 1 = optical
        S(ii).modality = 'Optical';
        S(ii).stimulusHardware = bera_raw.IsData(1).Speaker;
        S(ii).mode = bera_raw.IsData(1).BERAStimMode;
        S(ii).unit = 'mW';
        S(ii).duration = bera_raw.IsData(ii).isIntensTimeValue1;
        S(ii).intensity = bera_raw.IsData(ii).Intens1;
        S(ii).repRate = bera_raw.IsData(ii).StimRepeatPerSec;
    end
    [temp_protocol] = local_IDR(S);
    for ii = 1 : numel(bera_raw.IsData)
        S(ii).protocol = temp_protocol;
    end
```

This is a direct copy of the pattern used for every existing optical laser (`Laser ObisTTL`, `Laser Oxxius`, `LaserOxxiusMPA542`, etc.) — the only things that typically differ between lasers are:
- the exact `case` string,
- `anuTag`/`modality` (only different for non-optical hardware, e.g. acoustic or electric),
- occasionally which raw field holds `intensity`/`duration`, if your hardware's ExpControl/BERA integration stores it differently.

If the same laser is also used for **single-unit** recordings, add the equivalent `case` block to `@sutr/stim.m` — the structure is identical, just operating on `sutrRaw.IsData` instead of `bera_raw.IsData`, and using `SUStimMode` instead of `BERAStimMode`.

### Step c) Make sure calibration file naming matches

FEATHER resolves optical intensities from calibration `.txt` files generated by the LaserControl software, following the pattern:

```
<date>_<laserType>_<ComPort>_Power_Calibration_<OD>.txt
```

For **ABR**, `setCalibrationBeraFromLaserControl.m` reads all matching calibration files in the raw ABR folder into a table, then branches on the hardware string entered by the user in the `userberabrOD` GUI to decide how to interpret the OD/filter value and which calibration rows to use:

```matlab
if contains(UT.data{ii,3},'Obis')
    ...
elseif contains(UT.data{ii,3},'L660')
    ...
elseif contains(UT.data{ii,3},'LaserOxxiusMPA542')
    ...
end
```

You need to add an equivalent branch for your new laser here too — this is a **separate step from Step b)**, because `stim.m` only extracts raw stimulus values, while `setCalibrationBeraFromLaserControl.m` is what actually converts them into calibrated mW values:

```matlab
elseif contains(UT.data{ii,3},'CoboltMLD520')
    ii_inC = find(contains(T_cal.laserType,'CoboltMLD520'));
    C.calib = T_cal.calibArray(ii_inC);
```

Then make sure your calibration `.txt` files are actually named so that `laserType` (parsed from the filename) contains the same string, e.g.:

```
20250110_CoboltMLD520_COM7_Power_Calibration_0.txt
```

For **IC** recordings, calibration is resolved via `getCalibration.m` (if ExpControl already used calibrated values) or `calculateCalibration.m` (if a polynomial fit from % to mW is needed). Both currently check:

```matlab
if contains(IC.Stim.exp_type,'OBIS') || contains(IC.Stim.exp_type,'OXXIUS') || contains(IC.Stim.exp_type,'DarkRedLaser')
```

If your new laser will be used for IC recordings and the corresponding ExpControl module name doesn't match one of these substrings, you'll need to add it to this condition as well, in both `getCalibration.m` and `calculateCalibration.m`.

⚠️ **NEEDS INPUT FROM MAINTAINERS:** the calibration-matching logic in `setCalibrationBeraFromLaserControl.m` and `calculateCalibration.m` is hardcoded per-laser rather than driven by a lookup table — please confirm whether this is intentional/acceptable for new hardware going forward, or whether a more general mechanism is planned, since every new laser currently requires editing multiple files in parallel (this page, `stim.m`, the calibration functions).

### Step d) Update the GUI if new hardware-specific inputs are needed

For most new lasers, **no GUI changes are required** — `userberabrOD.mlapp` automatically reads and displays whatever string is in the raw `Speaker` field in its "hardware" column (see [[GUI Reference|User-Guide-GUI-Reference]]), and the user just needs to enter the OD/filter value as free text. Similarly, `ICuserInput.mlapp`'s Laser/COMPort/Filter columns are free-text and don't need modification just because a new laser exists.

You only need to modify a `.mlapp` if your new hardware requires an **entirely new kind of input** that doesn't fit the existing columns (e.g. a laser with two independently-set wavelengths, or a fundamentally different calibration parameter). If that's the case, see [[How to Add a New GUI Input Field|Developer-Guide-Adding-GUI-Field]].

⚠️ **NEEDS INPUT FROM MAINTAINERS:** the `.mlapp` files are binary App Designer files and could not be inspected directly while writing this documentation — please review whether `userberabrOD.mlapp` or `ICuserInput.mlapp` have any hardcoded dropdown lists of hardware names (rather than free text) that would also need updating when a new laser is added.

### Step e) Test using the example scripts as templates

Once steps b)–c) are done, test your new hardware end-to-end using the existing example scripts as templates, rather than writing a new test from scratch:

- `exampleScripts/processing_new_ABR_data.m` — for ABR recordings using your new laser.
- `exampleScripts/processing_new_IC_data.m` — for IC recordings using your new laser.

Run through each script's ABR/IC-relevant steps with `enablecache off`, using a real recording made with your new hardware, and confirm:

1. `B.Stim.modality`, `.unit`, `.intensity` are populated as expected after `allBerabr(ee)` (ABR) or `allIcme(ee)` (IC) run.
2. `userberabrOD`/`ICuserInput` correctly display and accept your hardware's identifying information.
3. After running `setCalibrationBeraFromLaserControl(ee)` (ABR) or `getCalibration`/`calculateCalibration` (IC), `B.C.Ical` / `IC.C.stimlistCal` contain sensible calibrated mW values, not raw drive percentages.
4. Downstream functions that depend on calibrated intensity (e.g. `intensityThreshold`, `intensityThresholdIC`) run without errors and produce plausible values.

---
