How to implement ECG viewer / hanging protocol not matched

Describe Your Question

  • A clear and concise description of what problem you are having.

I am trying to use the ECG viewer onto the release build, but when trying to open an ECG study, I get “The hanging protocol viewport is requesting to display defaultDisplaySetId displaySet that is not matched based on the provided criteria (e.g. matching rules).” on the console, with a black screen on the viewport.

I am using the mode ECG mode downloaded from the cli, and manually linked the extension from the @radicalimaging repo (

  • Which OHIF version you are using?
    3.7 release

What steps can we follow to reproduce the bug?

  1. Download and link ecg-dicom extension
  2. Download ECG mode from yarn run cli search
  3. Connect the app to a DB with ECGs
    4.Try to open one and open the console
Please use code blocks to show formatted errors or code snippets

I believe I have the mode and extension properly set up, but might be missing addind a hanging protocol? Quite new to the app and still very lost, appreciate the help a lot.

I have changed some names from sopClassHandlers but made sure they are named properly inbetween files.

Mode set up:

import { hotkeys } from '@ohif/core';
import { id } from './id';
import { initToolGroups, toolbarButtons } from '@ohif/mode-longitudinal';

const ohif = {
  layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
  sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
  hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default',
  leftPanel: '@ohif/extension-default.panelModule.seriesList',
  rightPanel: '@ohif/extension-default.panelModule.measure',

const cornerstone = {
  viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone',

const modeECG = {
  sopClassHandler: 'ecg-extension.sopClassHandlerModule.ecg-dicom',
  viewport: 'ecg-extension.viewportModule.ecg-dicom',

 * Just two dependencies to be able to render a viewport with panels in order
 * to make sure that the mode is working.
const extensionDependencies = {
  '@ohif/extension-default': '^3.0.0',
  '@ohif/extension-cornerstone': '^3.0.0',
  'ecg-extension': "^0.0.1"

function modeFactory({ modeConfiguration }) {
  return {
     * Mode ID, which should be unique among modes used by the viewer. This ID
     * is used to identify the mode in the viewer's state.
    routeName: 'ecg',
     * Mode name, which is displayed in the viewer's UI in the workList, for the
     * user to select the mode.
    displayName: 'ECG',
     * Runs when the Mode Route is mounted to the DOM. Usually used to initialize
     * Services and other resources.
    onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => {
      const { measurementService, toolbarService, toolGroupService } =;


      // Init Default and SR ToolGroups
      initToolGroups(extensionManager, toolGroupService, commandsManager);

      let unsubscribe;

      const activateTool = () => {
          groupId: 'WindowLevel',
          interactionType: 'tool',
          commands: [
              commandName: 'setToolActive',
              commandOptions: {
                toolName: 'WindowLevel',
              context: 'CORNERSTONE',

        // We don't need to reset the active tool whenever a viewport is getting
        // added to the toolGroup.

      // Since we only have one viewport for the basic cs3d mode and it has
      // only one hanging protocol, we can just use the first viewport
      ({ unsubscribe } = toolGroupService.subscribe(

      toolbarService.createButtonSection('primary', [
    onModeExit: ({ servicesManager }) => {
      const {
      } =;

    /** */
    validationTags: {
      study: [],
      series: [],
     * A boolean return value that indicates whether the mode is valid for the
     * modalities of the selected studies. For instance a PET/CT mode should be
    isValidMode: ({ modalities }) => {
      const modalities_list = modalities.split('\\');

      return modalities_list.includes('ECG')
     * Mode Routes are used to define the mode's behavior. A list of Mode Route
     * that includes the mode's path and the layout to be used. The layout will
     * include the components that are used in the layout. For instance, if the
     * default layoutTemplate is used (id: '@ohif/extension-default.layoutTemplateModule.viewerLayout')
     * it will include the leftPanels, rightPanels, and viewports. However, if
     * you define another layoutTemplate that includes a Footer for instance,
     * you should provide the Footer component here too. Note: We use Strings
     * to reference the component's ID as they are registered in the internal
     * ExtensionManager. The template for the string is:
     * `${extensionId}.{moduleType}.${componentId}`.
    routes: [
        path: 'ECG',
        layoutTemplate: ({ location, servicesManager }) => {
          return {
            id: ohif.layout,
            props: {
              leftPanels: [ohif.leftPanel],
              viewports: [
                  namespace: modeECG.viewport,
                  displaySetsToDisplay: [modeECG.sopClassHandler],
    /** List of extensions that are used by the mode */
    extensions: extensionDependencies,
    /** HangingProtocol used by the mode */
    hangingProtocol: 'default',
    /** SopClassHandlers used by the mode */
    sopClassHandlers: [modeECG.sopClassHandler, ohif.sopClassHandler],
    /** hotkeys for mode */
    hotkeys: [...hotkeys.defaults.hotkeyBindings],

const mode = {

export default mode;

Extension src/index:

import { id } from './id';
import getSopClassHandlerModule from "./getSopClassHandlerModule";
import React, { Suspense } from "react";

const Component = React.lazy(() => {
  return import('./viewports/EcgViewport');

const EcgViewport = props => {
  return (
    <Suspense fallback={(<div>Loading...</div>)}>
      <Component {...props} />

 * You can remove any of the following modules if you don't need them.
export default {
   * Only required property. Should be a unique value across all extensions.
   * You ID can be anything you want, but it should be unique.

   * Perform any pre-registration tasks here. This is called before the extension
   * is registered. Usually we run tasks such as: configuring the libraries
   * (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
   * this extension is providing.
  preRegistration: ({
    configuration = {},
  }) => { },

   * ViewportModule should provide a list of viewports that will be available in OHIF
   * for Modes to consume and use in the viewports. Each viewport is defined by
   * {name, component} object. Example of a viewport module is the CornerstoneViewport
   * that is provided by the Cornerstone extension in OHIF.
  getViewportModule({ servicesManager, extensionManager }) {
    const ExtendedEcgViewport = props => {
      return (

    return [{ name: 'ecg-dicom', component: ExtendedEcgViewport }];

   * SopClassHandlerModule should provide a list of sop class handlers that will be
   * available in OHIF for Modes to consume and use to create displaySets from Series.
   * Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
   * Examples include the default sop class handler provided by the default extension

ecg-extension/src/getSopClassHandlerModule (If not hanging protocol issue might be here?):

import { utils, classes } from '@ohif/core';
import { SOPClassHandlerId } from './id';

const { ImageSet } = classes;

const SOP_CLASS_UIDS = {

const sopClassUids = Object.values(SOP_CLASS_UIDS);

const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) => {
  return instances
    .map(instance => {
      const { Modality, SOPInstanceUID, SeriesDescription = "ECG" } = instance;
      const { SeriesDate, SeriesNumber, SeriesInstanceUID, StudyInstanceUID } = instance;
      const displaySet = {
        //plugin: id,
        displaySetInstanceUID: utils.guid(),
        SeriesNumber: SeriesNumber || 1,
        referencedImages: null,
        measurements: null,
        others: [instance],
        isDerivedDisplaySet: true,
        isLoaded: false,
        numImageFrames: 0,
      return displaySet;

export default function getSopClassHandlerModule({ servicesManager, extensionManager }) {
  const getDisplaySetsFromSeries = instances => {
    return _getDisplaySetsFromSeries(

  return [
      name: 'ecg-dicom',

Thank you for your time!

Got it working with the DB that comes from the “Try it out”, seems like the issue comes from the DB I was working with from the hospital not sending the same response when asking for the metadata as the try out.

Apologies for the post, cannot see a way to remove it, if any staff can or mark as resolved/edit name to resolved.

Thank you!