MicroPython REPL in emacs

MicroPython is a Python implementation for microcontrollers which, apart from coding in Python, allows running an interactive Python REPL over serial prompt in a basic way. This can be achieved by running picocom /dev/ttyUSB0 -b115200 in GNU/Linux as an example. Emacs provides some utilities for normal Python REPL which one can utilize to also interact with MicroPython. Emacs also supports communication over serial ports (make-serial-process). Nevertheless, a bit of adjustments are required to be able to communicate with a REPL over serial when using MicroPython. But once this is set up, one can evan use Tramp to connect to devices attached to a remote machine with power of emacs. This page describes how I got a REPL woking similar to what you get when you run normal Python in emacs with an ESP32 microcontroller board.

Local setup

As the most obvious choice, I first tried to use (make-serial-process :port "/dev/ttyUSB0" :speed 115200 :buffer "**Python**"), to make the REPL an inferior-python-mode buffer. But as discussed in this Reddit thread, the serial-term is based on term while inferior-python-mode is based on comint. Thus other steps are needed to make the REPL work. For that, I created the following function to use comint and picocom command to connect to the device and create a comint-supported REPL.

(defun my/start-micro-python ()
  (interactive)
  (let ((buffer-name "*Micropython*")
        (dev-addr (read-file-name "Serial device address: " "/dev/" nil t))
        (baudrate (read-number "Baud rate: " 115200)))
    (make-comint-in-buffer "picocom" buffer-name "picocom" nil dev-addr (concat "-b" ) (number-to-string baudrate))
    (my/micropython 1)
    (display-buffer buffer-name)))

This was a success, but MicroPython requires a \r to be sent to the REPL line to be processed as opposed to the usual \n that is sent by comint. So I needed the following to make it work.

(defun my/comint-micropython-sender (proc string)
  "Alter the coming sender to include \r at the line end"
  (let ((send-string (concat string "\n\r")))
    (comint-send-string proc send-string))
  (if (string-equal string "")
      (process-send-eof)))

(setq comint-input-sender 'my/comint-micropython-sender)

This is about everything you'd need if you wanted to copy and paste buffer contents into the REPL. However, we can go one step further and create relevant send-buffer and send-region as it is the case for normal Python REPL.

(defun my/micropython-send-region (beg end)
  "Send region to micro python comint"
  (interactive "r")
  (let* ((string (buffer-substring beg end))
         (new-string (s-replace "\n" "\r" string)))
    (comint-send-string "*Micropython*" "")
    (comint-send-string "*Micropython*" (concat new-string "\r\n")))
  )

(defun my/micropython-send-buffer ()
  "Send buffer to micro python comint"
  (interactive)
  (let* ((string (buffer-string))
         (new-string (s-replace "\n" "\r" string)))
    (comint-send-string "*Micropython*" "")
    (comint-send-string "*Micropython*" (concat new-string "\r\n")))
  )

Now I can call the my/micropython-send-region and my/micropython-send-buffer for sending region and whole buffer to the REPL, respectively.

The full module code for me look like the following:

(provide 'my-micropython)

(define-minor-mode my/micropython
  "Minor mode for micropython"
  :init-value nil)

(defun my/comint-micropython-sender (proc string)
  (let ((send-string
         (concat string "\n\r")))
    (comint-send-string proc send-string))
  (if (not (string-equal string ""))
      (process-send-eof)))

(defun my/micropython-send-region (beg end)
  "Send region to micro python comint"
  (interactive "r")
  (let* ((string (buffer-substring beg end))
         (new-string (s-replace "\n" "\r" string)))
    (comint-send-string "*Micropython*" "")
    (comint-send-string "*Micropython*" (concat new-string "\r\n")))
  )

(defun my/micropython-send-buffer ()
  "Send buffer to micro python comint"
  (interactive)
  (let* ((string (buffer-string))
         (new-string (s-replace "\n" "\r" string)))
    (comint-send-string "*Micropython*" "")
    (comint-send-string "*Micropython*" (concat new-string "\r\n")))
  )

(defun my/start-micropython ()
  (interactive)
  (let ((current-buffer (current-buffer))
        (buffer-name "*Micropython*")
        (dev-addr (read-file-name "Serial device address: " "/dev/" nil nil))
        (baudrate (read-number "Baud rate: " 115200)))
    (make-comint-in-buffer "picocom" buffer-name "picocom" nil dev-addr (concat "-b" ) (number-to-string baudrate))
    (my/micropython 1)
    (switch-to-buffer buffer-name)
    (setq-local comint-input-sender 'my/comint-micropython-sender)
    (switch-to-buffer current-buffer)
    (display-buffer buffer-name)))

The full module can be found in my personal emacs configuration repo.

Keybinds

We need to override C-c C-c and C-c C-r which are bound for python-mode and python-ts-mode. I use geneal for my keybinds:

(general-define-key
 :states '(normal visual)
 :keymaps '(override my/micropython-map)
 "C-c C-c" #'my/micropython-send-buffer
 "C-c C-r" #'my/micropython-send-buffer)

Control devices over Tramp

Do you have your microcontroller connected to a computer you have SSH access to? You can easily connect your comint REPL to the microcontroller and work remotely. Just make sure you have picocom installed on the remote machine and when the my/start-micropython asks for the device use local address for it, e.g. /dev/ttyUSB0 and not /ssh:user@remote:/dev/ttyUSB0.

Troubleshooting

Permission error

If you get permission error when trying to access device REPL, run sudo chmod 666 /dev/ttyUSB0 # update your device address.