diff --git a/Documentation/bluetooth/ecg.rst b/Documentation/bluetooth/ecg.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3af7cf771a757baf4a9ab6f4ab0fccb1046761e1
--- /dev/null
+++ b/Documentation/bluetooth/ecg.rst
@@ -0,0 +1,27 @@
+Bluetooth ECG Service
+========================
+
+.. warning::
+    The service is still work in progress and subject to change
+
+The ECG service provides access to the ECG sensor of the card10
+
+BLE Service
+-----------
+
+The current draft uses following service specification:
+
+- Service:
+
+  UUID: ``42230300-2342-2342-2342-234223422342``
+
+- ECG samples characteristic:
+
+  UUID: ``42230301-2342-2342-2342-234223422342``
+  notify
+
+ECG samples characteristic
+--------------------------
+
+List of 16 bit samples (big endian). Enable notifications to
+receive a stream of samples while the ECG app is open.
diff --git a/Documentation/index.rst b/Documentation/index.rst
index d7acad2eea356d26215186f0da41f0a85950fb32..5c8f389f0c4d34ec38a288aade40e75cd60d8847 100644
--- a/Documentation/index.rst
+++ b/Documentation/index.rst
@@ -75,6 +75,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the
    bluetooth/ess
    bluetooth/file-transfer
    bluetooth/card10
+   bluetooth/ecg
    bluetooth/nimble
 
 Indices and tables
diff --git a/preload/apps/ecg/__init__.py b/preload/apps/ecg/__init__.py
index 978141d0e7c6722ad31e4ec1f6e976bd4e37195a..a19cebc80ae0a80f4bd365683ac1101d5a70058e 100644
--- a/preload/apps/ecg/__init__.py
+++ b/preload/apps/ecg/__init__.py
@@ -7,6 +7,7 @@ import max30001
 import math
 import struct
 import itertools
+import bluetooth
 from ecg.settings import *
 
 config = ecg_settings()
@@ -43,6 +44,52 @@ sensor = 0
 disp = display.open()
 last_sample_count = 1
 
+
+_IRQ_CENTRAL_CONNECT = const(1)
+_IRQ_CENTRAL_DISCONNECT = const(2)
+_IRQ_GATTS_WRITE = const(3)
+
+
+def ble_irq(event, data):
+    global ble_streaming
+    if event == _IRQ_CENTRAL_CONNECT:
+        print("BLE Connected")
+    elif event == _IRQ_CENTRAL_DISCONNECT:
+        print("BLE Disconnected")
+    elif event == _IRQ_GATTS_WRITE:
+        conn_handle, value_handle = data
+        if value_handle == ecg_cccd_handle:
+            value = b.gatts_read(value_handle)
+            print("New cccd value:", value)
+            # Value of 0 means notifcations off
+            if value == b"\x00\x00":
+                ble_streaming = False
+                if not config.get_option("BLE Disp"):
+                    disp.backlight(20)
+            else:
+                ble_streaming = True
+                if not config.get_option("BLE Disp"):
+                    disp.backlight(0)
+
+
+b = bluetooth.BLE()
+b.active(True)
+b.irq(ble_irq)
+
+_ECG_UUID = bluetooth.UUID("42230300-2342-2342-2342-234223422342")
+_ECG_DATA = (
+    bluetooth.UUID("42230301-2342-2342-2342-234223422342"),
+    bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,
+)
+
+_ECG_SERVICE = (_ECG_UUID, (_ECG_DATA,))
+((ecg_data_handle,),) = b.gatts_register_services((_ECG_SERVICE,))
+ecg_cccd_handle = ecg_data_handle + 1
+
+# Disable streaming by default
+ble_streaming = False
+b.gatts_write(ecg_cccd_handle, "\x00\x00")
+
 leds.dim_top(1)
 COLORS = [((23 + (15 * i)) % 360, 1.0, 1.0) for i in range(11)]
 
@@ -129,7 +176,20 @@ def detect_pulse(num_new_samples):
 
 
 def callback_ecg(datasets):
-    global update_screen, history, filebuffer, write, samples_since_start_of_write
+    global update_screen, history, filebuffer, write, samples_since_start_of_write, ble_streaming, ecg_data_handle
+
+    if ble_streaming:
+        try:
+            b.gatts_notify(
+                1, ecg_data_handle, struct.pack(">" + ("h" * len(datasets)), *datasets)
+            )
+        except:
+            pass
+
+        # Don't update the screen if it should be off during a connection
+        if not config.get_option("BLE Disp"):
+            return
+
     update_screen += len(datasets)
     if write > 0:
         samples_since_start_of_write += len(datasets)
@@ -441,7 +501,13 @@ def main():
                 else:
                     pause_screen = -1  # hide graph
                     leds.clear()  # disable all LEDs
+                    if ble_streaming and not config.get_option("BLE Disp"):
+                        disp.backlight(20)
+
                     config.run()  # show config menu
+
+                    if ble_streaming and not config.get_option("BLE Disp"):
+                        disp.backlight(0)
                     close_sensor()  # reset sensor in case mode or bias was changed TODO do not close sensor otherwise?
                     open_sensor()
                     pause_screen = 0  # start plotting graph again
diff --git a/preload/apps/ecg/settings.py b/preload/apps/ecg/settings.py
index ca49bbd109a0b7d372b7320ea5b89c4444fd396b..2336a5c798e9c6e36652f69011669f1f5ceff7c0 100644
--- a/preload/apps/ecg/settings.py
+++ b/preload/apps/ecg/settings.py
@@ -95,6 +95,7 @@ def ecg_settings():
     config.add_option(("Filter", itertools.cycle([("HP", {"HP"}), ("off", {})])))
     config.add_option(("Rate", itertools.cycle([("128Hz", 128), ("256Hz", 256)])))
     config.add_option(("Window", itertools.cycle([("1x", 1), ("2x", 2), ("3x", 3)])))
+    config.add_option(("BLE Disp", itertools.cycle([("Off", False), ("On", True)])))
     config.add_option(
         (
             "Log",