Repository: mne-tools/mne-python

Issue:
`mne.Brain` fails to set renderer when blocking
### Description of the problem

We are attempting to create some visualization for some fnirs data and have found the existing visualizations functions to be insufficient for our needs. We, therefore, have been looking into creating some bespoke functions using the `mne.viz.Brain` class. 

While doing this, we found that when setting the `block` param in the `mne.viz.Brain` constructor, subsequent calls to methods of the Brain class fail, specifically `show_view`.

### Steps to reproduce

```Python
This snippet runs without issue

import mne

brain = mne.viz.Brain("sample", subjects_dir=subjects_dir)
brain.show_view(azimuth=190, elevation=70, distance=350, focalpoint=(0, 0, 20))

This snippet fails on the call to `show_view`
```python
import mne

brain = mne.viz.Brain("sample", subjects_dir=subjects_dir, block=True)
brain.show_view(azimuth=190, elevation=70, distance=350, focalpoint=(0, 0, 20))
```
```


### Link to data

n/a

### Expected results

Method calls to `Brain` objects created with the `block` param should not fail.

### Actual results

Method calls to `Brain` objects created with the `block` param fail. I expect that somewhere in this class that when `block` is set, once the window is closed, the renderer is cleaned up, so subsequent calls find that it is set to none. If we set the `show` param in the `Brain` constructor to False, then we again, don't receive this error.

### Additional information

```
Platform             Linux-6.8.9-arch1-1-x86_64-with-glibc2.39
Python               3.11.8 | packaged by conda-forge | (main, Feb 16 2024, 20:53:32) [GCC 12.3.0]
Executable           /home/alexk101/.mambaforge/envs/fnirs/bin/python
CPU                   (16 cores)
Memory               30.5 GB

Core
├☑ mne               1.8.0.dev33+g1c5b39ff1d9 (devel, latest release is 1.7.0)
├☑ numpy             1.26.4 (OpenBLAS 0.3.26 with 16 threads)
├☑ scipy             1.12.0
└☑ matplotlib        3.8.3 (backend=QtAgg)

Numerical (optional)
├☑ sklearn           1.4.1.post1
├☑ numba             0.59.0
├☑ nibabel           5.2.1
├☑ nilearn           0.10.3
├☑ pandas            2.0.3
├☑ h5io              0.2.2
├☑ h5py              3.10.0
└☐ unavailable       dipy, openmeeg, cupy

Visualization (optional)
├☑ pyvista           0.43.5 (OpenGL 4.5.0 NVIDIA 550.78 via NVIDIA GeForce RTX 3070 Ti Laptop GPU/PCIe/SSE2)
├☑ pyvistaqt         0.11.0
├☑ vtk               9.2.6
├☑ qtpy              2.4.1 (PyQt5=5.15.8)
└☐ unavailable       ipympl, pyqtgraph, mne-qt-browser, ipywidgets, trame_client, trame_server, trame_vtk, trame_vuetify

Ecosystem (optional)
├☑ mne-nirs          0.7.0.dev0
└☐ unavailable       mne-bids, mne-features, mne-connectivity, mne-icalabel, mne-bids-pipeline, neo, eeglabio, edfio, mffpy, pybv
```

Discussion:
drammock: test failure is Windows pip-pre (seems unrelated):

```
FAILED mne/decoding/tests/test_base.py::test_get_coef_multiclass_full[4-3-2] - pytest.PytestUnhandledThreadExceptionWarning: Exception in thread Thread-20

Traceback (most recent call last):
  File "C:\hostedtoolcache\windows\Python\3.12.4\x64\Lib\threading.py", line 1073, in _bootstrap_inner
    self.run()
  File "D:\a\1\s\mne\utils\progressbar.py", line 196, in run
    self._mne_pb.update(self._mne_pb._mmap.sum())
  File "D:\a\1\s\mne\utils\progressbar.py", line 124, in update
    self.update_with_increment_value(cur_value - self._tqdm.n)
  File "D:\a\1\s\mne\utils\progressbar.py", line 136, in update_with_increment_value
    self._tqdm.update(increment_value)
  File "C:\hostedtoolcache\windows\Python\3.12.4\x64\Lib\site-packages\tqdm\std.py", line 1241, in update
    self._ema_dt(dt)
TypeError: tqdm.close.<locals>.<lambda>() takes 0 positional arguments but 1 was given
```

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 77fdc9abf..c0ab9aac2 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,7 @@
 repos:
   # Ruff mne
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.5.1
+    rev: v0.5.2
     hooks:
       - id: ruff
         name: ruff lint mne
diff --git a/doc/changes/devel/12300.apichange.rst b/doc/changes/devel/12300.apichange.rst
new file mode 100644
index 000000000..f9d082a4b
--- /dev/null
+++ b/doc/changes/devel/12300.apichange.rst
@@ -0,0 +1,3 @@
+A new argument ``events_as_annotations`` has been added to :func:`mne.io.read_raw_egi`
+with a default value of ``False`` that will change to ``True`` in version 1.9, by
+`Scott Huberty`_ and `Eric Larson`_.
diff --git a/doc/changes/devel/12300.bugfix.rst b/doc/changes/devel/12300.bugfix.rst
new file mode 100644
index 000000000..c5c167d01
--- /dev/null
+++ b/doc/changes/devel/12300.bugfix.rst
@@ -0,0 +1,1 @@
+Fix bug where an event that occurred only once was excluded in :func:`mne.io.read_raw_egi`, by :newcontrib:`Ping-Keng Jao`.
diff --git a/doc/changes/devel/12719.apichange.rst b/doc/changes/devel/12719.apichange.rst
new file mode 100644
index 000000000..d1512070f
--- /dev/null
+++ b/doc/changes/devel/12719.apichange.rst
@@ -0,0 +1,2 @@
+The ``block`` argument to :class:`mne.viz.Brain` is deprecated and will be removed in
+1.9, use :func:`matplotlib.pyplot.show` with ``block=True`` instead, by `Eric Larson`_.
\ No newline at end of file
diff --git a/doc/changes/names.inc b/doc/changes/names.inc
index c6f35fade..d54368e06 100644
--- a/doc/changes/names.inc
+++ b/doc/changes/names.inc
@@ -454,6 +454,8 @@
 
 .. _Pierre-Antoine Bannier: https://github.com/PABannier
 
+.. _Ping-Keng Jao: https://github.com/nafraw
+
 .. _Proloy Das: https://github.com/proloyd
 
 .. _Qian Chu: https://github.com/qian-chu
diff --git a/mne/channels/channels.py b/mne/channels/channels.py
index 4470f91a3..0cb3e2984 100644
--- a/mne/channels/channels.py
+++ b/mne/channels/channels.py
@@ -260,7 +260,7 @@ def unify_bad_channels(insts):
     valid_types = (BaseRaw, Epochs, Evoked, BaseSpectrum)
     for inst in insts:
         _validate_type(inst, valid_types, "each object in insts")
-        if type(inst) != inst_type:
+        if type(inst) is not inst_type:
             raise ValueError("All insts must be the same type")
 
     # ensure all insts have the same channels and channel order
diff --git a/mne/io/egi/egi.py b/mne/io/egi/egi.py
index e95577f86..980b6a7b5 100644
--- a/mne/io/egi/egi.py
+++ b/mne/io/egi/egi.py
@@ -13,10 +13,11 @@ import numpy as np
 from ..._fiff.constants import FIFF
 from ..._fiff.meas_info import _empty_info
 from ..._fiff.utils import _create_chs, _read_segments_file
+from ...annotations import Annotations
 from ...utils import _check_fname, _validate_type, logger, verbose, warn
 from ..base import BaseRaw
 from .egimff import _read_raw_egi_mff
-from .events import _combine_triggers
+from .events import _combine_triggers, _triage_include_exclude
 
 
 def _read_header(fid):
@@ -101,13 +102,12 @@ def read_raw_egi(
     exclude=None,
     preload=False,
     channel_naming="E%d",
+    *,
+    events_as_annotations=None,
     verbose=None,
 ) -> "RawEGI":
     """Read EGI simple binary as raw object.
 
-    .. note:: This function attempts to create a synthetic trigger channel.
-              See the Notes section below.
-
     Parameters
     ----------
     input_fname : path-like
@@ -120,14 +120,13 @@ def read_raw_egi(
         Names of channels or list of indices that should be designated
         MISC channels. Default is None.
     include : None | list
-       The event channels to be ignored when creating the synthetic
-       trigger. Defaults to None.
+       The event channels to be included when creating the synthetic
+       trigger or annotations. Defaults to None.
        Note. Overrides ``exclude`` parameter.
     exclude : None | list
        The event channels to be ignored when creating the synthetic
-       trigger. Defaults to None. If None, channels that have more than
-       one event and the ``sync`` and ``TREV`` channels will be
-       ignored.
+       trigger or annotations. Defaults to None. If None, the ``sync`` and ``TREV``
+       channels will be ignored. This is ignored when ``include`` is not None.
     %(preload)s
 
         .. versionadded:: 0.11
@@ -135,8 +134,15 @@ def read_raw_egi(
         Channel naming convention for the data channels. Defaults to ``'E%%d'``
         (resulting in channel names ``'E1'``, ``'E2'``, ``'E3'``...). The
         effective default prior to 0.14.0 was ``'EEG %%03d'``.
+        .. versionadded:: 0.14.0
 
-         .. versionadded:: 0.14.0
+    events_as_annotations : bool
+        If True, annotations are created from experiment events. If False (default),
+        a synthetic trigger channel ``STI 014`` is created from experiment events.
+        See the Notes section for details.
+        The default will change from False to True in version 1.9.
+
+        .. versionadded:: 1.8.0
     %(verbose)s
 
     Returns
@@ -151,27 +157,51 @@ def read_raw_egi(
 
     Notes
     -----
-    The trigger channel names are based on the arbitrary user dependent event
-    codes used. However this function will attempt to generate a **synthetic
-    trigger channel** named ``STI 014`` in accordance with the general
-    Neuromag / MNE naming pattern.
-
-    The event_id assignment equals ``np.arange(n_events) + 1``. The resulting
-    ``event_id`` mapping is stored as attribute to the resulting raw object but
-    will be ignored when saving to a fiff. Note. The trigger channel is
-    artificially constructed based on timestamps received by the Netstation.
-    As a consequence, triggers have only short durations.
-
-    This step will fail if events are not mutually exclusive.
+    When ``events_from_annotations=True``, event codes on stimulus channels like
+    ``DIN1`` are stored as annotations with the ``description`` set to the stimulus
+    channel name.
+
+    When ``events_from_annotations=False`` and events are present on the included
+    stimulus channels, a new stim channel ``STI014`` will be synthesized from the
+    events. It will contain 1-sample pulses where the Netstation file had event
+    timestamps. A ``raw.event_id`` dictionary is added to the raw object that will have
+    arbitrary sequential integer IDs for the events. This will fail if any timestamps
+    are duplicated. The ``event_id`` will also not survive a save/load roundtrip.
+
+    For these reasons, it is recommended to use ``events_as_annotations=True``.
     """
     _validate_type(input_fname, "path-like", "input_fname")
     input_fname = str(input_fname)
+    if events_as_annotations is None:
+        warn(
+            "events_as_annotations defaults to False in 1.8 but will change to "
+            "True in 1.9, set it explicitly to avoid this warning",
+            FutureWarning,
+        )
+        events_as_annotations = False
+
     if input_fname.rstrip("/\\").endswith(".mff"):  # allows .mff or .mff/
         return _read_raw_egi_mff(
-            input_fname, eog, misc, include, exclude, preload, channel_naming, verbose
+            input_fname,
+            eog,
+            misc,
+            include,
+            exclude,
+            preload,
+            channel_naming,
+            events_as_annotations=events_as_annotations,
+            verbose=verbose,
         )
     return RawEGI(
-        input_fname, eog, misc, include, exclude, preload, channel_naming, verbose
+        input_fname,
+        eog,
+        misc,
+        include,
+        exclude,
+        preload,
+        channel_naming,
+        events_as_annotations=events_as_annotations,
+        verbose=verbose,
     )
 
 
@@ -190,6 +220,8 @@ class RawEGI(BaseRaw):
         exclude=None,
         preload=False,
         channel_naming="E%d",
+        *,
+        events_as_annotations=True,
         verbose=None,
     ):
         input_fname = str(_check_fname(input_fname, "read", True, "input_fname"))
@@ -209,58 +241,18 @@ class RawEGI(BaseRaw):
 
         logger.info("    Assembling measurement info ...")
 
-        event_codes = []
-        if egi_info["n_events"] > 0:
-            event_codes = list(egi_info["event_codes"])
-            if include is None:
-                exclude_list = ["sync", "TREV"] if exclude is None else exclude
-                exclude_inds = [
-                    i for i, k in enumerate(event_codes) if k in exclude_list
-                ]
-                more_excludes = []
-                if exclude is None:
-                    for ii, event in enumerate(egi_events):
-                        if event.sum() <= 1 and event_codes[ii]:
-                            more_excludes.append(ii)
-                if len(exclude_inds) + len(more_excludes) == len(event_codes):
-                    warn(
-                        "Did not find any event code with more than one event.",
-                        RuntimeWarning,
-                    )
-                else:
-                    exclude_inds.extend(more_excludes)
-
-                exclude_inds.sort()
-                include_ = [
-                    i for i in np.arange(egi_info["n_events"]) if i not in exclude_inds
-                ]
-                include_names = [k for i, k in enumerate(event_codes) if i in include_]
-            else:
-                include_ = [i for i, k in enumerate(event_codes) if k in include]
-                include_names = include
-
-            for kk, v in [("include", include_names), ("exclude", exclude)]:
-                if isinstance(v, list):
-                    for k in v:
-                        if k not in event_codes:
-                            raise ValueError(f'Could find event named "{k}"')
-                elif v is not None:
-                    raise ValueError(f"`{kk}` must be None or of type list")
-
-            event_ids = np.arange(len(include_)) + 1
+        event_codes = egi_info["event_codes"]
+        include = _triage_include_exclude(include, exclude, egi_events, egi_info)
+        if egi_info["n_events"] > 0 and not events_as_annotations:
+            event_ids = np.arange(len(include)) + 1
             logger.info('    Synthesizing trigger channel "STI 014" ...')
-            excl_events = ", ".join(
-                k for i, k in enumerate(event_codes) if i not in include_
-            )
-            logger.info(f"    Excluding events {{{excl_events}}} ...")
             egi_info["new_trigger"] = _combine_triggers(
-                egi_events[include_], remapping=event_ids
+                egi_events[[e in include for e in event_codes]], remapping=event_ids
             )
             self.event_id = dict(
-                zip([e for e in event_codes if e in include_names], event_ids)
+                zip([e for e in event_codes if e in include], event_ids)
             )
         else:
-            # No events
             self.event_id = None
             egi_info["new_trigger"] = None
         info = _empty_info(egi_info["samp_rate"])
@@ -275,11 +267,12 @@ class RawEGI(BaseRaw):
         my_timestamp = time.mktime(my_time.timetuple())
         info["meas_date"] = (my_timestamp, 0)
         ch_names = [channel_naming % (i + 1) for i in range(egi_info["n_channels"])]
-        ch_names.extend(list(egi_info["event_codes"]))
+        cals = np.repeat(cal, len(ch_names))
+        ch_names.extend(list(event_codes))
+        cals = np.concatenate([cals, np.ones(egi_info["n_events"])])
         if egi_info["new_trigger"] is not None:
             ch_names.append("STI 014")  # our new_trigger
-        nchan = len(ch_names)
-        cals = np.repeat(cal, nchan)
+            cals = np.concatenate([cals, [1.0]])
         ch_coil = FIFF.FIFFV_COIL_EEG
         ch_kind = FIFF.FIFFV_EEG_CH
         chs = _create_chs(ch_names, cals, ch_coil, ch_kind, eog, (), (), misc)
@@ -292,7 +285,6 @@ class RawEGI(BaseRaw):
             chs[idx].update(
                 {
                     "unit_mul": FIFF.FIFF_UNITM_NONE,
-                    "cal": 1.0,
                     "kind": FIFF.FIFFV_STIM_CH,
                     "coil_type": FIFF.FIFFV_COIL_NONE,
                     "unit": FIFF.FIFF_UNIT_NONE,
@@ -314,6 +306,17 @@ class RawEGI(BaseRaw):
             raw_extras=[egi_info],
             verbose=verbose,
         )
+        if events_as_annotations:
+            annot = dict(onset=list(), duration=list(), description=list())
+            for code, row in zip(egi_info["event_codes"], egi_events):
+                if code not in include:
+                    continue
+                onset = np.where(row)[0] / self.info["sfreq"]
+                annot["onset"].extend(onset)
+                annot["duration"].extend([0.0] * len(onset))
+                annot["description"].extend([code] * len(onset))
+            if annot:
+                self.set_annotations(Annotations(**annot))
 
     def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
         """Read a segment of data from a file."""
diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py
index b8093a5e9..1b76ff60c 100644
--- a/mne/io/egi/egimff.py
+++ b/mne/io/egi/egimff.py
@@ -20,7 +20,7 @@ from ...channels.montage import make_dig_montage
 from ...evoked import EvokedArray
 from ...utils import _check_fname, _check_option, _soft_import, logger, verbose, warn
 from ..base import BaseRaw
-from .events import _combine_triggers, _read_events
+from .events import _combine_triggers, _read_events, _triage_include_exclude
 from .general import (
     _block_r,
     _extract,
@@ -351,68 +351,21 @@ def _read_raw_egi_mff(
     exclude=None,
     preload=False,
     channel_naming="E%d",
+    *,
+    events_as_annotations=False,
     verbose=None,
 ):
-    """Read EGI mff binary as raw object.
-
-    .. note:: This function attempts to create a synthetic trigger channel.
-              See notes below.
-
-    Parameters
-    ----------
-    input_fname : path-like
-        Path to the raw file.
-    eog : list or tuple
-        Names of channels or list of indices that should be designated
-        EOG channels. Default is None.
-    misc : list or tuple
-        Names of channels or list of indices that should be designated
-        MISC channels. Default is None.
-    include : None | list
-       The event channels to be ignored when creating the synthetic
-       trigger. Defaults to None.
-       Note. Overrides `exclude` parameter.
-    exclude : None | list
-       The event channels to be ignored when creating the synthetic
-       trigger. Defaults to None. If None, channels that have more than
-       one event and the ``sync`` and ``TREV`` channels will be
-       ignored.
-    %(preload)s
-    channel_naming : str
-        Channel naming convention for the data channels. Defaults to 'E%%d'
-        (resulting in channel names 'E1', 'E2', 'E3'...). The effective default
-        prior to 0.14.0 was 'EEG %%03d'.
-    %(verbose)s
-
-    Returns
-    -------
-    raw : instance of RawMff
-        A Raw object containing EGI mff data.
-        See :class:`mne.io.Raw` for documentation of attributes and methods.
-
-    Notes
-    -----
-    The trigger channel names are based on the arbitrary user dependent event
-    codes used. However this function will attempt to generate a synthetic
-    trigger channel named ``STI 014`` in accordance with the general
-    Neuromag / MNE naming pattern.
-
-    The event_id assignment equals ``np.arange(n_events) + 1``. The resulting
-    ``event_id`` mapping is stored as attribute to the resulting raw object but
-    will be ignored when saving to a fiff. Note. The trigger channel is
-    artificially constructed based on timestamps received by the Netstation.
-    As a consequence, triggers have only short durations.
-
-    This step will fail if events are not mutually exclusive.
-
-    See Also
-    --------
-    mne.io.Raw : Documentation of attributes and methods of RawMff.
-
-    .. versionadded:: 0.15.0
-    """
+    """Read EGI mff binary as raw object."""
     return RawMff(
-        input_fname, eog, misc, include, exclude, preload, channel_naming, verbose
+        input_fname,
+        eog,
+        misc,
+        include,
+        exclude,
+        preload,
+        channel_naming,
+        events_as_annotations=events_as_annotations,
+        verbose=verbose,
     )
 
 
@@ -431,6 +384,8 @@ class RawMff(BaseRaw):
         exclude=None,
         preload=False,
         channel_naming="E%d",
+        *,
+        events_as_annotations=True,
         verbose=None,
     ):
         """Init the RawMff class."""
@@ -451,75 +406,36 @@ class RawMff(BaseRaw):
             misc = np.where(np.array(egi_info["chan_type"]) != "eeg")[0].tolist()
 
         logger.info("    Reading events ...")
-        egi_events, egi_info = _read_events(input_fname, egi_info)
+        egi_events, egi_info, mff_events = _read_events(input_fname, egi_info)
         cals = _get_eeg_calibration_info(input_fname, egi_info)
         logger.info("    Assembling measurement info ...")
-        if egi_info["n_events"] > 0:
-            event_codes = list(egi_info["event_codes"])
-            if include is None:
-                exclude_list = ["sync", "TREV"] if exclude is None else exclude
-                exclude_inds = [
-                    i for i, k in enumerate(event_codes) if k in exclude_list
-                ]
-                more_excludes = []
-                if exclude is None:
-                    for ii, event in enumerate(egi_events):
-                        if event.sum() <= 1 and event_codes[ii]:
-                            more_excludes.append(ii)
-                if len(exclude_inds) + len(more_excludes) == len(event_codes):
-                    warn(
-                        "Did not find any event code with more than one event.",
-                        RuntimeWarning,
-                    )
-                else:
-                    exclude_inds.extend(more_excludes)
-
-                exclude_inds.sort()
-                include_ = [
-                    i for i in np.arange(egi_info["n_events"]) if i not in exclude_inds
-                ]
-                include_names = [k for i, k in enumerate(event_codes) if i in include_]
-            else:
-                include_ = [i for i, k in enumerate(event_codes) if k in include]
-                include_names = include
-
-            for kk, v in [("include", include_names), ("exclude", exclude)]:
-                if isinstance(v, list):
-                    for k in v:
-                        if k not in event_codes:
-                            raise ValueError(f"Could not find event named {repr(k)}")
-                elif v is not None:
-                    raise ValueError(f"`{kk}` must be None or of type list")
+        event_codes = egi_info["event_codes"]
+        include = _triage_include_exclude(include, exclude, egi_events, egi_info)
+        if egi_info["n_events"] > 0 and not events_as_annotations:
             logger.info('    Synthesizing trigger channel "STI 014" ...')
-            excl_events = ", ".join(
-                k for i, k in enumerate(event_codes) if i not in include_
-            )
-            logger.info(f"    Excluding events {{{excl_events}}} ...")
-            if all(ch.startswith("D") for ch in include_names):
+            if all(ch.startswith("D") for ch in include):
                 # support the DIN format DIN1, DIN2, ..., DIN9, DI10, DI11, ... DI99,
                 # D100, D101, ..., D255 that we get when sending 0-255 triggers on a
                 # parallel port.
                 events_ids = list()
-                for ch in include_names:
+                for ch in include:
                     while not ch[0].isnumeric():
                         ch = ch[1:]
                     events_ids.append(int(ch))
             else:
-                events_ids = np.arange(len(include_)) + 1
+                events_ids = np.arange(len(include)) + 1
             egi_info["new_trigger"] = _combine_triggers(
-                egi_events[include_], remapping=events_ids
+                egi_events[[c in include for c in event_codes]], remapping=events_ids
             )
             self.event_id = dict(
-                zip([e for e in event_codes if e in include_names], events_ids)
+                zip([e for e in event_codes if e in include], events_ids)
             )
             if egi_info["new_trigger"] is not None:
                 egi_events = np.vstack([egi_events, egi_info["new_trigger"]])
-            assert egi_events.shape[1] == egi_info["last_samps"][-1]
         else:
-            # No events
             self.event_id = None
             egi_info["new_trigger"] = None
-            event_codes = []
+        assert egi_events.shape[1] == egi_info["last_samps"][-1]
 
         meas_dt_utc = egi_info["meas_dt_local"].astimezone(datetime.timezone.utc)
         info = _empty_info(egi_info["sfreq"])
@@ -531,15 +447,16 @@ class RawMff(BaseRaw):
         ch_names, mon = _read_locs(input_fname, egi_info, channel_naming)
         # Second: Stim
         ch_names.extend(list(egi_info["event_codes"]))
+        n_extra = len(event_codes) + len(misc) + len(eog) + len(egi_info["pns_names"])
         if egi_info["new_trigger"] is not None:
             ch_names.append("STI 014")  # channel for combined events
-        cals = np.concatenate(
-            [cals, np.repeat(1, len(event_codes) + 1 + len(misc) + len(eog))]
-        )
+            n_extra += 1
 
         # Third: PNS
         ch_names.extend(egi_info["pns_names"])
-        cals = np.concatenate([cals, np.repeat(1, len(egi_info["pns_names"]))])
+
+        cals = np.concatenate([cals, np.ones(n_extra)])
+        assert len(cals) == len(ch_names), (len(cals), len(ch_names))
 
         # Actually create channels as EEG, then update stim and PNS
         ch_coil = FIFF.FIFFV_COIL_EEG
@@ -608,6 +525,7 @@ class RawMff(BaseRaw):
         last_samps = [egi_info["last_samps"][-1] - 1]
 
         annot = dict(onset=list(), duration=list(), description=list())
+
         if len(idx["pns"]):
             # PNS Data is present and should be read:
             egi_info["pns_filepath"] = op.join(input_fname, egi_info["pns_fname"])
@@ -650,8 +568,17 @@ class RawMff(BaseRaw):
                 annot["duration"].append(gap / egi_info["sfreq"])
                 annot["description"].append("BAD_ACQ_SKIP")
 
+        # create events from annotations
+        if events_as_annotations:
+            for code, samples in mff_events.items():
+                if code not in include:
+                    continue
+                annot["onset"].extend(np.array(samples) / egi_info["sfreq"])
+                annot["duration"].extend([0.0] * len(samples))
+                annot["description"].extend([code] * len(samples))
+
         if len(annot["onset"]):
-            self.set_annotations(Annotations(**annot, orig_time=None))
+            self.set_annotations(Annotations(**annot))
 
     def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
         """Read a chunk of data."""
diff --git a/mne/io/egi/events.py b/mne/io/egi/events.py
index 500848ee7..35f305e0a 100644
--- a/mne/io/egi/events.py
+++ b/mne/io/egi/events.py
@@ -8,7 +8,7 @@ from os.path import basename, join, splitext
 
 import numpy as np
 
-from ...utils import _soft_import, logger
+from ...utils import _soft_import, _validate_type, logger, warn
 
 
 def _read_events(input_fname, info):
@@ -31,7 +31,7 @@ def _read_events(input_fname, info):
             if (i < 0) or (i >= events.shape[1]):
                 continue
             events[n][i] = n + 1
-    return events, info
+    return events, info, mff_events
 
 
 def _read_mff_events(filename, sfreq):
@@ -168,3 +168,39 @@ def _combine_triggers(data, remapping=None):
         if np.any(idx):
             new_trigger[idx] += event_id
     return new_trigger
+
+
+def _triage_include_exclude(include, exclude, egi_events, egi_info):
+    """Triage include and exclude."""
+    _validate_type(exclude, (list, None), "exclude")
+    _validate_type(include, (list, None), "include")
+    event_codes = list(egi_info["event_codes"])
+    for name, lst in dict(exclude=exclude, include=include).items():
+        for ii, item in enumerate(lst or []):
+            what = f"{name}[{ii}]"
+            _validate_type(item, str, what)
+            if item not in event_codes:
+                raise ValueError(
+                    f"Could not find event channel named {what}={repr(item)}"
+                )
+    if include is None:
+        if exclude is None:
+            default_exclude = ["sync", "TREV"]
+            exclude = [code for code in default_exclude if code in event_codes]
+            for code, event in zip(event_codes, egi_events):
+                if event.sum() < 1 and code:
+                    exclude.append(code)
+            if (
+                len(exclude) == len(event_codes)
+                and egi_info["n_events"]
+                and set(exclude) - set(default_exclude)
+            ):
+                warn(
+                    "Did not find any event code with at least one event.",
+                    RuntimeWarning,
+                )
+        include = [k for k in event_codes if k not in exclude]
+    del exclude
+    excl_events = ", ".join(k for k in event_codes if k not in include)
+    logger.info(f"    Excluding events {{{excl_events}}} ...")
+    return include
diff --git a/mne/source_estimate.py b/mne/source_estimate.py
index 7ddffa920..772851483 100644
--- a/mne/source_estimate.py
+++ b/mne/source_estimate.py
@@ -479,7 +479,7 @@ def _make_stc(
 def _verify_source_estimate_compat(a, b):
     """Make sure two SourceEstimates are compatible for arith. operations."""
     compat = False
-    if type(a) != type(b):
+    if type(a) is not type(b):
         raise ValueError(f"Cannot combine {type(a)} and {type(b)}.")
     if len(a.vertices) == len(b.vertices):
         if all(np.array_equal(av, vv) for av, vv in zip(a.vertices, b.vertices)):
diff --git a/mne/utils/numerics.py b/mne/utils/numerics.py
index df3da8343..8cffd119b 100644
--- a/mne/utils/numerics.py
+++ b/mne/utils/numerics.py
@@ -761,7 +761,7 @@ def object_diff(a, b, pre="", *, allclose=False):
     """
     pd = _check_pandas_installed(strict=False)
     out = ""
-    if type(a) != type(b):
+    if type(a) is not type(b):
         # Deal with NamedInt and NamedFloat
         for sub in (int, float):
             if isinstance(a, sub) and isinstance(b, sub):
diff --git a/mne/utils/progressbar.py b/mne/utils/progressbar.py
index b1938c2fa..b8b4e6722 100644
--- a/mne/utils/progressbar.py
+++ b/mne/utils/progressbar.py
@@ -133,7 +133,10 @@ class ProgressBar:
             will be computed as
             ``(self.cur_value + increment_value / max_value) * 100``.
         """
-        self._tqdm.update(increment_value)
+        try:
+            self._tqdm.update(increment_value)
+        except TypeError:  # can happen during GC on Windows
+            pass
 
     def __iter__(self):
         """Iterate to auto-increment the pbar with 1."""
diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py
index 7d5c85881..34ed7608b 100644
--- a/mne/viz/_brain/_brain.py
+++ b/mne/viz/_brain/_brain.py
@@ -180,7 +180,8 @@ class Brain:
     show : bool
         Display the window as soon as it is ready. Defaults to True.
     block : bool
-        If True, start the Qt application event loop. Default to False.
+        Deprecated and will be removed in 1.9, do not use. Consider using
+        :func:`matplotlib.pyplot.show` with ``block=True`` instead.
 
     Attributes
     ----------
@@ -304,7 +305,7 @@ class Brain:
         silhouette=False,
         theme=None,
         show=True,
-        block=False,
+        block=None,
     ):
         from ..backends.renderer import _get_renderer, backend
 
@@ -357,6 +358,11 @@ class Brain:
         subjects_dir = get_subjects_dir(subjects_dir)
         if subjects_dir is not None:
             subjects_dir = str(subjects_dir)
+        if block is not None:
+            warn(
+                "block is deprecated and will be removed in 1.9, use "
+                "plt.show(block=True) instead"
+            )
 
         self.time_viewer = False
         self._hash = time.time_ns()
diff --git a/tutorials/preprocessing/90_eyetracking_data.py b/tutorials/preprocessing/90_eyetracking_data.py
index 6c44fb29f..bad4eeeda 100644
--- a/tutorials/preprocessing/90_eyetracking_data.py
+++ b/tutorials/preprocessing/90_eyetracking_data.py
@@ -37,7 +37,7 @@ et_fpath = data_path() / "eeg-et" / "sub-01_task-plr_eyetrack.asc"
 eeg_fpath = data_path() / "eeg-et" / "sub-01_task-plr_eeg.mff"
 
 raw_et = mne.io.read_raw_eyelink(et_fpath, create_annotations=["blinks"])
-raw_eeg = mne.io.read_raw_egi(eeg_fpath, preload=True, verbose="warning")
+raw_eeg = mne.io.read_raw_egi(eeg_fpath, events_as_annotations=True).load_data()
 raw_eeg.filter(1, 30)
 
 # %%


Test patch:
diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py
index 706d830a5..3c756e726 100644
--- a/mne/channels/tests/test_montage.py
+++ b/mne/channels/tests/test_montage.py
@@ -1074,7 +1074,11 @@ def test_egi_dig_montage(tmp_path):
     )
 
     # Test accuracy and embedding within raw object
-    raw_egi = read_raw_egi(egi_raw_fname, channel_naming="EEG %03d")
+    raw_egi = read_raw_egi(
+        egi_raw_fname,
+        channel_naming="EEG %03d",
+        events_as_annotations=True,
+    )
 
     raw_egi.set_montage(dig_montage)
     test_raw_egi = read_raw_fif(egi_fif_fname)
diff --git a/mne/io/egi/tests/test_egi.py b/mne/io/egi/tests/test_egi.py
index 71120d8d6..2f680fd7b 100644
--- a/mne/io/egi/tests/test_egi.py
+++ b/mne/io/egi/tests/test_egi.py
@@ -15,7 +15,7 @@ import pytest
 from numpy.testing import assert_allclose, assert_array_equal
 from scipy import io as sio
 
-from mne import find_events, pick_types
+from mne import events_from_annotations, find_events, pick_types
 from mne._fiff.constants import FIFF
 from mne.datasets.testing import data_path, requires_testing_data
 from mne.io import read_evokeds_mff, read_raw_egi, read_raw_fif
@@ -58,6 +58,9 @@ egi_eprime_pause_skips = [(1344000.0, 1804000.0)]
 egi_pause_w1337_events = None
 egi_pause_w1337_skips = [(21956000.0, 40444000.0), (60936000.0, 89332000.0)]
 
+# TODO: Remove once complete deprecation / FutureWarning about events_as_annonations
+pytestmark = pytest.mark.filterwarnings("ignore:.*events_as_annotation.*:FutureWarning")
+
 
 @requires_testing_data
 @pytest.mark.parametrize(
@@ -142,7 +145,8 @@ def test_egi_mff_pause_chunks(fname, tmp_path):
 
 
 @requires_testing_data
-def test_io_egi_mff():
+@pytest.mark.parametrize("events_as_annotations", (True, False))
+def test_io_egi_mff(events_as_annotations):
     """Test importing EGI MFF simple binary files."""
     pytest.importorskip("defusedxml")
     # want vars for n chans
@@ -150,7 +154,7 @@ def test_io_egi_mff():
     n_eeg = 128
     n_card = 3
 
-    raw = read_raw_egi(egi_mff_fname, include=None)
+    raw = read_raw_egi(egi_mff_fname, events_as_annotations=events_as_annotations)
     assert "RawMff" in repr(raw)
     assert raw.orig_format == "single"
     include = ["DIN1", "DIN2", "DIN3", "DIN4", "DIN5", "DIN7"]
@@ -160,6 +164,7 @@ def test_io_egi_mff():
         include=include,
         channel_naming="EEG %03d",
         test_scaling=False,  # XXX probably some bug
+        events_as_annotations=events_as_annotations,
     )
     assert raw.info["sfreq"] == 1000.0
     assert len(raw.info["dig"]) == n_card + n_eeg + n_ref
@@ -183,21 +188,43 @@ def test_io_egi_mff():
     # test our custom channel naming logic functionality
     eeg_chan = [c for c in raw.ch_names if "EEG" in c]
     assert len(eeg_chan) == n_eeg  # 128: VREF will not match in comprehension
-    assert "STI 014" in raw.ch_names
+    if events_as_annotations:
+        assert "STI 014" not in raw.ch_names
+        assert raw.event_id is None
+        event_id = {"DIN1": 1, "DIN2": 2, "DIN3": 3, "DIN4": 4, "DIN5": 5, "DIN7": 7}
+        events, _ = events_from_annotations(raw, event_id=event_id)
+    else:
+        assert "STI 014" in raw.ch_names
+        events = find_events(raw, stim_channel="STI 014")
+        event_id = raw.event_id
 
-    events = find_events(raw, stim_channel="STI 014")
     assert len(events) == 8
     assert np.unique(events[:, 1])[0] == 0
     assert np.unique(events[:, 0])[0] != 0
     assert np.unique(events[:, 2])[0] != 0
+    assert "DIN1" in event_id
 
     with pytest.raises(ValueError, match="Could not find event"):
         read_raw_egi(egi_mff_fname, include=["Foo"])
     with pytest.raises(ValueError, match="Could not find event"):
         read_raw_egi(egi_mff_fname, exclude=["Bar"])
     for ch in include:
-        assert ch in raw.event_id
-        assert raw.event_id[ch] == int(ch[-1])
+        assert ch in event_id
+        assert event_id[ch] == int(ch[-1])
+    # test converting stim triggers to annotations
+    if events_as_annotations:
+        # Grab the first annotation. Should be the first "DIN1" event.
+        assert len(raw.annotations)
+        onset, dur, desc, _ = raw.annotations[0].values()
+        assert_allclose(onset, 2.438)
+        assert np.isclose(dur, 0)
+        assert desc == "DIN1"
+        # grab the DIN1 channel
+        din1 = raw.get_data(picks="DIN1")
+        # Check that the time in sec of first event is the same as the first annotation
+        pin_hi_idx = np.where(din1 == 1)[1]
+        pin_hi_sec = pin_hi_idx / raw.info["sfreq"]
+        assert np.isclose(pin_hi_sec[0], onset)
 
 
 def test_io_egi():
@@ -209,12 +236,10 @@ def test_io_egi():
     data = data[1:]
     data *= 1e-6  # µV
 
-    with pytest.warns(RuntimeWarning, match="Did not find any event code"):
-        raw = read_raw_egi(egi_fname, include=None)
+    raw = read_raw_egi(egi_fname, events_as_annotations=False)
 
     # The reader should accept a Path, too.
-    with pytest.warns(RuntimeWarning, match="Did not find any event code"):
-        raw = read_raw_egi(Path(egi_fname), include=None)
+    raw_annot = read_raw_egi(Path(egi_fname), events_as_annotations=True)
 
     assert "RawEGI" in repr(raw)
     data_read, t_read = raw[:256]
@@ -251,11 +276,18 @@ def test_io_egi():
     new_trigger = _combine_triggers(triggers, events_ids)
     assert_array_equal(np.unique(new_trigger), np.unique([0, 12, 24]))
 
-    pytest.raises(ValueError, read_raw_egi, egi_fname, include=["Foo"], preload=False)
-    pytest.raises(ValueError, read_raw_egi, egi_fname, exclude=["Bar"], preload=False)
+    with pytest.raises(ValueError, match="Could not find.*include.*"):
+        read_raw_egi(egi_fname, include=["Foo"])
+    with pytest.raises(ValueError, match="Could not find.*exclude.*"):
+        read_raw_egi(egi_fname, exclude=["Bar"])
     for ii, k in enumerate(include, 1):
         assert k in raw.event_id
         assert raw.event_id[k] == ii
+    assert raw_annot.event_id is None
+    events, event_id = events_from_annotations(raw_annot, event_id=raw.event_id)
+    assert event_id == raw.event_id
+    events_2 = find_events(raw)
+    assert_array_equal(events, events_2)
 
 
 @requires_testing_data
@@ -371,7 +403,6 @@ def test_io_egi_crop_no_preload():
     assert_allclose(raw._data, raw_preload._data)
 
 
-@pytest.mark.filterwarnings("ignore::FutureWarning")
 @requires_testing_data
 @pytest.mark.parametrize(
     "idx, cond, tmax, signals, bads",
@@ -453,7 +484,6 @@ def test_io_egi_evokeds_mff(idx, cond, tmax, signals, bads):
     assert evoked_cond.info["device_info"]["type"] == "HydroCel GSN 256 1.0"
 
 
-@pytest.mark.filterwarnings("ignore::FutureWarning")
 @requires_testing_data
 def test_read_evokeds_mff_bad_input():
     """Test errors are thrown when reading invalid input file."""
diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py
index 2bcf50767..cd259d751 100644
--- a/mne/tests/test_annotations.py
+++ b/mne/tests/test_annotations.py
@@ -1171,7 +1171,7 @@ def test_annotations_simple_iteration():
             elements.values(), EXPECTED_ELEMENTS_TYPE, expected_values
         ):
             assert np.isscalar(elem)
-            assert type(elem) == expected_type
+            assert isinstance(elem, expected_type)
             assert elem == expected_value