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*")
("Serial device address: " "/dev/" nil t))
(dev-addr (read-file-name "Baud rate: " 115200)))
(baudrate (read-number "picocom" buffer-name "picocom" nil dev-addr (concat "-b" ) (number-to-string baudrate))
(make-comint-in-buffer 1)
(my/micropython (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"
"r")
(interactive let* ((string (buffer-substring beg end))
("\n" "\r" string)))
(new-string (s-replace "*Micropython*" "")
(comint-send-string "*Micropython*" (concat new-string "\r\n")))
(comint-send-string
)
defun my/micropython-send-buffer ()
("Send buffer to micro python comint"
(interactive)let* ((string (buffer-string))
("\n" "\r" string)))
(new-string (s-replace "*Micropython*" "")
(comint-send-string "*Micropython*" (concat new-string "\r\n")))
(comint-send-string )
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"
nil)
:init-value
defun my/comint-micropython-sender (proc string)
(let ((send-string
(string "\n\r")))
(concat
(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"
"r")
(interactive let* ((string (buffer-substring beg end))
("\n" "\r" string)))
(new-string (s-replace "*Micropython*" "")
(comint-send-string "*Micropython*" (concat new-string "\r\n")))
(comint-send-string
)
defun my/micropython-send-buffer ()
("Send buffer to micro python comint"
(interactive)let* ((string (buffer-string))
("\n" "\r" string)))
(new-string (s-replace "*Micropython*" "")
(comint-send-string "*Micropython*" (concat new-string "\r\n")))
(comint-send-string
)
defun my/start-micropython ()
(
(interactive)let ((current-buffer (current-buffer))
("*Micropython*")
(buffer-name "Serial device address: " "/dev/" nil nil))
(dev-addr (read-file-name "Baud rate: " 115200)))
(baudrate (read-number "picocom" buffer-name "picocom" nil dev-addr (concat "-b" ) (number-to-string baudrate))
(make-comint-in-buffer 1)
(my/micropython
(switch-to-buffer buffer-name)'my/comint-micropython-sender)
(setq-local comint-input-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
.