diff --git a/src/app/mhz19/Kconfig b/src/app/mhz19/Kconfig
new file mode 100644
index 0000000000000000000000000000000000000000..473cea94d80b90401300b6f8964c9695634780d1
--- /dev/null
+++ b/src/app/mhz19/Kconfig
@@ -0,0 +1,6 @@
+# Copyright 2020 Daniel Friesel
+#
+# SPDX-License-Identifier: CC0-1.0
+
+prompt "MH-Z19C reader"
+depends on arch_msp430fr5994lp && !meta_driver_stdin && loop && !wakeup
diff --git a/src/app/mhz19/Makefile.inc b/src/app/mhz19/Makefile.inc
new file mode 100644
index 0000000000000000000000000000000000000000..0eea52a8aaf27f7c93e1fc002662af73861caa61
--- /dev/null
+++ b/src/app/mhz19/Makefile.inc
@@ -0,0 +1,9 @@
+# vim:ft=make
+#
+# Copyright 2020 Daniel Friesel
+#
+# SPDX-License-Identifier: CC0-1.0
+
+ifdef app
+	loop = 1
+endif
diff --git a/src/app/mhz19/main.cc b/src/app/mhz19/main.cc
new file mode 100644
index 0000000000000000000000000000000000000000..23c79848b8bc3b07d4bf15557947b09d4846a39f
--- /dev/null
+++ b/src/app/mhz19/main.cc
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2021 Daniel Friesel
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+#include "arch.h"
+#include "driver/gpio.h"
+#include "driver/stdout.h"
+
+class SerialOutput : public OutputStream {
+	private:
+		SerialOutput(const SerialOutput &copy);
+
+	public:
+		SerialOutput () {}
+		void setup();
+
+		virtual void put(char c) override;
+};
+
+/*
+ * Baud rate calculation according to datasheet:
+ * N := f_{BRCLK} / Baudrate = F_CPU / 115200 in our case
+ * if N <= 16: OS16 = 0, UCBR0 = int(N)
+ * if N > 16: OS16 = 1, UCBR0 = int(N/16), UCBRF0 = int(((n/16) - int(n/16)) * 16) = int(N)%16
+ * Set UCBRS0 according to table 21-4
+ */
+
+void SerialOutput::setup()
+{
+	UCA1CTLW0 |= UCSWRST;
+#if F_CPU == 16000000UL
+	// 16M / 9600 == 1666.6667 -> UCOS16 = 1, UCBR0 = 104, UCBRF0 = 2, UCBRS0 = 0xd6 ("0.6667")
+	UCA1CTLW0 = UCSWRST | UCSSEL__SMCLK;
+	UCA1MCTLW = UCOS16 | (2<<4) | 0xd600;
+	UCA1BR0 = 104;
+#elif F_CPU == 8000000UL
+	// 8M / 9600 == 833.3333 -> UCOS16 = 1, UCBR0 = 52, UCBRF0 = 1, UCBRS0 = 0x49 ("0.3335")
+	UCA1CTLW0 = UCSWRST | UCSSEL__SMCLK;
+	UCA1MCTLW = UCOS16 | (1<<4) | 0x4900;
+	UCA1BR0 = 52;
+#else
+#error Unsupported F_CPU
+#endif
+
+	UCA1IRCTL = 0;
+	UCA1ABCTL = 0;
+
+	P2REN &= ~(BIT5 | BIT6);
+	P2SEL0 &= ~(BIT5 | BIT6);
+	P2SEL1 |= BIT5 | BIT6;
+
+	UCA1CTLW0 &= ~UCSWRST;
+
+	//UCA1IE |= UCRXIE;
+}
+
+void SerialOutput::put(char c)
+{
+	while (!(UCA1IFG & UCTXIFG));
+	UCA1TXBUF = c;
+
+	if (c == '\n') {
+		put('\r');
+	}
+}
+
+SerialOutput sout;
+
+class SerialInput {
+	private:
+		SerialInput(const SerialInput &copy);
+		char buffer[64];
+		volatile unsigned char write_pos, read_pos;
+
+	public:
+		SerialInput() : write_pos(0), read_pos(0) {}
+		void setup();
+		bool hasKey();
+		char getKey();
+
+		inline void addKey(char key) {
+			buffer[write_pos++] = key;
+			write_pos %= 64;
+		}
+};
+
+void SerialInput::setup()
+{
+	UCA1IE |= UCRXIE;
+}
+
+bool SerialInput::hasKey()
+{
+	if (write_pos != read_pos) {
+		return true;
+	}
+	return false;
+}
+
+char SerialInput::getKey()
+{
+	char ret = buffer[read_pos++];
+	read_pos %= 64;
+	return ret;
+}
+
+SerialInput sin;
+
+__attribute__((interrupt(USCI_A1_VECTOR))) __attribute__((wakeup)) void handle_stdin()
+{
+	if (UCA1IFG & UCRXIFG) {
+		sin.addKey(UCA1RXBUF);
+	}
+}
+
+const unsigned char mhz19c_read[] = {0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
+
+void loop(void)
+{
+	static unsigned int interval = 0;
+	static char buf[9];
+	unsigned char i;
+	unsigned int co2_ppm;
+	signed int temperature;
+	unsigned int unknown_status;
+	unsigned int unknown_cal;
+	if (interval++ == 2) {
+		gpio.led_on(0);
+		for (i = 0; i < sizeof(mhz19c_read); i++) {
+			sout.put(mhz19c_read[i]);
+		}
+		arch.sleep_ms(100);
+		buf[1] = 0;
+		for (i = 0; i < 9; i++) {
+			if (sin.hasKey()) {
+				buf[i] = sin.getKey();
+			} else {
+				break;
+			}
+		}
+		if (i == 9) {
+			if (buf[0] == 0xff && buf[1] == 0x86) {
+				// see https://revspace.nl/MHZ19#Command_0x86_.28read_concentration.29
+				co2_ppm = (buf[2] << 8) + buf[3];
+				temperature = buf[4] - 40;
+				unknown_status = buf[5];
+				unknown_cal = (buf[6] << 8) + buf[7];
+				kout << "CO₂: " << co2_ppm << " ppm @ " << temperature << " °c     ";
+				kout << unknown_status << " / " << unknown_cal << endl;
+			} else {
+				kout << "! communication error, expected 0xff 0x86, got" << hex << (int)buf[0] << ' ' << (int)buf[1] << dec << endl;
+			}
+		} else {
+			kout << "! timeout" << endl;
+		}
+		gpio.led_off(0);
+		interval = 0;
+	}
+}
+
+int main(void)
+{
+	arch.setup();
+	gpio.setup();
+	kout.setup();
+	sout.setup();
+	sin.setup();
+
+	arch.idle_loop();
+
+	return 0;
+}