diff --git a/epicardium/ble/ble_main.c b/epicardium/ble/ble_main.c
index c3f7d86a00df4170045ec321bc1fff7913b01833..a6e7343088a4609eeaf2c22314f6a8350a44a47e 100644
--- a/epicardium/ble/ble_main.c
+++ b/epicardium/ble/ble_main.c
@@ -34,6 +34,7 @@
 #include "cccd.h"
 #include "ess.h"
 #include "hid.h"
+#include "uart.h"
 
 #include "ble_api.h"
 #include "epicardium.h"
@@ -234,6 +235,7 @@ static const attsCccSet_t bleCccSet[BLE_NUM_CCC_IDX] =
   {HID_INPUT_REPORT_2_CH_CCC_HDL,       ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},    /* HIDAPP_IN_MOUSE_CCC_HDL */
   {HID_INPUT_REPORT_3_CH_CCC_HDL,       ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},    /* HIDAPP_IN_CONSUMER_CCC_HDL */
   {ESS_IAQ_CH_CCC_HDL,    ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},   /* BLE_ESS_IAQ_CCC_IDX */
+  {UART_TX_CH_CCC_HDL,    ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},   /* BLE_ESS_IAQ_CCC_IDX */
 };
 
 /**************************************************************************************************
diff --git a/epicardium/ble/cccd.h b/epicardium/ble/cccd.h
index 5c3edf3cb64361af9e5cb841731a80318f102d90..0e49f311871c7c3ee6a872ce42fecf7f48853097 100644
--- a/epicardium/ble/cccd.h
+++ b/epicardium/ble/cccd.h
@@ -13,6 +13,7 @@ enum
   HIDAPP_IN_MOUSE_CCC_HDL,              /*! HID Input Report characteristic for mouse inputs */
   HIDAPP_IN_CONSUMER_CCC_HDL,             /*! HID Input Report characteristic for consumer control inputs */
   BLE_ESS_IAQ_CCC_IDX,                    /*! Environmental sensing service, IAQ characteristic */
+  UART_TX_CH_CCC_IDX,
   BLE_NUM_CCC_IDX
 };
 
diff --git a/epicardium/ble/uart.c b/epicardium/ble/uart.c
index 5fdfb74ac9885975e125f51216bd2953986eca9c..8fbe737db7ebc8710b74cc3a2618abd7b86e1db4 100644
--- a/epicardium/ble/uart.c
+++ b/epicardium/ble/uart.c
@@ -1,3 +1,5 @@
+#include "uart.h"
+
 #include "modules/modules.h"
 
 #include "wsf_types.h"
@@ -11,24 +13,10 @@
 #include <string.h>
 #include <stdbool.h>
 
-#define UART_START_HDL 0x800            /*!< \brief Service start handle. */
-#define UART_END_HDL (UART_MAX_HDL - 1) /*!< \brief Service end handle. */
-
 /**************************************************************************************************
  Handles
 **************************************************************************************************/
 
-/*! \brief UART Service Handles */
-enum { UART_SVC_HDL = UART_START_HDL, /*!< \brief UART service declaration */
-       UART_RX_CH_HDL,                /*!< \brief UART rx characteristic */
-       UART_RX_HDL,                   /*!< \brief UART rx value */
-       UART_TX_CH_HDL,                /*!< \brief UART tx characteristic */
-       UART_TX_HDL,                   /*!< \brief UART tx value */
-       UART_TX_CH_CCC_HDL,            /*!< \brief UART tx CCCD */
-       UART_MAX_HDL                   /*!< \brief Maximum handle. */
-};
-/**@}*/
-
 /* clang-format off */
 static const uint8_t UARTSvc[] = {0x9E,0xCA,0xDC,0x24,0x0E,0xE5,0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x01,0x00,0x40,0x6E};
 static const uint16_t UARTSvc_len = sizeof(UARTSvc);
@@ -41,7 +29,10 @@ static const uint8_t uartTxCh[] = {ATT_PROP_READ | ATT_PROP_NOTIFY, UINT16_TO_BY
 static const uint16_t uartTxCh_len = sizeof(uartTxCh);
 static const uint8_t attUartTxChUuid[] = {0x9E,0xCA,0xDC,0x24,0x0E,0xE5, 0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x03,0x00,0x40,0x6E};
 
-static uint8_t ble_uart_tx_buf[128];
+static uint8_t uartValTxChCcc[] = {UINT16_TO_BYTES(0x0000)};
+static const uint16_t uartLenTxChCcc = sizeof(uartValTxChCcc);
+
+static uint8_t ble_uart_tx_buf[20];
 static uint16_t ble_uart_buf_tx_fill = 0;
 /* clang-format on */
 
@@ -96,15 +87,16 @@ static const attsAttr_t uartAttrCfgList[] = {
 	},
 	/* UART tx CCC descriptor */
 	{
-		.pUuid       = attCliChCfgUuid,
-		.pValue      = NULL,
-		.pLen        = NULL,
-		.maxLen      = 0,
-		.settings    = ATTS_SET_CCC,
-		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-			       ATTS_PERMIT_WRITE_AUTH | ATTS_PERMIT_READ |
-			       ATTS_PERMIT_READ_ENC | ATTS_PERMIT_READ_AUTH,
+		.pUuid    = attCliChCfgUuid,
+		.pValue   = uartValTxChCcc,
+		.pLen     = (uint16_t *)&uartLenTxChCcc,
+		.maxLen   = sizeof(uartValTxChCcc),
+		.settings = ATTS_SET_CCC,
+		.permissions =
+			(ATTS_PERMIT_READ |
+			 ATTS_PERMIT_WRITE) // How about security?
 	},
+
 };
 
 dmConnId_t active_connection = 0;
@@ -119,63 +111,68 @@ static uint8_t UARTWriteCback(
 	attsAttr_t *pAttr
 ) {
 	active_connection = connId;
+	static bool was_r = false;
 
-	//printf("UARTWriteCback %d: ", len);
 	int i;
 	for (i = 0; i < len; i++) {
-		//printf("%c", pValue[i]);
+		if (pValue[i] == '\n' && !was_r) {
+			serial_enqueue_char('\r');
+		}
+		was_r = pValue[i] == '\r';
 		serial_enqueue_char(pValue[i]);
 	}
-	serial_enqueue_char('\r');
-	//printf("\n");
-
-#if 0
-  AttsSetAttr(UART_TX_HDL, len, pValue);
-  AttsHandleValueNtf(connId, UART_TX_HDL, len, pValue);
-#endif
 
 	return ATT_SUCCESS;
 }
 
 static int ble_uart_lasttick = 0;
 
-void ble_uart_write(uint8_t *pValue, uint8_t len)
+void ble_uart_flush(void)
 {
-	for (int i = 0; i < len; i++) {
-		if (pValue[i] >= 0x20 && pValue[i] < 0x7f) {
-			ble_uart_tx_buf[ble_uart_buf_tx_fill] = pValue[i];
-			ble_uart_buf_tx_fill++;
-		}
-
-		if (ble_uart_buf_tx_fill == 128 || pValue[i] == '\r' ||
-		    pValue[i] == '\n') {
-			if (ble_uart_buf_tx_fill > 0) {
-				if (active_connection) {
-					int x = xTaskGetTickCount() -
-						ble_uart_lasttick;
-					if (x < 100) {
-						/*
-						 * TODO: Ugly hack if we already
-						 *     send something recently.
-						 *     Figure out how fast we
-						 *     can send or use indications.
-						 */
-						vTaskDelay(100 - x);
-					}
-					AttsHandleValueNtf(
-						active_connection,
-						UART_TX_HDL,
-						ble_uart_buf_tx_fill,
-						ble_uart_tx_buf
-					);
-					ble_uart_lasttick = xTaskGetTickCount();
-				}
-				ble_uart_buf_tx_fill = 0;
+	if (ble_uart_buf_tx_fill > 0) {
+		if (active_connection) {
+			int x = xTaskGetTickCount() - ble_uart_lasttick;
+			if (x < 100) {
+				/*
+				 * TODO: Ugly hack if we already
+				 * sent something recently.
+				 * Use ATTS_HANDLE_VALUE_CNF instead.
+				 */
+				vTaskDelay(100 - x);
 			}
+			AttsHandleValueNtf(
+				active_connection,
+				UART_TX_HDL,
+				ble_uart_buf_tx_fill,
+				ble_uart_tx_buf
+			);
+			ble_uart_lasttick = xTaskGetTickCount();
 		}
+		ble_uart_buf_tx_fill = 0;
 	}
 }
 
+void ble_uart_write_char(uint8_t c)
+{
+	ble_uart_tx_buf[ble_uart_buf_tx_fill] = c;
+	ble_uart_buf_tx_fill++;
+
+	// TODO: increase buffer if configured MTU allows it
+	if (ble_uart_buf_tx_fill == sizeof(ble_uart_tx_buf)) {
+		ble_uart_flush();
+	}
+}
+
+void ble_uart_write(uint8_t *pValue, uint8_t len)
+{
+	for (int i = 0; i < len; i++) {
+		ble_uart_write_char(pValue[i]);
+	}
+
+	// TODO schedule timer in a few ms to flush the buffer
+	ble_uart_flush();
+}
+
 static attsGroup_t uartCfgGroup = {
 	.pAttr       = (attsAttr_t *)uartAttrCfgList,
 	.writeCback  = UARTWriteCback,
diff --git a/epicardium/ble/uart.h b/epicardium/ble/uart.h
new file mode 100644
index 0000000000000000000000000000000000000000..ff34e90db74b41b5042c17d1aa87d2ca1cdae64e
--- /dev/null
+++ b/epicardium/ble/uart.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#define UART_START_HDL 0x800            /*!< \brief Service start handle. */
+#define UART_END_HDL (UART_MAX_HDL - 1) /*!< \brief Service end handle. */
+
+/*! \brief UART Service Handles */
+enum { UART_SVC_HDL = UART_START_HDL, /*!< \brief UART service declaration */
+       UART_RX_CH_HDL,                /*!< \brief UART rx characteristic */
+       UART_RX_HDL,                   /*!< \brief UART rx value */
+       UART_TX_CH_HDL,                /*!< \brief UART tx characteristic */
+       UART_TX_HDL,                   /*!< \brief UART tx value */
+       UART_TX_CH_CCC_HDL,            /*!< \brief UART tx CCCD */
+       UART_MAX_HDL                   /*!< \brief Maximum handle. */
+};
+
+
diff --git a/epicardium/modules/serial.c b/epicardium/modules/serial.c
index f2ec9e9344c9566005fc3e896f70ece2bdbc798e..bbf8c015902cfca4db0d184b304f67d4b1beb0e5 100644
--- a/epicardium/modules/serial.c
+++ b/epicardium/modules/serial.c
@@ -50,10 +50,7 @@ void serial_return_to_synchronous()
 	write_stream_buffer = NULL;
 }
 
-/*
- * API-call to write a string.  Output goes to both CDCACM and UART
- */
-void epic_uart_write_str(const char *str, size_t length)
+static void write_str_(const char *str, size_t length)
 {
 	if (length == 0) {
 		return;
@@ -129,6 +126,15 @@ void epic_uart_write_str(const char *str, size_t length)
 	}
 }
 
+/*
+ * API-call to write a string.  Output goes to both CDCACM, UART and BLE
+ */
+void epic_uart_write_str(const char *str, size_t length)
+{
+	ble_uart_write((uint8_t *)str, length);
+	write_str_(str, length);
+}
+
 static void serial_flush_from_isr(void)
 {
 	uint8_t rx_data[32];
@@ -196,7 +202,7 @@ static void serial_flush_from_thread(void)
 		taskEXIT_CRITICAL();
 
 		cdcacm_write((uint8_t *)&rx_data, received_bytes);
-		ble_uart_write((uint8_t *)&rx_data, received_bytes);
+		//ble_uart_write((uint8_t *)&rx_data, received_bytes);
 	} while (received_bytes > 0);
 }
 
@@ -243,6 +249,9 @@ int epic_uart_read_str(char *buf, size_t cnt)
 	return i;
 }
 
+/*
+ * Write a string.  Output goes to both CDCACM and UART
+ */
 long _write_epicardium(int fd, const char *buf, size_t cnt)
 {
 	/*
@@ -252,12 +261,12 @@ long _write_epicardium(int fd, const char *buf, size_t cnt)
 	size_t i, last = 0;
 	for (i = 0; i < cnt; i++) {
 		if (buf[i] == '\n') {
-			epic_uart_write_str(&buf[last], i - last);
-			epic_uart_write_str("\r", 1);
+			write_str_(&buf[last], i - last);
+			write_str_("\r", 1);
 			last = i;
 		}
 	}
-	epic_uart_write_str(&buf[last], cnt - last);
+	write_str_(&buf[last], cnt - last);
 	return cnt;
 }