diff --git a/Documentation/index.rst b/Documentation/index.rst
index 561fb9bddf931dc6492f05bbbc94654fc4c8c8d0..0607bdaa5c89d6b1508c25c809375db529a9ee36 100644
--- a/Documentation/index.rst
+++ b/Documentation/index.rst
@@ -24,6 +24,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the
    pycardium/stdlib
    pycardium/bhi160
    pycardium/bme680
+   pycardium/max30001
    pycardium/buttons
    pycardium/color
    pycardium/display
diff --git a/Documentation/pycardium/max30001.rst b/Documentation/pycardium/max30001.rst
new file mode 100644
index 0000000000000000000000000000000000000000..1fb0ddb954a53dfd04282e16aa549a2de5a9a726
--- /dev/null
+++ b/Documentation/pycardium/max30001.rst
@@ -0,0 +1,5 @@
+``max30001`` - MAX30001
+=====================
+
+.. automodule:: max30001
+   :members:
diff --git a/pycardium/meson.build b/pycardium/meson.build
index bb8d03b4fddca90a01be91b8de731abdfd7d3e74..32973bae0c945a0208ba4c1040367dac75b84fb9 100644
--- a/pycardium/meson.build
+++ b/pycardium/meson.build
@@ -9,6 +9,7 @@ modsrc = files(
   'modules/interrupt.c',
   'modules/sys_leds.c',
   'modules/light_sensor.c',
+  'modules/max30001-sys.c',
   'modules/os.c',
   'modules/personal_state.c',
   'modules/power.c',
diff --git a/pycardium/modules/interrupt.c b/pycardium/modules/interrupt.c
index 927b936b906a8a7a8a952d12bf9940ab76ce8eff..838ef2b3a397887221041804a0477a5d1cfdbce5 100644
--- a/pycardium/modules/interrupt.c
+++ b/pycardium/modules/interrupt.c
@@ -91,6 +91,9 @@ static const mp_rom_map_elem_t interrupt_module_globals_table[] = {
 	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_BHI160_ORIENTATION) },
 	{ MP_ROM_QSTR(MP_QSTR_BHI160_GYROSCOPE),
 	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_BHI160_GYROSCOPE) },
+	{ MP_ROM_QSTR(MP_QSTR_MAX30001_ECG),
+	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_MAX30001_ECG) },
+
 };
 static MP_DEFINE_CONST_DICT(
 	interrupt_module_globals, interrupt_module_globals_table
diff --git a/pycardium/modules/max30001-sys.c b/pycardium/modules/max30001-sys.c
new file mode 100644
index 0000000000000000000000000000000000000000..08d5bad68fd9bdc4806a875aab6aecab75ba4262
--- /dev/null
+++ b/pycardium/modules/max30001-sys.c
@@ -0,0 +1,76 @@
+#include "py/obj.h"
+#include "py/runtime.h"
+#include "py/builtin.h"
+#include "epicardium.h"
+#include "api/common.h"
+#include "mphalport.h"
+
+STATIC mp_obj_t mp_max30001_enable_sensor(size_t n_args, const mp_obj_t *args)
+{
+	struct max30001_sensor_config cfg = { 0 };
+	cfg.usb                           = mp_obj_is_true(args[0]);
+	cfg.bias                          = mp_obj_is_true(args[1]);
+	cfg.sample_rate                   = mp_obj_get_int(args[2]);
+	cfg.sample_buffer_len             = mp_obj_get_int(args[3]);
+
+	int stream_id = epic_max30001_enable_sensor(&cfg);
+
+	return MP_OBJ_NEW_SMALL_INT(stream_id);
+}
+
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(
+	mp_max30001_enable_sensor_obj, 4, 4, mp_max30001_enable_sensor
+);
+
+STATIC mp_obj_t mp_max30001_read_sensor(mp_obj_t stream_id_in)
+{
+	int16_t buf[256];
+	int stream_id = mp_obj_get_int(stream_id_in);
+
+	int n = epic_stream_read(stream_id, buf, sizeof(buf));
+
+	mp_obj_list_t *list = mp_obj_new_list(0, NULL);
+	for (int i = 0; i < n; i++) {
+		mp_obj_list_append(list, mp_obj_new_int(buf[i]));
+	}
+
+	return MP_OBJ_FROM_PTR(list);
+}
+
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(
+	mp_max30001_read_sensor_obj, mp_max30001_read_sensor
+);
+
+STATIC mp_obj_t mp_max30001_disable_sensor(void)
+{
+	int ret = epic_max30001_disable_sensor();
+
+	return MP_OBJ_NEW_SMALL_INT(ret);
+}
+
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(
+	mp_max30001_disable_sensor_obj, mp_max30001_disable_sensor
+);
+
+STATIC const mp_rom_map_elem_t max30001_module_globals_table[] = {
+	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_max30001) },
+	{ MP_ROM_QSTR(MP_QSTR_enable_sensor),
+	  MP_ROM_PTR(&mp_max30001_enable_sensor_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_read_sensor),
+	  MP_ROM_PTR(&mp_max30001_read_sensor_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_disable_sensor),
+	  MP_ROM_PTR(&mp_max30001_disable_sensor_obj) },
+};
+STATIC MP_DEFINE_CONST_DICT(
+	max30001_module_globals, max30001_module_globals_table
+);
+
+// Define module object.
+const mp_obj_module_t max30001_module = {
+	.base    = { &mp_type_module },
+	.globals = (mp_obj_dict_t *)&max30001_module_globals,
+};
+
+/* clang-format off */
+// Register the module to make it available in Python
+MP_REGISTER_MODULE(MP_QSTR_sys_max30001, max30001_module, MODULE_MAX30001_ENABLED);
diff --git a/pycardium/modules/py/max30001.py b/pycardium/modules/py/max30001.py
new file mode 100644
index 0000000000000000000000000000000000000000..67bc5169f4f2ca0020c78054f767bfdae000bab4
--- /dev/null
+++ b/pycardium/modules/py/max30001.py
@@ -0,0 +1,97 @@
+import sys_max30001
+import interrupt
+import ucollections
+
+
+class MAX30001:
+    """
+    The MAX30001 class provides a stram interface to the MAX30001
+    ECG and BIO-Z sensor.
+
+    .. code-block:: python
+
+        import max30001
+        m = max30001.MAX30001()
+        m.read()
+        m.close()
+    """
+
+    def __init__(
+        self,
+        usb=False,
+        bias=True,
+        sample_rate=128,
+        callback=None,
+        sample_buffer_len=256,
+    ):
+        """
+        Initializes the MAX30001 (if it is not already running).
+
+        :param usb: True if all ECG leads should use the USB-C connector
+        :param bias: True if the internal bias of the ECG should be used. Mandatory if the card10 is not attached to the body and the USB-C connector is used.
+        :param sample_rate: Selected sample rate in Hz. Supported values: 128 and 256.
+        :param callback: If not None: A callback which is called with the data when ever new data is available
+        :param sample_buffer: Length of the internal buffer (in samples)
+        """
+
+        self.sample_rate = sample_rate
+        self.callback = callback
+        self.sample_buffer_len = sample_buffer_len
+        self.interrupt_id = interrupt.MAX30001_ECG
+        self.usb = usb
+        self.bias = bias
+        self._callback = callback
+        self.enable_sensor()
+
+    def enable_sensor(self):
+        """
+        Enables the sensor. Automatically called by __init__.
+        """
+        interrupt.disable_callback(self.interrupt_id)
+        interrupt.set_callback(self.interrupt_id, self._interrupt)
+        self.stream_id = sys_max30001.enable_sensor(
+            self.usb, self.bias, self.sample_rate, self.sample_buffer_len
+        )
+
+        if self.stream_id < 0:
+            raise ValueError("Enable sensor returned %i", self.stream_id)
+
+        self.active = True
+
+        if self._callback:
+            interrupt.enable_callback(self.interrupt_id)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, _et, _ev, _t):
+        self.close()
+
+    def close(self):
+        """
+        Close the currently open connection to the sensor.
+        """
+
+        if self.active:
+            self.active = False
+            ret = sys_max30001.disable_sensor(self.sensor_id)
+
+            if ret < 0:
+                raise ValueError("Disable sensor returned %i", ret)
+
+            interrupt.disable_callback(self.interrupt_id)
+            interrupt.set_callback(self.interrupt_id, None)
+
+    def read(self):
+        """
+        Read as many samples (signed integer) as currently available.
+        """
+        if self.active:
+            return sys_max30001.read_sensor(self.stream_id)
+        return []
+
+    def _interrupt(self, _):
+        if self.active:
+            data = self.read()
+            if self._callback:
+                self._callback(data)
diff --git a/pycardium/modules/py/meson.build b/pycardium/modules/py/meson.build
index 5548fff7e9fb15c17fec2f6c1c8d9add23a6541d..75bf937c7b2d394d0834542e333bb050328c0823 100644
--- a/pycardium/modules/py/meson.build
+++ b/pycardium/modules/py/meson.build
@@ -4,6 +4,7 @@ python_modules = files(
   'htmlcolor.py',
   'display.py',
   'leds.py',
+  'max30001.py',
   'pride.py',
   'ledfx.py',
   'simple_menu.py',
diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h
index d789c8730c42f68925b4d058fe35e93449b2e4bb..095f989871cbc0c84ee7941a6e21946278928453 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -163,3 +163,5 @@ Q(NO_CONTACT)
 Q(CHAOS)
 Q(COMMUNICATION)
 Q(CAMP)
+
+Q(MAX30001_ECG)
diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h
index 43f878706cda83c9d7350f211a9b5ee3686dbea9..aad4320d8516f121552eb0f63d8ebbd91fa841a6 100644
--- a/pycardium/mpconfigport.h
+++ b/pycardium/mpconfigport.h
@@ -53,6 +53,7 @@ int mp_hal_trng_read_int(void);
 #define MODULE_INTERRUPT_ENABLED            (1)
 #define MODULE_LEDS_ENABLED                 (1)
 #define MODULE_LIGHT_SENSOR_ENABLED         (1)
+#define MODULE_MAX30001_ENABLED             (1)
 #define MODULE_OS_ENABLED                   (1)
 #define MODULE_PERSONAL_STATE_ENABLED       (1)
 #define MODULE_POWER_ENABLED                (1)