diff --git a/CHANGELOG.md b/CHANGELOG.md
index e580443a064b511fba178ad0f53a2330ced62723..65648e861ca72314e2e10e797fdddf7cf7cf5088 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ## [Unreleased]
+
+
+## [v1.9] - 2019-08-28 23:23 - [IcebergLettuce]
+[IcebergLettuce]: https://card10.badge.events.ccc.de/release/card10-v1.9-IcebergLettuce.zip
+
 ### Added
 - `tools/pycard10.py`: Tool to interact with card10's serial connection and
   upload files directly:
@@ -16,18 +21,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - **pycardium**: Support for RAW REPL mode.
 - **bhi160**: Function to disable all sensors (`bhi160.disable_all_sensors()`).
 - `ls_cmsis_dap`: A tool to enumerate CMSIS-DAP debuggers.
+- Tons of new features to `simple_menu`: Timeout, scrolling of long texts,
+  robustness against crashes, and proper exiting.
+- `card10.cfg` config file which allows enabling *ELF* files.
+- Analog read for wristband GPIOs.
 
 ### Changed
+- Refactored *menu* and *personal-state* apps.
 - `main.py` was moved into an app to allow easier reconfiguration of the
   default app.  The new `main.py` points to the "old" one so behavior is not
   changed.
 - After a timeout, the menu will close and `main.py` will run again.
+- BLE security updates.
+- More detailed battery state display in nickname app.
+- Improved ECG app.
 
 ### Removed
 - Some unused font files.
 
 ### Fixed
 - Fixed a regression which made the ECG app no longer work.
+- Fixed card10 advertising support for AT-commands.
+- Rectangles being one pixel too small.
 
 
 
@@ -192,7 +207,8 @@ fbf7c8c0 fix(menu.py) Refactored menu.py based on !138
 ## [v1.0] - 2019-08-21 00:50
 Initial release.
 
-[Unreleased]: https://git.card10.badge.events.ccc.de/card10/firmware/compare/v1.8...master
+[Unreleased]: https://git.card10.badge.events.ccc.de/card10/firmware/compare/v1.9...master
+[v1.9]: https://git.card10.badge.events.ccc.de/card10/firmware/compare/v1.8...v1.9
 [v1.8]: https://git.card10.badge.events.ccc.de/card10/firmware/compare/v1.7...v1.8
 [v1.7]: https://git.card10.badge.events.ccc.de/card10/firmware/compare/v1.6...v1.7
 [v1.6]: https://git.card10.badge.events.ccc.de/card10/firmware/compare/v1.5...v1.6
diff --git a/Documentation/pycardium/gpio.rst b/Documentation/pycardium/gpio.rst
index 7d17b3fdf3597aba664ebc46feae7f7a41dc62ba..28854d141843ba5d84016b2361aa12052fae18d8 100644
--- a/Documentation/pycardium/gpio.rst
+++ b/Documentation/pycardium/gpio.rst
@@ -52,7 +52,7 @@ output in your scripts.
    :returns: Current value of the GPIO pin.
       If the pin is configured as ADC, the value returned
       will be between 0 and 1000, representing voltages from
-      0V to 3.3V
+      0V to 3.3V (:py:data:`gpio.ADC` is only available in 1.9+).
 
 .. py:data:: WRISTBAND_1
 
@@ -81,6 +81,12 @@ output in your scripts.
 
    Configures a pin as input.
 
+.. py:data:: ADC
+
+   Configure pin as ADC input.
+
+   .. versionadded: 1.9
+
 .. py:data:: PULL_UP
 
    Enables the internal pull-up resistor of a pin.
diff --git a/Documentation/pycardium/simple_menu.rst b/Documentation/pycardium/simple_menu.rst
index 36758c643e7356e44c4343260c2aa529f651e9f1..58236e2c4691d91d5a0fe36e9a4862f50aeff748 100644
--- a/Documentation/pycardium/simple_menu.rst
+++ b/Documentation/pycardium/simple_menu.rst
@@ -25,4 +25,6 @@ displaying menus.  You can use it like this:
 .. autoclass:: simple_menu.Menu
    :members:
 
+.. autodata:: simple_menu.TIMEOUT
+
 .. autofunction:: simple_menu.button_events
diff --git a/epicardium/ble/card10.c b/epicardium/ble/card10.c
index 2105ac02ca03e168fe8711f8c2797799e6801070..78e128bb4e82ac74133614cad0ef56cd0f198d89 100644
--- a/epicardium/ble/card10.c
+++ b/epicardium/ble/card10.c
@@ -256,228 +256,272 @@ static uint16_t initLightSensorLen = sizeof(initLightSensorValue);
  */
 
 static const attsAttr_t card10SvcAttrList[] = {
-	{ .pUuid       = attPrimSvcUuid,
-	  .pValue      = (uint8_t *)UUID_svc,
-	  .pLen        = (uint16_t *)&UUID_len,
-	  .maxLen      = sizeof(UUID_svc),
-	  .permissions = ATTS_PERMIT_READ },
+	{
+		.pUuid       = attPrimSvcUuid,
+		.pValue      = (uint8_t *)UUID_svc,
+		.pLen        = (uint16_t *)&UUID_len,
+		.maxLen      = sizeof(UUID_svc),
+		.permissions = ATTS_PERMIT_READ,
+	},
 
 	// TIME
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_time,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_time),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_time,
-	  .pValue   = timeValue,
-	  .pLen     = &timeLen,
-	  .maxLen   = sizeof(uint64_t),
-	  .settings = (ATTS_SET_WRITE_CBACK | ATTS_SET_READ_CBACK),
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH | ATTS_PERMIT_READ |
-		   ATTS_PERMIT_READ_ENC | ATTS_PERMIT_READ_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_time,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_time),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_time,
+		.pValue      = timeValue,
+		.pLen        = &timeLen,
+		.maxLen      = sizeof(uint64_t),
+		.settings    = ATTS_SET_WRITE_CBACK | ATTS_SET_READ_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH | ATTS_PERMIT_READ |
+			       ATTS_PERMIT_READ_ENC | ATTS_PERMIT_READ_AUTH,
+	},
 
 	// VIBRA
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_vibra,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_vibra),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_vibra,
-	  .pValue   = NULL,
-	  .maxLen   = sizeof(uint16_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_vibra,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_vibra),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_vibra,
+		.pValue      = NULL,
+		.maxLen      = sizeof(uint16_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// ROCKETS
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_rockets,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_rockets),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_rockets,
-	  .pValue   = NULL,
-	  .maxLen   = 3 * sizeof(uint8_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_rockets,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_rockets),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_rockets,
+		.pValue      = NULL,
+		.maxLen      = 3 * sizeof(uint8_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// BG LED Bottom left
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_led_bg_bottom_left,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_led_bg_bottom_left),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_led_bg_bottom_left,
-	  .pValue   = NULL,
-	  .maxLen   = 3 * sizeof(uint8_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_led_bg_bottom_left,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_led_bg_bottom_left),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_led_bg_bottom_left,
+		.pValue      = NULL,
+		.maxLen      = 3 * sizeof(uint8_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// BG LED Bottom right
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_led_bg_bottom_right,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_led_bg_bottom_right),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_led_bg_bottom_right,
-	  .pValue   = NULL,
-	  .maxLen   = 3 * sizeof(uint8_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_led_bg_bottom_right,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_led_bg_bottom_right),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_led_bg_bottom_right,
+		.pValue      = NULL,
+		.maxLen      = 3 * sizeof(uint8_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// BG LED top right
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_led_bg_top_right,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_led_bg_top_right),
-	  .settings    = 0,
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_led_bg_top_right,
-	  .pValue   = NULL,
-	  .maxLen   = 3 * sizeof(uint8_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_led_bg_top_right,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_led_bg_top_right),
+		.settings    = 0,
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_led_bg_top_right,
+		.pValue      = NULL,
+		.maxLen      = 3 * sizeof(uint8_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// BG LED top left
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_led_bg_top_left,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_led_bg_top_left),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_led_bg_top_left,
-	  .pValue   = NULL,
-	  .maxLen   = 3 * sizeof(uint8_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_led_bg_top_left,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_led_bg_top_left),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_led_bg_top_left,
+		.pValue      = NULL,
+		.maxLen      = 3 * sizeof(uint8_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// Dim bottom module
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_leds_bottom_dim,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_leds_bottom_dim),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_leds_bottom_dim,
-	  .pValue   = NULL,
-	  .pLen     = 0,
-	  .maxLen   = sizeof(uint8_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_leds_bottom_dim,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_leds_bottom_dim),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_leds_bottom_dim,
+		.pValue      = NULL,
+		.pLen        = 0,
+		.maxLen      = sizeof(uint8_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// Dim top module
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_leds_top_dim,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_leds_top_dim),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_leds_top_dim,
-	  .pValue   = NULL,
-	  .maxLen   = sizeof(uint8_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_leds_top_dim,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_leds_top_dim),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_leds_top_dim,
+		.pValue      = NULL,
+		.maxLen      = sizeof(uint8_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// led powersafe
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_led_powersafe,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_led_powersafe),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_led_powersafe,
-	  .pValue   = NULL,
-	  .maxLen   = sizeof(uint8_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_led_powersafe,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_led_powersafe),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_led_powersafe,
+		.pValue      = NULL,
+		.maxLen      = sizeof(uint8_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// flashlight
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_flashlight,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_flashlight),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_flashlight,
-	  .pValue   = NULL,
-	  .maxLen   = sizeof(uint8_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_flashlight,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_flashlight),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_flashlight,
+		.pValue      = NULL,
+		.maxLen      = sizeof(uint8_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// personal state
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_personal_state,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_personal_state),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_personal_state,
-	  .pValue   = &personalStateValue,
-	  .pLen     = &personalStateLen,
-	  .maxLen   = sizeof(uint16_t),
-	  .settings = (ATTS_SET_WRITE_CBACK | ATTS_SET_READ_CBACK),
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH | ATTS_PERMIT_READ |
-		   ATTS_PERMIT_READ_ENC | ATTS_PERMIT_READ_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_personal_state,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_personal_state),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_personal_state,
+		.pValue      = &personalStateValue,
+		.pLen        = &personalStateLen,
+		.maxLen      = sizeof(uint16_t),
+		.settings    = ATTS_SET_WRITE_CBACK | ATTS_SET_READ_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH | ATTS_PERMIT_READ |
+			       ATTS_PERMIT_READ_ENC | ATTS_PERMIT_READ_AUTH,
+	},
 
 	// ABOVE LEDS
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_leds_above,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_leds_above),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_leds_above,
-	  .pValue   = NULL,
-	  .maxLen   = 11 * 3 * sizeof(uint8_t),
-	  .settings = ATTS_SET_WRITE_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-		   ATTS_PERMIT_WRITE_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_leds_above,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_leds_above),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_leds_above,
+		.pValue      = NULL,
+		.maxLen      = 11 * 3 * sizeof(uint8_t),
+		.settings    = ATTS_SET_WRITE_CBACK,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
 
 	// Light sensor
 
-	{ .pUuid       = attChUuid,
-	  .pValue      = (uint8_t *)UUID_char_light_sensor,
-	  .pLen        = (uint16_t *)&UUID_char_len,
-	  .maxLen      = sizeof(UUID_char_light_sensor),
-	  .permissions = ATTS_PERMIT_READ },
-	{ .pUuid    = UUID_attChar_light_sensor,
-	  .pValue   = initLightSensorValue,
-	  .pLen     = &initLightSensorLen,
-	  .maxLen   = sizeof(uint8_t),
-	  .settings = ATTS_SET_READ_CBACK,
-	  .permissions =
-		  (ATTS_PERMIT_READ | ATTS_PERMIT_READ_ENC |
-		   ATTS_PERMIT_READ_AUTH) },
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)UUID_char_light_sensor,
+		.pLen        = (uint16_t *)&UUID_char_len,
+		.maxLen      = sizeof(UUID_char_light_sensor),
+		.permissions = ATTS_PERMIT_READ,
+	},
+	{
+		.pUuid       = UUID_attChar_light_sensor,
+		.pValue      = initLightSensorValue,
+		.pLen        = &initLightSensorLen,
+		.maxLen      = sizeof(uint8_t),
+		.settings    = ATTS_SET_READ_CBACK,
+		.permissions = ATTS_PERMIT_READ | ATTS_PERMIT_READ_ENC |
+			       ATTS_PERMIT_READ_AUTH,
+	},
 };
 
 // validating, that the service really get all charateristics
diff --git a/epicardium/ble/filetransfer.c b/epicardium/ble/filetransfer.c
index fa2cff4d3e8f884a6f382f469fab13c7126045cb..88b31308bf19cf9c02f45615a34b58fa19d5300f 100644
--- a/epicardium/ble/filetransfer.c
+++ b/epicardium/ble/filetransfer.c
@@ -143,7 +143,8 @@ static const attsAttr_t fileTransCfgList[] = {
 		.pLen        = NULL,
 		.maxLen      = 128,
 		.settings    = ATTS_SET_WRITE_CBACK | ATTS_SET_VARIABLE_LEN,
-		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_AUTH,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
 	},
 	/* File transfer Central RX characteristic */
 	{
@@ -161,7 +162,8 @@ static const attsAttr_t fileTransCfgList[] = {
 		.pLen        = &attRxChConfigValue_len,
 		.maxLen      = sizeof(attRxChConfigValue),
 		.settings    = ATTS_SET_VARIABLE_LEN,
-		.permissions = ATTS_PERMIT_READ | ATTS_PERMIT_READ_AUTH,
+		.permissions = ATTS_PERMIT_READ | ATTS_PERMIT_READ_ENC |
+			       ATTS_PERMIT_READ_AUTH,
 	},
 	/* File transfer Central RX notification channel */
 	{
@@ -170,8 +172,9 @@ static const attsAttr_t fileTransCfgList[] = {
 		.pLen        = &attRxChConfigValue_len,
 		.maxLen      = sizeof(attRxChConfigValue),
 		.settings    = ATTS_SET_CCC,
-		.permissions = ATTS_PERMIT_READ | ATTS_PERMIT_READ_AUTH |
-			       ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_AUTH,
+		.permissions = ATTS_PERMIT_READ | ATTS_PERMIT_READ_ENC |
+			       ATTS_PERMIT_READ_AUTH | ATTS_PERMIT_WRITE |
+			       ATTS_PERMIT_WRITE_ENC | ATTS_PERMIT_WRITE_AUTH,
 	},
 };
 
diff --git a/epicardium/ble/uart.c b/epicardium/ble/uart.c
index 584ae0ea211cc6728963bb6d3669344307ad527f..5fdfb74ac9885975e125f51216bd2953986eca9c 100644
--- a/epicardium/ble/uart.c
+++ b/epicardium/ble/uart.c
@@ -31,66 +31,84 @@ enum { UART_SVC_HDL = UART_START_HDL, /*!< \brief UART service declaration */
 
 /* 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);
 
 static const uint8_t uartRxCh[] = {ATT_PROP_WRITE, UINT16_TO_BYTES(UART_RX_HDL), 0x9E,0xCA,0xDC,0x24,0x0E,0xE5,0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x02,0x00,0x40,0x6E};
-const uint8_t attUartRxChUuid[] = {0x9E,0xCA,0xDC,0x24,0x0E,0xE5, 0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x02,0x00,0x40,0x6E};
+static const uint16_t uartRxCh_len = sizeof(uartRxCh);
+static const uint8_t attUartRxChUuid[] = {0x9E,0xCA,0xDC,0x24,0x0E,0xE5, 0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x02,0x00,0x40,0x6E};
 
 static const uint8_t uartTxCh[] = {ATT_PROP_READ | ATT_PROP_NOTIFY, UINT16_TO_BYTES(UART_TX_HDL), 0x9E,0xCA,0xDC,0x24,0x0E,0xE5,0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x03,0x00,0x40,0x6E};
-const uint8_t attUartTxChUuid[] = {0x9E,0xCA,0xDC,0x24,0x0E,0xE5, 0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x03,0x00,0x40,0x6E};
-/* clang-format on */
+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 void *SvcUARTAddGroupDyn(void)
-{
-	void *pSHdl;
-	uint8_t initCcc[] = { UINT16_TO_BYTES(0x0000) };
-
-	/* Create the service */
-	pSHdl = AttsDynCreateGroup(UART_START_HDL, UART_END_HDL);
-
-	if (pSHdl != NULL) {
-		/* clang-format off */
-		/* Primary service */
-		AttsDynAddAttrConst( pSHdl, attPrimSvcUuid, UARTSvc, sizeof(UARTSvc),
-			0, ATTS_PERMIT_READ);
-
-		/* UART rx characteristic */
-		AttsDynAddAttrConst( pSHdl, attChUuid, uartRxCh, sizeof(uartRxCh),
-			0, ATTS_PERMIT_READ);
-		// XXX: attUartRxChUuid is 16 bytes but nothing says so....
-		/* UART rx value */
-		// XXX: not sure if max value of 128 is fine...
-		AttsDynAddAttr( pSHdl, attUartRxChUuid, NULL, 0, 128,
-			ATTS_SET_WRITE_CBACK | ATTS_SET_VARIABLE_LEN, ATTS_PERMIT_WRITE);
-
-		/* UART tx characteristic */
-		AttsDynAddAttrConst( pSHdl, attChUuid, uartTxCh, sizeof(uartTxCh),
-			0, ATTS_PERMIT_READ);
-		/* UART tx value */
-		/* TODO: do we need ATTS_SET_READ_CBACK ? */
-		AttsDynAddAttr( pSHdl, attUartTxChUuid, NULL, 0, sizeof(uint8_t),
-			ATTS_SET_READ_CBACK, ATTS_PERMIT_READ);
-		/* UART tx CCC descriptor */
-		AttsDynAddAttr( pSHdl, attCliChCfgUuid, initCcc, sizeof(uint16_t), sizeof(uint16_t),
-			ATTS_SET_CCC, ATTS_PERMIT_READ | ATTS_PERMIT_WRITE);
-		/* clang-format on */
-	}
+static uint8_t ble_uart_tx_buf[128];
+static uint16_t ble_uart_buf_tx_fill = 0;
+/* clang-format on */
 
-	return pSHdl;
-}
+/* Attribute list for uriCfg group */
+static const attsAttr_t uartAttrCfgList[] = {
+	/* Primary service */
+	{
+		.pUuid       = attPrimSvcUuid,
+		.pValue      = (uint8_t *)UARTSvc,
+		.pLen        = (uint16_t *)&UARTSvc_len,
+		.maxLen      = sizeof(UARTSvc),
+		.settings    = 0,
+		.permissions = ATTS_PERMIT_READ,
+	},
+	/* UART rx characteristic */
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)uartRxCh,
+		.pLen        = (uint16_t *)&uartRxCh_len,
+		.maxLen      = sizeof(uartRxCh),
+		.settings    = 0,
+		.permissions = ATTS_PERMIT_READ,
+	},
+	/* UART rx value */
+	{
+		.pUuid       = attUartRxChUuid,
+		.pValue      = NULL,
+		.pLen        = NULL,
+		.maxLen      = 128,
+		.settings    = ATTS_SET_WRITE_CBACK | ATTS_SET_VARIABLE_LEN,
+		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
+			       ATTS_PERMIT_WRITE_AUTH,
+	},
+	/* UART tx characteristic */
+	{
+		.pUuid       = attChUuid,
+		.pValue      = (uint8_t *)uartTxCh,
+		.pLen        = (uint16_t *)&uartTxCh_len,
+		.maxLen      = sizeof(uartTxCh),
+		.settings    = 0,
+		.permissions = ATTS_PERMIT_READ,
+	},
+	/* UART tx value */
+	{
+		.pUuid       = attUartTxChUuid,
+		.pValue      = ble_uart_tx_buf,
+		.pLen        = &ble_uart_buf_tx_fill,
+		.maxLen      = sizeof(ble_uart_tx_buf),
+		.settings    = 0,
+		.permissions = ATTS_PERMIT_READ | ATTS_PERMIT_READ_ENC |
+			       ATTS_PERMIT_READ_AUTH,
+	},
+	/* 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,
+	},
+};
 
 dmConnId_t active_connection = 0;
 
-static uint8_t UARTReadCback(
-	dmConnId_t connId,
-	uint16_t handle,
-	uint8_t operation,
-	uint16_t offset,
-	attsAttr_t *pAttr
-) {
-	printf("read callback\n");
-	return ATT_SUCCESS;
-}
-
 static uint8_t UARTWriteCback(
 	dmConnId_t connId,
 	uint16_t handle,
@@ -119,8 +137,6 @@ static uint8_t UARTWriteCback(
 	return ATT_SUCCESS;
 }
 
-static uint8_t ble_uart_tx_buf[128];
-static uint8_t ble_uart_buf_tx_fill;
 static int ble_uart_lasttick = 0;
 
 void ble_uart_write(uint8_t *pValue, uint8_t len)
@@ -134,11 +150,6 @@ void ble_uart_write(uint8_t *pValue, uint8_t len)
 		if (ble_uart_buf_tx_fill == 128 || pValue[i] == '\r' ||
 		    pValue[i] == '\n') {
 			if (ble_uart_buf_tx_fill > 0) {
-				AttsSetAttr(
-					UART_TX_HDL,
-					ble_uart_buf_tx_fill,
-					ble_uart_tx_buf
-				);
 				if (active_connection) {
 					int x = xTaskGetTickCount() -
 						ble_uart_lasttick;
@@ -165,11 +176,15 @@ void ble_uart_write(uint8_t *pValue, uint8_t len)
 	}
 }
 
+static attsGroup_t uartCfgGroup = {
+	.pAttr       = (attsAttr_t *)uartAttrCfgList,
+	.writeCback  = UARTWriteCback,
+	.startHandle = UART_START_HDL,
+	.endHandle   = UART_END_HDL,
+};
+
 void bleuart_init(void)
 {
-	/* Add the UART service dynamically */
-	void *pSHdl;
-	pSHdl = SvcUARTAddGroupDyn();
-	AttsDynRegister(pSHdl, UARTReadCback, UARTWriteCback);
-	//AttsDynRegister(pSHdl, NULL, UARTWriteCback);
+	/* Add the UART service */
+	AttsAddGroup(&uartCfgGroup);
 }
diff --git a/epicardium/main.c b/epicardium/main.c
index 56639ea56ead01fc06138e0f2f712518193c475f..51edbe93d6f1abe06bf5a15fbb01bf48f99ff383 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -1,6 +1,7 @@
 #include "modules/modules.h"
 #include "modules/log.h"
 #include "modules/filesystem.h"
+#include "modules/config.h"
 #include "card10-version.h"
 
 #include "FreeRTOS.h"
@@ -20,14 +21,16 @@ int main(void)
 	LOG_DEBUG("startup", "Initializing hardware ...");
 	hardware_early_init();
 
+	load_config();
+
 	/*
 	 * Version Splash
 	 */
 	const char *version_buf = CARD10_VERSION;
 	const int off           = (160 - (int)strlen(version_buf) * 14) / 2;
-	epic_disp_clear(0x9dc0);
-	epic_disp_print(10, 20, "Epicardium", 0x6c20, 0x9dc0);
-	epic_disp_print(off > 0 ? off : 0, 40, version_buf, 0x6c20, 0x9dc0);
+	epic_disp_clear(0x0000);
+	epic_disp_print(10, 20, "Epicardium", 0xfe20, 0x0000);
+	epic_disp_print(off > 0 ? off : 0, 40, version_buf, 0xfe20, 0x0000);
 	epic_disp_update();
 	mxc_delay(2000000);
 
diff --git a/epicardium/meson.build b/epicardium/meson.build
index cf67023b9f7143336875d973108d8ee0a080ffd1..a6a6c05261c16ced283c4d8591b335237f6acd37 100644
--- a/epicardium/meson.build
+++ b/epicardium/meson.build
@@ -70,7 +70,7 @@ subdir('ble/')
 
 subdir('l0der/')
 
-epicardium_cargs = []
+epicardium_cargs = ['-D_POSIX_C_SOURCE=200809']
 if get_option('jailbreak_card10')
   epicardium_cargs += [
     '-DJAILBREAK_CARD10=1',
diff --git a/epicardium/modules/config.c b/epicardium/modules/config.c
new file mode 100644
index 0000000000000000000000000000000000000000..47396e2195b8b262f41279dbdad4e588430efdef
--- /dev/null
+++ b/epicardium/modules/config.c
@@ -0,0 +1,347 @@
+#include "modules/log.h"
+#include "modules/config.h"
+#include "modules/filesystem.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#define CONFIG_MAX_LINE_LENGTH 80
+
+enum OptionType {
+	OptionType_Boolean,
+	OptionType_Int,
+	OptionType_Float,
+	OptionType_String,
+};
+
+struct config_option {
+	const char *name;
+	enum OptionType type;
+	union {
+		bool boolean;
+		long integer;
+		double floating_point;
+		char *string;
+	} value;
+};
+
+static struct config_option s_options[_EpicOptionCount] = {
+/* clang-format off */
+	#define INIT_Boolean(v)		        { .boolean        = (v) }
+	#define INIT_Int(v)  		        { .integer        = (v) }
+	#define INIT_Float(v)  		        { .floating_point = (v) }
+	#define INIT_String(v)  	        { .string         = (v) }
+	#define INIT_(tp, v)                INIT_ ## tp (v)
+	#define INIT(tp, v)                 INIT_ (tp, v)
+
+	#define CARD10_SETTING(identifier, spelling, tp, default_value)     \
+		[Option ## identifier] = { .name  = (spelling),                 \
+					               .type  = OptionType_ ## tp,          \
+					               .value = INIT(tp, (default_value)) },
+
+	#include "modules/config.def"
+	/* clang-format on */
+};
+
+static struct config_option *findOption(const char *key)
+{
+	for (int i = 0; i < _EpicOptionCount; ++i) {
+		if (!strcmp(key, s_options[i].name)) {
+			return &s_options[i];
+		}
+	}
+	return NULL;
+}
+
+static bool set_bool(struct config_option *opt, const char *value)
+{
+	bool val;
+	if (!strcmp(value, "1")) {
+		val = true;
+	} else if (!strcmp(value, "true")) {
+		val = true;
+	} else if (!strcmp(value, "0")) {
+		val = false;
+	} else if (!strcmp(value, "false")) {
+		val = false;
+	} else {
+		return false;
+	}
+	opt->value.boolean = val;
+	LOG_DEBUG(
+		"card10.cfg",
+		"setting '%s' to %s",
+		opt->name,
+		val ? "true" : "false"
+	);
+	return true;
+}
+
+static bool set_int(struct config_option *opt, const char *value)
+{
+	char *endptr;
+	size_t len = strlen(value);
+	int v      = strtol(value, &endptr, 0);
+	if (endptr != (value + len)) {
+		return false;
+	}
+	opt->value.integer = v;
+	LOG_DEBUG("card10.cfg", "setting '%s' to %d (0x%08x)", opt->name, v, v);
+	return true;
+}
+
+static bool set_float(struct config_option *opt, const char *value)
+{
+	char *endptr;
+	size_t len = strlen(value);
+	double v   = strtod(value, &endptr);
+	if (endptr != (value + len)) {
+		return false;
+	}
+	opt->value.floating_point = v;
+	LOG_DEBUG("card10.cfg", "setting '%s' to %f", opt->name, v);
+	return true;
+}
+
+const char *elide(const char *str)
+{
+	static char ret[21];
+	size_t len = strlen(str);
+	if (len <= 20) {
+		return str;
+	}
+	strncpy(ret, str, 17);
+	ret[17] = '.';
+	ret[18] = '.';
+	ret[19] = '.';
+	ret[20] = '\0';
+	return ret;
+}
+
+static bool set_string(struct config_option *opt, const char *value)
+{
+	//this leaks, but the lifetime of these ends when epicardium exits, so...
+	char *leaks       = strdup(value);
+	opt->value.string = leaks;
+	LOG_DEBUG("card10.cfg", "setting '%s' to %s", opt->name, elide(leaks));
+	return true;
+}
+
+static void configure(const char *key, const char *value, int lineNumber)
+{
+	struct config_option *opt = findOption(key);
+	if (!opt) {
+		//invalid key
+		LOG_WARN(
+			"card10.cfg",
+			"line %d: ignoring unknown option '%s'",
+			lineNumber,
+			key
+		);
+		return;
+	}
+	bool ok = false;
+	switch (opt->type) {
+	case OptionType_Boolean:
+		ok = set_bool(opt, value);
+		break;
+	case OptionType_Int:
+		ok = set_int(opt, value);
+		break;
+	case OptionType_Float:
+		ok = set_float(opt, value);
+		break;
+	case OptionType_String:
+		ok = set_string(opt, value);
+		break;
+	default:
+		assert(0 && "unreachable");
+	}
+	if (!ok) {
+		LOG_WARN(
+			"card10.cfg",
+			"line %d: ignoring invalid value '%s' for option '%s'",
+			lineNumber,
+			value,
+			key
+		);
+	}
+}
+
+static void doline(char *line, char *eol, int lineNumber)
+{
+	//skip leading whitespace
+	while (*line && isspace(*line))
+		++line;
+
+	char *key = line;
+	if (*key == '#') {
+		//skip comments
+		return;
+	}
+
+	char *eq = strchr(line, '=');
+	if (!eq) {
+		if (*key) {
+			LOG_WARN(
+				"card10.cfg",
+				"line %d (%s): syntax error",
+				lineNumber,
+				elide(line)
+			);
+		}
+		return;
+	}
+
+	char *e_key = eq - 1;
+	//skip trailing whitespace in key
+	while (e_key > key && isspace(*e_key))
+		--e_key;
+	e_key[1] = '\0';
+	if (*key == '\0') {
+		LOG_WARN("card10.cfg", "line %d: empty key", lineNumber);
+		return;
+	}
+
+	char *value = eq + 1;
+	//skip leading whitespace
+	while (*value && isspace(*value))
+		++value;
+
+	char *e_val = eol - 1;
+	//skip trailing whitespace
+	while (e_val > value && isspace(*e_val))
+		--e_val;
+	if (*value == '\0') {
+		LOG_WARN(
+			"card10.cfg",
+			"line %d: empty value for option '%s'",
+			lineNumber,
+			key
+		);
+		return;
+	}
+
+	configure(key, value, lineNumber);
+}
+
+bool config_get_boolean(enum EpicConfigOption option)
+{
+	struct config_option *opt = &s_options[option];
+	assert(opt->type == OptionType_Boolean);
+	return opt->value.boolean;
+}
+
+long config_get_integer(enum EpicConfigOption option)
+{
+	struct config_option *opt = &s_options[option];
+	assert(opt->type == OptionType_Int);
+	return opt->value.integer;
+}
+
+double config_get_float(enum EpicConfigOption option)
+{
+	struct config_option *opt = &s_options[option];
+	assert(opt->type == OptionType_Float);
+	return opt->value.floating_point;
+}
+
+const char *config_get_string(enum EpicConfigOption option)
+{
+	struct config_option *opt = &s_options[option];
+	assert(opt->type == OptionType_String);
+	return opt->value.string;
+}
+
+void load_config(void)
+{
+	LOG_DEBUG("card10.cfg", "loading...");
+	int fd = epic_file_open("card10.cfg", "r");
+	if (fd < 0) {
+		LOG_DEBUG(
+			"card10.cfg",
+			"loading failed: %s (%d)",
+			strerror(-fd),
+			fd
+		);
+		return;
+	}
+	char buf[CONFIG_MAX_LINE_LENGTH];
+	int lineNumber = 0;
+	int nread;
+	do {
+		//zero-terminate in case file is empty
+		buf[0] = '\0';
+		nread  = epic_file_read(fd, buf, sizeof(buf));
+		if (nread < sizeof(buf)) {
+			//add fake EOL to ensure termination
+			buf[nread] = '\n';
+		}
+		char *line   = buf;
+		char *eol    = NULL;
+		int last_eol = 0;
+		while (line) {
+			//line points one character past the las (if any) '\n' hence '- 1'
+			last_eol = line - buf - 1;
+			eol      = strchr(line, '\n');
+			++lineNumber;
+			if (eol) {
+				*eol = '\0';
+				doline(line, eol, lineNumber);
+				line = eol + 1;
+			} else {
+				if (line == buf) {
+					//line did not fit into buf
+					LOG_WARN(
+						"card10.cfg",
+						"line:%d: too long - aborting",
+						lineNumber
+					);
+					return;
+				} else {
+					int seek_back = last_eol - nread;
+					LOG_DEBUG(
+						"card10.cfg",
+						"nread, last_eol, seek_back: %d,%d,%d",
+						nread,
+						last_eol,
+						seek_back
+					);
+					assert(seek_back <= 0);
+					if (seek_back) {
+						int rc = epic_file_seek(
+							fd,
+							seek_back,
+							SEEK_CUR
+						);
+						if (rc < 0) {
+							LOG_ERR("card10.cfg",
+								"seek failed, aborting");
+							return;
+						}
+						char newline;
+						rc = epic_file_read(
+							fd, &newline, 1
+						);
+						if (rc < 0 || newline != '\n') {
+							LOG_ERR("card10.cfg",
+								"seek failed, aborting");
+							LOG_DEBUG(
+								"card10.cfg",
+								"seek failed at read-back of newline: rc: %d read: %d",
+								rc,
+								(int)newline
+							);
+							return;
+						}
+					}
+					break;
+				}
+			}
+		}
+	} while (nread == sizeof(buf));
+}
diff --git a/epicardium/modules/config.def b/epicardium/modules/config.def
new file mode 100644
index 0000000000000000000000000000000000000000..455aaedd8d61f821c1d08ee99bb45c3a61d983ba
--- /dev/null
+++ b/epicardium/modules/config.def
@@ -0,0 +1,11 @@
+#ifndef CARD10_SETTING
+#  define CARD10_SETTING(identifier, spelling, type, default_value)
+#endif
+
+CARD10_SETTING(ExecuteElf, "execute_elf", Boolean, false)
+//CARD10_SETTING(Nick, "nick", String, "an0n")
+//CARD10_SETTING(Timeout, "timeout", Integer, 123)
+//CARD10_SETTING(Dampening, "dampening", Float, 420)
+
+
+#undef CARD10_SETTING
diff --git a/epicardium/modules/config.h b/epicardium/modules/config.h
new file mode 100644
index 0000000000000000000000000000000000000000..b72b08a5205bf9344bc3e3d805423dc826f30ccc
--- /dev/null
+++ b/epicardium/modules/config.h
@@ -0,0 +1,21 @@
+#ifndef EPICARDIUM_MODULES_CONFIG_H_INCLUDED
+#define EPICARDIUM_MODULES_CONFIG_H_INCLUDED
+
+#include <stdbool.h>
+
+enum EpicConfigOption {
+    #define CARD10_SETTING(identifier, spelling, type, default_value) Option ## identifier,
+	#include "modules/config.def"
+    _EpicOptionCount
+};
+
+//initialize configuration values and load card10.cfg
+void load_config(void);
+
+bool config_get_boolean(enum EpicConfigOption option);
+long config_get_integer(enum EpicConfigOption option);
+double config_get_float(enum EpicConfigOption option);
+const char* config_get_string(enum EpicConfigOption option);
+
+
+#endif//EPICARDIUM_MODULES_CONFIG_H_INCLUDED
diff --git a/epicardium/modules/lifecycle.c b/epicardium/modules/lifecycle.c
index 375cfef61dd9972fbb418f72659a1e30e69ef615..650664d2e97ee15ecbd35199e176ded925a78bfb 100644
--- a/epicardium/modules/lifecycle.c
+++ b/epicardium/modules/lifecycle.c
@@ -1,6 +1,7 @@
 #include "epicardium.h"
 #include "modules/log.h"
 #include "modules/modules.h"
+#include "modules/config.h"
 #include "api/dispatcher.h"
 #include "api/interrupt-sender.h"
 #include "l0der/l0der.h"
@@ -49,6 +50,7 @@ static volatile struct load_info async_load = {
 
 /* Whether to write the menu script before attempting to load. */
 static volatile bool write_menu = false;
+static bool execute_elfs        = false;
 
 /* Helpers {{{ */
 
@@ -88,9 +90,7 @@ static int load_stat(char *name)
  */
 static int do_load(struct load_info *info)
 {
-#if defined(JAILBREAK_CARD10) && (JAILBREAK_CARD10 == 1)
 	struct l0dable_info l0dable;
-#endif
 	int res;
 
 	if (*info->name == '\0') {
@@ -129,18 +129,22 @@ static int do_load(struct load_info *info)
 	case PL_PYTHON_INTERP:
 		core1_load(PYCARDIUM_IVT, info->name);
 		break;
-#if defined(JAILBREAK_CARD10) && (JAILBREAK_CARD10 == 1)
 	case PL_L0DABLE:
-		res = l0der_load_path(info->name, &l0dable);
-		if (res != 0) {
-			LOG_ERR("lifecycle", "l0der failed: %d\n", res);
-			xSemaphoreGive(api_mutex);
-			return -ENOEXEC;
+		if (execute_elfs) {
+			res = l0der_load_path(info->name, &l0dable);
+			if (res != 0) {
+				LOG_ERR("lifecycle", "l0der failed: %d\n", res);
+				xSemaphoreGive(api_mutex);
+				return -ENOEXEC;
+			}
+			core1_load(l0dable.isr_vector, "");
+		} else {
+			LOG_WARN(
+				"lifecycle",
+				"Execution of .elf l0dables is disabled"
+			);
 		}
-		core1_load(l0dable.isr_vector, "");
-
 		break;
-#endif
 	default:
 		LOG_ERR("lifecyle",
 			"Attempted to load invalid payload (%s)",
@@ -379,6 +383,8 @@ void vLifecycleTask(void *pvParameters)
 
 	hardware_init();
 
+	execute_elfs = config_get_boolean(OptionExecuteElf);
+
 	/* When triggered, reset core 1 to menu */
 	while (1) {
 		ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index 693ca9a8f94a432ff64d770f91d9e9005c88d42a..628249a305098911b6dfe3c861b8759d6d5a5a62 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -21,5 +21,6 @@ module_sources = files(
   'trng.c',
   'vibra.c',
   'watchdog.c',
-  'usb.c'
+  'usb.c',
+  'config.c',
 )
diff --git a/preload/apps/personal_state/__init__.py b/preload/apps/personal_state/__init__.py
index 7605652c95070dba46cb8e572e48e2ebd2f6c641..f636e0c43cccfe9bd1781833a04b8776021e97ed 100644
--- a/preload/apps/personal_state/__init__.py
+++ b/preload/apps/personal_state/__init__.py
@@ -1,13 +1,11 @@
 """
 Personal State Script
-===========
-With this script you can 
+=====================
 """
-import buttons
 import color
-import display
 import os
 import personal_state
+import simple_menu
 
 states = [
     ("No State", personal_state.NO_STATE),
@@ -18,75 +16,35 @@ states = [
 ]
 
 
-def button_events():
-    """Iterate over button presses (event-loop)."""
-    yield 0
-    button_pressed = False
-    while True:
-        v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
+class StateMenu(simple_menu.Menu):
+    color_sel = color.WHITE
 
-        if v == 0:
-            button_pressed = False
+    def on_scroll(self, item, index):
+        personal_state.set(item[1], False)
 
-        if not button_pressed and v & buttons.BOTTOM_LEFT != 0:
-            button_pressed = True
-            yield buttons.BOTTOM_LEFT
+    def on_select(self, item, index):
+        personal_state.set(item[1], True)
+        os.exit()
 
-        if not button_pressed and v & buttons.BOTTOM_RIGHT != 0:
-            button_pressed = True
-            yield buttons.BOTTOM_RIGHT
+    def draw_entry(self, item, index, offset):
+        if item[1] == personal_state.NO_CONTACT:
+            bg = color.RED
+            fg = color.WHITE
+        elif item[1] == personal_state.CHAOS:
+            bg = color.CHAOSBLUE
+            fg = color.CHAOSBLUE_DARK
+        elif item[1] == personal_state.COMMUNICATION:
+            bg = color.COMMYELLOW
+            fg = color.COMMYELLOW_DARK
+        elif item[1] == personal_state.CAMP:
+            bg = color.CAMPGREEN
+            fg = color.CAMPGREEN_DARK
+        else:
+            bg = color.Color(100, 100, 100)
+            fg = color.Color(200, 200, 200)
 
-        if not button_pressed and v & buttons.TOP_RIGHT != 0:
-            button_pressed = True
-            yield buttons.TOP_RIGHT
-
-
-COLOR1, COLOR2 = (color.CHAOSBLUE_DARK, color.CHAOSBLUE)
-
-
-def draw_menu(disp, idx, offset):
-    disp.clear()
-
-    for y, i in enumerate(range(len(states) + idx - 3, len(states) + idx + 4)):
-        selected = states[i % len(states)]
-        disp.print(
-            " " + selected[0] + " " * (11 - len(selected[0])),
-            posy=offset + y * 20 - 40,
-            bg=COLOR1 if i % 2 == 0 else COLOR2,
-        )
-
-    disp.print(">", posy=20, fg=color.COMMYELLOW, bg=COLOR2 if idx % 2 == 0 else COLOR1)
-    disp.update()
-
-
-def main():
-    disp = display.open()
-    numstates = len(states)
-
-    current, _ = personal_state.get()
-    for ev in button_events():
-        if ev == buttons.BOTTOM_RIGHT:
-            # Scroll down
-            draw_menu(disp, current, -8)
-            current = (current + 1) % numstates
-            state = states[current]
-            personal_state.set(state[1], False)
-        elif ev == buttons.BOTTOM_LEFT:
-            # Scroll up
-            draw_menu(disp, current, 8)
-            current = (current + numstates - 1) % numstates
-            state = states[current]
-            personal_state.set(state[1], False)
-        elif ev == buttons.TOP_RIGHT:
-            state = states[current]
-            personal_state.set(state[1], True)
-            # Select & start
-            disp.clear().update()
-            disp.close()
-            os.exit(0)
-
-        draw_menu(disp, current, 0)
+        self.disp.print(" " + str(item[0]) + " " * 9, posy=offset, fg=fg, bg=bg)
 
 
 if __name__ == "__main__":
-    main()
+    StateMenu(states).run()
diff --git a/preload/menu.py b/preload/menu.py
index b8479c1dbdf9600d2af700249a35a33a770e9ad1..e9077e3611920f1fdb74b33347fe8db4f88eca27 100644
--- a/preload/menu.py
+++ b/preload/menu.py
@@ -5,286 +5,91 @@ You can customize this script however you want :)  If you want to go back to
 the default version, just delete this file; the firmware will recreate it on
 next run.
 """
-import buttons
+import collections
 import color
 import display
 import os
-import utime
-import ujson
+import simple_menu
 import sys
+import ujson
+import utime
 
-BUTTON_TIMER_POPPED = -1
-COLOR_BG = color.CHAOSBLUE_DARK
-COLOR_BG_SEL = color.CHAOSBLUE
-COLOR_ARROW = color.COMMYELLOW
-COLOR_TEXT = color.COMMYELLOW
-MAXCHARS = 11
-HOMEAPP = "main.py"
-
-
-def create_folders():
-    try:
-        os.mkdir("/apps")
-    except:
-        pass
-
-
-def read_metadata(app_folder):
-    try:
-        info_file = "/apps/%s/metadata.json" % (app_folder)
-        with open(info_file) as f:
-            information = f.read()
-        return ujson.loads(information)
-    except Exception as e:
-        print("Failed to read metadata for %s" % (app_folder))
-        sys.print_exception(e)
-        return {
-            "author": "",
-            "name": app_folder,
-            "description": "",
-            "category": "",
-            "revision": 0,
-        }
+App = collections.namedtuple("App", ["name", "path"])
 
 
-def list_apps():
-    """Create a list of available apps."""
-    apps = []
+def enumerate_apps():
+    """List all installed apps."""
+    for f in os.listdir("/"):
+        if f == "main.py":
+            yield App("Home", f)
 
-    # add main application
-    for mainFile in os.listdir("/"):
-        if mainFile == HOMEAPP:
-            apps.append(
-                [
-                    "/%s" % HOMEAPP,
-                    {
-                        "author": "card10badge Team",
-                        "name": "Home",
-                        "description": "",
-                        "category": "",
-                        "revision": 0,
-                    },
-                ]
-            )
-
-    dirlist = [
-        entry for entry in sorted(os.listdir("/apps")) if not entry.startswith(".")
-    ]
+    for app in sorted(os.listdir("/apps")):
+        if app.startswith("."):
+            continue
 
-    # list all hatchary style apps (not .elf and not .py)
-    # with or without metadata.json
-    for appFolder in dirlist:
-        if not (appFolder.endswith(".py") or appFolder.endswith(".elf")):
-            metadata = read_metadata(appFolder)
-            if not metadata.get("bin", None):
-                fileName = "/apps/%s/__init__.py" % appFolder
-            else:
-                fileName = "/apps/%s/%s" % (appFolder, metadata["bin"])
-            apps.append([fileName, metadata])
+        if app.endswith(".py") or app.endswith(".elf"):
+            yield App(app, "/apps/" + app)
+            continue
 
-    # list simple python scripts
-    for pyFile in dirlist:
-        if pyFile.endswith(".py"):
-            apps.append(
-                [
-                    "/apps/%s" % pyFile,
-                    {
-                        "author": "",
-                        "name": pyFile,
-                        "description": "",
-                        "category": "",
-                        "revision": 0,
-                    },
-                ]
-            )
+        try:
+            with open("/apps/" + app + "/metadata.json") as f:
+                info = ujson.load(f)
 
-    # list simple elf binaries
-    for elfFile in dirlist:
-        if elfFile.endswith(".elf"):
-            apps.append(
-                [
-                    "/apps/%s" % elfFile,
-                    {
-                        "author": "",
-                        "name": elfFile,
-                        "description": "",
-                        "category": "",
-                        "revision": 0,
-                    },
-                ]
+            yield App(
+                info["name"], "/apps/{}/{}".format(app, info.get("bin", "__init__.py"))
             )
+        except Exception as e:
+            print("Could not load /apps/{}/metadata.json!".format(app))
+            sys.print_exception(e)
+
+
+class MainMenu(simple_menu.Menu):
+    timeout = 30.0
+
+    def entry2name(self, app):
+        return app.name
+
+    def on_select(self, app, index):
+        self.disp.clear().update()
+        try:
+            print("Trying to load " + app.path)
+            os.exec(app.path)
+        except OSError as e:
+            print("Loading failed: ")
+            sys.print_exception(e)
+            self.error("Loading", "failed")
+            utime.sleep(1.0)
+            os.exit(1)
+
+    def on_timeout(self):
+        try:
+            f = open("main.py")
+            f.close()
+            os.exec("main.py")
+        except OSError:
+            pass
+
+
+def no_apps_message():
+    """Display a warning if no apps are installed."""
+    with display.open() as disp:
+        disp.clear(color.COMMYELLOW)
+        disp.print(
+            " No apps ", posx=17, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
+        )
+        disp.print(
+            "available", posx=17, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
+        )
+        disp.update()
 
-    return apps
-
-
-def button_events(timeout=0):
-    """Iterate over button presses (event-loop)."""
-    yield 0
-    button_pressed = False
-    count = 0
     while True:
-        v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
-        if timeout > 0 and count > 0 and count % timeout == 0:
-            yield BUTTON_TIMER_POPPED
-
-        if timeout > 0:
-            count += 1
-
-        if v == 0:
-            button_pressed = False
-
-        if not button_pressed and v & buttons.BOTTOM_LEFT != 0:
-            button_pressed = True
-            yield buttons.BOTTOM_LEFT
-
-        if not button_pressed and v & buttons.BOTTOM_RIGHT != 0:
-            button_pressed = True
-            yield buttons.BOTTOM_RIGHT
-
-        if not button_pressed and v & buttons.TOP_RIGHT != 0:
-            button_pressed = True
-            yield buttons.TOP_RIGHT
-
-        utime.sleep_ms(10)
-
+        utime.sleep(0.5)
 
-def triangle(disp, x, y, left, scale=6, color=[255, 0, 0]):
-    """Draw a triangle to show there's more text in this line"""
-    yf = 1 if left else -1
-    disp.line(x - scale * yf, int(y + scale / 2), x, y, col=color)
-    disp.line(x, y, x, y + scale, col=color)
-    disp.line(x, y + scale, x - scale * yf, y + int(scale / 2), col=color)
 
+if __name__ == "__main__":
+    apps = list(enumerate_apps())
 
-def draw_menu(disp, applist, pos, appcount, lineoffset):
-    disp.clear()
-
-    start = 0
-    if pos > 0:
-        start = pos - 1
-    if start + 4 > appcount:
-        start = appcount - 4
-    if start < 0:
-        start = 0
-
-    for i, app in enumerate(applist):
-        if i >= start + 4 or i >= appcount:
-            break
-        if i >= start:
-            disp.rect(
-                0,
-                (i - start) * 20,
-                159,
-                (i - start) * 20 + 20,
-                col=COLOR_BG_SEL if i == pos else COLOR_BG,
-            )
-
-            line = app[1]["name"]
-            linelength = len(line)
-            off = 0
-
-            # calc line offset for scrolling
-            if i == pos and linelength > (MAXCHARS - 1) and lineoffset > 0:
-                off = (
-                    lineoffset
-                    if lineoffset + (MAXCHARS - 1) < linelength
-                    else linelength - (MAXCHARS - 1)
-                )
-            if lineoffset > linelength:
-                off = 0
-
-            disp.print(
-                " " + line[off : (off + (MAXCHARS - 1))],
-                posy=(i - start) * 20,
-                fg=COLOR_TEXT,
-                bg=COLOR_BG_SEL if i == pos else COLOR_BG,
-            )
-            if i == pos:
-                disp.print(">", posy=(i - start) * 20, fg=COLOR_ARROW, bg=COLOR_BG_SEL)
-
-            if linelength > (MAXCHARS - 1) and off < linelength - (MAXCHARS - 1):
-                triangle(disp, 153, (i - start) * 20 + 6, False, 6)
-                triangle(disp, 154, (i - start) * 20 + 7, False, 4)
-                triangle(disp, 155, (i - start) * 20 + 8, False, 2)
-            if off > 0:
-                triangle(disp, 24, (i - start) * 20 + 6, True, 6)
-                triangle(disp, 23, (i - start) * 20 + 7, True, 4)
-                triangle(disp, 22, (i - start) * 20 + 8, True, 2)
-
-    disp.update()
-
-
-def main():
-    create_folders()
-    disp = display.open()
-    applist = list_apps()
-    numapps = len(applist)
-    current = 0
-    lineoffset = 0
-    timerscrollspeed = 1
-    timerstartscroll = 5
-    timercountpopped = 0
-    timerinactivity = 100
-    for ev in button_events(10):
-        if numapps == 0:
-            disp.clear(COLOR_BG)
-            disp.print(" No apps ", posx=17, posy=20, fg=COLOR_TEXT, bg=COLOR_BG)
-            disp.print("available", posx=17, posy=40, fg=COLOR_TEXT, bg=COLOR_BG)
-            disp.update()
-            continue
-
-        if ev == buttons.BOTTOM_RIGHT:
-            # Scroll down
-            current = (current + 1) % numapps
-            lineoffset = 0
-            timercountpopped = 0
-
-        elif ev == buttons.BOTTOM_LEFT:
-            # Scroll up
-            current = (current + numapps - 1) % numapps
-            lineoffset = 0
-            timercountpopped = 0
-
-        elif ev == BUTTON_TIMER_POPPED:
-            timercountpopped += 1
-            if (
-                timercountpopped >= timerstartscroll
-                and (timercountpopped - timerstartscroll) % timerscrollspeed == 0
-            ):
-                lineoffset += 1
-
-            if applist[0][0] == "/%s" % HOMEAPP and timercountpopped >= timerinactivity:
-                print("Inactivity timer popped")
-                disp.clear().update()
-                disp.close()
-                try:
-                    os.exec("/%s" % HOMEAPP)
-                except OSError as e:
-                    print("Loading failed: ", e)
-                    os.exit(1)
-
-        elif ev == buttons.TOP_RIGHT:
-            # Select & start
-            disp.clear().update()
-            disp.close()
-            try:
-                os.exec(applist[current][0])
-            except OSError as e:
-                print("Loading failed: ", e)
-                os.exit(1)
-
-        draw_menu(disp, applist, current, numapps, lineoffset)
-
+    if apps == []:
+        no_apps_message()
 
-if __name__ == "__main__":
-    try:
-        main()
-    except Exception as e:
-        sys.print_exception(e)
-        with display.open() as d:
-            d.clear(color.COMMYELLOW)
-            d.print("Menu", posx=52, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW)
-            d.print("crashed", posx=31, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW)
-            d.update()
-            utime.sleep(2)
-        os.exit(1)
+    MainMenu(apps).run()
diff --git a/pycardium/modules/py/simple_menu.py b/pycardium/modules/py/simple_menu.py
index 42b117cc7998ac94397bee646f1c3fa82538af11..b9c535eeac14fcec5048b7a09377ee615161ec34 100644
--- a/pycardium/modules/py/simple_menu.py
+++ b/pycardium/modules/py/simple_menu.py
@@ -1,9 +1,14 @@
 import buttons
 import color
 import display
+import sys
+import utime
 
+TIMEOUT = 0x100
+""":py:func:`~simple_menu.button_events` timeout marker."""
 
-def button_events():
+
+def button_events(timeout=None):
     """
     Iterate over button presses (event-loop).
 
@@ -26,11 +31,30 @@ def button_events():
                 pass
 
     .. versionadded:: 1.4
+
+    :param float,optional timeout:
+       Timeout after which the generator should yield in any case.  If a
+       timeout is defined, the generator will periodically yield
+       :py:data:`simple_menu.TIMEOUT`.
+
+       .. versionadded:: 1.9
     """
     yield 0
+
     v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
     button_pressed = True if v != 0 else False
+
+    if timeout is not None:
+        timeout = int(timeout * 1000)
+        next_tick = utime.time_ms() + timeout
+
     while True:
+        if timeout is not None:
+            current_time = utime.time_ms()
+            if current_time >= next_tick:
+                next_tick += timeout
+                yield TIMEOUT
+
         v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
 
         if v == 0:
@@ -49,6 +73,10 @@ def button_events():
             yield buttons.TOP_RIGHT
 
 
+class _ExitMenuException(Exception):
+    pass
+
+
 class Menu:
     """
     A simple menu for card10.
@@ -77,6 +105,21 @@ class Menu:
     color_sel = color.COMMYELLOW
     """Color of the selector."""
 
+    scroll_speed = 0.5
+    """
+    Time to wait before scrolling to the right.
+
+    .. versionadded:: 1.9
+    """
+
+    timeout = None
+    """
+    Optional timeout for inactivity.  Once this timeout is reached,
+    :py:meth:`~simple_menu.Menu.on_timeout` will be called.
+
+    .. versionadded:: 1.9
+    """
+
     def on_scroll(self, item, index):
         """
         Hook when the selector scrolls to a new item.
@@ -102,12 +145,30 @@ class Menu:
         """
         pass
 
+    def on_timeout(self):
+        """
+        The inactivity timeout has been triggered.  See
+        :py:attr:`simple_menu.Menu.timeout`.
+
+        .. versionadded:: 1.9
+        """
+        self.exit()
+
+    def exit(self):
+        """
+        Exit the event-loop.  This should be called from inside an ``on_*`` hook.
+
+        .. versionadded:: 1.9
+        """
+        raise _ExitMenuException()
+
     def __init__(self, entries):
         if len(entries) == 0:
             raise ValueError("at least one entry is required")
 
         self.entries = entries
         self.idx = 0
+        self.select_time = utime.time_ms()
         self.disp = display.open()
 
     def entry2name(self, value):
@@ -142,8 +203,21 @@ class Menu:
             but **not** an index into ``entries``.
         :param int offset: Y-offset for this entry.
         """
+        string = self.entry2name(value)
+
+        if offset != 20 or len(string) < 10:
+            string = " " + string + " " * 9
+        else:
+            # Slowly scroll entry to the side
+            time_offset = (utime.time_ms() - self.select_time) // int(
+                self.scroll_speed * 1000
+            )
+            time_offset = time_offset % (len(string) - 7) - 1
+            time_offset = min(len(string) - 10, max(0, time_offset))
+            string = " " + string[time_offset:]
+
         self.disp.print(
-            " " + self.entry2name(value) + " " * 9,
+            string,
             posy=offset,
             fg=self.color_text,
             bg=self.color_1 if index % 2 == 0 else self.color_2,
@@ -171,18 +245,70 @@ class Menu:
 
         self.disp.update()
 
+    def error(self, line1, line2=""):
+        """
+        Display an error message.
+
+        :param str line1: First line of the error message.
+        :param str line2: Second line of the error message.
+
+        .. versionadded:: 1.9
+        """
+        self.disp.clear(color.COMMYELLOW)
+
+        offset = max(0, (160 - len(line1) * 14) // 2)
+        self.disp.print(
+            line1, posx=offset, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
+        )
+
+        offset = max(0, (160 - len(line2) * 14) // 2)
+        self.disp.print(
+            line2, posx=offset, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
+        )
+
+        self.disp.update()
+
     def run(self):
         """Start the event-loop."""
-        for ev in button_events():
-            if ev == buttons.BOTTOM_RIGHT:
-                self.draw_menu(-8)
-                self.idx = (self.idx + 1) % len(self.entries)
-                self.on_scroll(self.entries[self.idx], self.idx)
-            elif ev == buttons.BOTTOM_LEFT:
-                self.draw_menu(8)
-                self.idx = (self.idx + len(self.entries) - 1) % len(self.entries)
-                self.on_scroll(self.entries[self.idx], self.idx)
-            elif ev == buttons.TOP_RIGHT:
-                self.on_select(self.entries[self.idx], self.idx)
-
-            self.draw_menu()
+        try:
+            timeout = self.scroll_speed
+            if self.timeout is not None and self.timeout < self.scroll_speed:
+                timeout = self.timeout
+
+            for ev in button_events(timeout):
+                if ev == buttons.BOTTOM_RIGHT:
+                    self.select_time = utime.time_ms()
+                    self.draw_menu(-8)
+                    self.idx = (self.idx + 1) % len(self.entries)
+                    try:
+                        self.on_scroll(self.entries[self.idx], self.idx)
+                    except Exception as e:
+                        print("Exception during menu.on_scroll():")
+                        sys.print_exception(e)
+                elif ev == buttons.BOTTOM_LEFT:
+                    self.select_time = utime.time_ms()
+                    self.draw_menu(8)
+                    self.idx = (self.idx + len(self.entries) - 1) % len(self.entries)
+                    try:
+                        self.on_scroll(self.entries[self.idx], self.idx)
+                    except Exception as e:
+                        print("Exception during menu.on_scroll():")
+                        sys.print_exception(e)
+                elif ev == buttons.TOP_RIGHT:
+                    try:
+                        self.on_select(self.entries[self.idx], self.idx)
+                        self.select_time = utime.time_ms()
+                    except Exception as e:
+                        print("Menu crashed!")
+                        sys.print_exception(e)
+                        self.error("Menu", "crashed")
+                        utime.sleep(1.0)
+
+                self.draw_menu()
+
+                if self.timeout is not None and (
+                    utime.time_ms() - self.select_time
+                ) > int(self.timeout * 1000):
+                    self.on_timeout()
+        except _ExitMenuException:
+            pass