diff --git a/esp8266/esp_mphal.c b/esp8266/esp_mphal.c
index c8a88c725ee65b91057e8e5466e49eec04e9f817..7068d8e6093b8d7f1c998f93e1512bc087eec834 100644
--- a/esp8266/esp_mphal.c
+++ b/esp8266/esp_mphal.c
@@ -34,6 +34,7 @@
 #include "py/obj.h"
 #include "py/mpstate.h"
 #include "extmod/misc.h"
+#include "lib/utils/pyexec.h"
 
 extern void ets_wdt_disable(void);
 extern void wdt_feed(void);
@@ -157,3 +158,59 @@ void __assert_func(const char *file, int line, const char *func, const char *exp
 void mp_hal_signal_input(void) {
     system_os_post(UART_TASK_ID, 0, 0);
 }
+
+static int call_dupterm_read(void) {
+    if (MP_STATE_PORT(term_obj) == NULL) {
+        return -1;
+    }
+
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t read_m[3];
+        mp_load_method(MP_STATE_PORT(term_obj), MP_QSTR_read, read_m);
+        read_m[2] = MP_OBJ_NEW_SMALL_INT(1);
+        mp_obj_t res = mp_call_method_n_kw(1, 0, read_m);
+        if (res == mp_const_none) {
+            return -2;
+        }
+        mp_buffer_info_t bufinfo;
+        mp_get_buffer_raise(res, &bufinfo, MP_BUFFER_READ);
+        if (bufinfo.len == 0) {
+            mp_printf(&mp_plat_print, "dupterm: EOF received, deactivating\n");
+            MP_STATE_PORT(term_obj) = NULL;
+            return -1;
+        }
+        nlr_pop();
+        return *(byte*)bufinfo.buf;
+    } else {
+        // Temporarily disable dupterm to avoid infinite recursion
+        mp_obj_t save_term = MP_STATE_PORT(term_obj);
+        MP_STATE_PORT(term_obj) = NULL;
+        mp_printf(&mp_plat_print, "dupterm: ");
+        mp_obj_print_exception(&mp_plat_print, nlr.ret_val);
+        MP_STATE_PORT(term_obj) = save_term;
+    }
+
+    return -1;
+}
+
+STATIC void dupterm_task_handler(os_event_t *evt) {
+    while (1) {
+        int c = call_dupterm_read();
+        if (c < 0) {
+            break;
+        }
+        ringbuf_put(&input_buf, c);
+    }
+    mp_hal_signal_input();
+}
+
+STATIC os_event_t dupterm_evt_queue[4];
+
+void dupterm_task_init() {
+    system_os_task(dupterm_task_handler, DUPTERM_TASK_ID, dupterm_evt_queue, MP_ARRAY_SIZE(dupterm_evt_queue));
+}
+
+void mp_hal_signal_dupterm_input(void) {
+    system_os_post(DUPTERM_TASK_ID, 0, 0);
+}
diff --git a/esp8266/esp_mphal.h b/esp8266/esp_mphal.h
index bedf80b19fa1c3083631cdc9b6f6c784c89079e6..6713e425514ed42bd6bb4deca2386bf7d9a6e79d 100644
--- a/esp8266/esp_mphal.h
+++ b/esp8266/esp_mphal.h
@@ -36,6 +36,8 @@ extern const struct _mp_print_t mp_debug_print;
 extern ringbuf_t input_buf;
 // Call this after putting data to input_buf
 void mp_hal_signal_input(void);
+// Call this when data is available in dupterm object
+void mp_hal_signal_dupterm_input(void);
 
 void mp_hal_init(void);
 void mp_hal_rtc_init(void);
@@ -47,7 +49,9 @@ void mp_hal_set_interrupt_char(int c);
 uint32_t mp_hal_get_cpu_freq(void);
 
 #define UART_TASK_ID 0
+#define DUPTERM_TASK_ID 1
 void uart_task_init();
+void dupterm_task_init();
 
 void ets_event_poll(void);
 #define ETS_POLL_WHILE(cond) { while (cond) ets_event_poll(); }
diff --git a/esp8266/main.c b/esp8266/main.c
index 4fc7757c8131cc2ef19cc87564ea04e2c3ec267d..bf68ebc96d961f878412d84755501ee5e5ebe20e 100644
--- a/esp8266/main.c
+++ b/esp8266/main.c
@@ -69,6 +69,7 @@ void init_done(void) {
     mp_reset();
     mp_hal_stdout_tx_str("\r\n");
     pyexec_event_repl_init();
+    dupterm_task_init();
 }
 
 void user_init(void) {