diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 7886cc5f0bb28fbc2ba5d818e43ef5f7879c0e50..cc0926d1078f25f3638486ba66072a9bf3d73d2b 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -1,6 +1,8 @@
 #ifndef _EPICARDIUM_H
 #define _EPICARDIUM_H
 #include <stdint.h>
+#include <stddef.h>
+#include <errno.h>
 
 #ifndef API
 #define API(id, def) def
@@ -12,6 +14,7 @@
 #define API_LEDS_SET           0x3
 #define API_VIBRA_SET          0x4
 #define API_VIBRA_VIBRATE      0x5
+#define API_STREAM_READ        0x6
 /* clang-format on */
 
 /**
@@ -55,6 +58,61 @@ API(API_UART_READ, char epic_uart_read_chr(void));
  */
 API(API_LEDS_SET, void epic_leds_set(int led, uint8_t r, uint8_t g, uint8_t b));
 
+/**
+ * Sensor Data Streams
+ * ===================
+ * A few of card10's sensors can do continuous measurements.  To allow
+ * performant access to their data, the following function is made for generic
+ * access to streams.
+ */
+
+/**
+ * Read sensor data into a buffer.  ``epic_stream_read()`` will read as many
+ * sensor data packets as possible into ``buf`` and return as soon as possible.
+ * It will poke the sensor driver once to check whether new data can be fetched.
+ * If there is no new sensor data, ``epic_stream_read()`` will return ``0`` and
+ * not touch ``buf``.  Otherwise it will return the number of data packets which
+ * were read into ``buf``.
+ *
+ * :param int sd: Sensor Descriptor.  You get sensor descriptors as return
+ *    values when activating the respective sensors.
+ * :param void* buf: Buffer where sensor data should be read into.
+ * :param size_t count: How many bytes to read at max.  Note that fewer bytes
+ *    might be read.  In most cases, this should be ``sizeof(buf)``.
+ * :return: Number of data packets read (**not** number of bytes) or a negative
+ *    error value.  Possible errors:
+ *
+ *    - ``-ENODEV``: Sensor is not currently available.
+ *    - ``-EBADF``: The given sensor descriptor is unknown.
+ *    - ``-EINVAL``:  If ``count`` is not a multiple of the sensor data packet
+ *      size.
+ *
+ * **Example**:
+ *
+ * .. code-block:: cpp
+ *
+ *    #include "epicardium.h"
+ *
+ *    struct foo_measurement sensor_data[16];
+ *    int foo_sd, n;
+ *
+ *    foo_sd = epic_foo_sensor_enable(9001);
+ *
+ *    while (1) {
+ *            n = epic_stream_read(
+ *                    foo_sd,
+ *                    &sensor_data,
+ *                    sizeof(sensor_data)
+ *            );
+ *
+ *            // Print out the measured sensor samples
+ *            for (int i = 0; i < n; i++) {
+ *                    printf("Measured: %?\n", sensor_data[i]);
+ *            }
+ *    }
+ */
+API(API_STREAM_READ, int epic_stream_read(int sd, void *buf, size_t count));
+
 /**
  * Misc
  * ====
diff --git a/epicardium/main.c b/epicardium/main.c
index 0117add3c1290474fba9f8cb6bb88c18339057a0..51d365f2c0a754b1432dfe5ae14ddf95de620aee 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -11,6 +11,7 @@
 #include "api/dispatcher.h"
 #include "modules/modules.h"
 #include "modules/log.h"
+#include "modules/stream.h"
 
 #include <Heart.h>
 #include "GUI_Paint.h"
@@ -53,6 +54,7 @@ int main(void)
 	}
 
 	fatfs_init();
+	stream_init();
 
 	LOG_INFO("startup", "Initializing tasks ...");
 
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index dfc27d00574c49ec1bdbeffc99321f3bb4f1758e..a038fb21d7f5ef7c650a10851503150a27c0b1eb 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -4,5 +4,6 @@ module_sources = files(
   'log.c',
   'pmic.c',
   'serial.c',
+  'stream.c',
   'vibra.c',
 )
diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h
index 8fe73d59755c6146bba1d364938a6305af9e1baa..0db3d11bf84ca07f8a65f499fa1497a55940968c 100644
--- a/epicardium/modules/modules.h
+++ b/epicardium/modules/modules.h
@@ -15,4 +15,3 @@ void vSerialTask(void *pvParameters);
 void vPmicTask(void *pvParameters);
 
 #endif /* MODULES_H */
-
diff --git a/epicardium/modules/stream.c b/epicardium/modules/stream.c
new file mode 100644
index 0000000000000000000000000000000000000000..63e63070e6c8c6aaf06386506edb9f6acd13582b
--- /dev/null
+++ b/epicardium/modules/stream.c
@@ -0,0 +1,74 @@
+#include <string.h>
+
+#include "epicardium.h"
+#include "stream.h"
+
+static struct stream_info *stream_table[SD_MAX];
+
+int stream_init()
+{
+	memset(stream_table, 0x00, sizeof(stream_table));
+	return 0;
+}
+
+int stream_register(int sd, struct stream_info *stream)
+{
+	if (sd < 0 || sd >= SD_MAX) {
+		return -EINVAL;
+	}
+
+	if (stream_table[sd] != NULL) {
+		/* Stream already registered */
+		return -EACCES;
+	}
+
+	stream_table[sd] = stream;
+	return 0;
+}
+
+int stream_deregister(int sd, struct stream_info *stream)
+{
+	if (sd < 0 || sd >= SD_MAX) {
+		return -EINVAL;
+	}
+
+	if (stream_table[sd] != stream) {
+		/* Stream registered by someone else */
+		return -EACCES;
+	}
+
+	stream_table[sd] = NULL;
+	return 0;
+}
+
+int epic_stream_read(int sd, void *buf, size_t count)
+{
+	if (sd < 0 || sd >= SD_MAX) {
+		return -EBADF;
+	}
+
+	struct stream_info *stream = stream_table[sd];
+	if (stream == NULL) {
+		return -ENODEV;
+	}
+
+	/* Poll the stream */
+	int ret = stream->poll_stream();
+	if (ret < 0) {
+		return ret;
+	}
+
+	/* Check buffer sizing */
+	if (count % stream->item_size != 0) {
+		return -EINVAL;
+	}
+
+	size_t i;
+	for (i = 0; i < count; i += stream->item_size) {
+		if (!xQueueReceive(stream->queue, buf + i, 10)) {
+			break;
+		}
+	}
+
+	return i / stream->item_size;
+}
diff --git a/epicardium/modules/stream.h b/epicardium/modules/stream.h
new file mode 100644
index 0000000000000000000000000000000000000000..d5dd30794d2367320c2f9ef8e2112769b0add203
--- /dev/null
+++ b/epicardium/modules/stream.h
@@ -0,0 +1,59 @@
+#ifndef STREAM_H
+#define STREAM_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "FreeRTOS.h"
+#include "queue.h"
+
+/**
+ * **Stream Descriptors**:
+ *
+ *    All supported streams have to have a unique ID in this list.  ``SD_MAX``
+ *    must be greater than or equal to the highest defined ID.  Please keep IDs
+ *    in sequential order.
+ */
+enum stream_descriptor {
+	/** Highest descriptor must always be ``SD_MAX``. */
+	SD_MAX,
+};
+
+struct stream_info {
+	QueueHandle_t queue;
+	size_t item_size;
+	int (*poll_stream)();
+};
+
+/**
+ * Register a stream so it can be read from Epicardium API.
+ *
+ * :param int sd: Stream Descriptor.  Must be from the above enum.
+ * :param stream_info stream: Stream info.
+ * :returns: ``0`` on success or a negative value on error.  Possible errors:
+ *
+ *    - ``-EINVAL``: Out of range sensor descriptor.
+ *    - ``-EACCES``: Stream already registered.
+ */
+int stream_register(int sd, struct stream_info *stream);
+
+/**
+ * Deregister a stream.
+ *
+ * :param int sd:  Stream Descriptor.
+ * :param stream_info stream: The stream which should be registered for the
+ *    stream ``sd``.  If a different stream is registered, this function
+ *    will return an error.
+ * :returns: ``0`` on success or a negative value on error.  Possible errors:
+ *
+ *    - ``-EINVAL``: Out of range sensor descriptor.
+ *    - ``-EACCES``: Stream ``stream`` was not registered for ``sd``.
+ */
+int stream_deregister(int sd, struct stream_info *stream);
+
+/*
+ * Initialize stream interface.  Called by main().
+ */
+int stream_init();
+
+#endif /* STREAM_H */