Keystroke injection in REPL

All ESP32 boards running MicroPython.
Target audience: MicroPython users with an ESP32 board.
Post Reply
Trekintosh
Posts: 5
Joined: Fri Dec 06, 2019 7:10 pm

Keystroke injection in REPL

Post by Trekintosh » Mon Dec 16, 2019 4:08 am

*NOTE: THIS IS NOT ABOUT MALICIOUS KEYSTROKE INJECTION*

Hello,

I apologize if this is the wrong section but it seemed appropriate. I'm making a self-contained ESP32 powered Micropython computer. I'm currently writing a driver for an xbox 360 chatpad, and I have it interpreting keystrokes in the background, now it's time to send them to REPL. Problem: I have no idea how to do this. My initial thought was to try and do some kind of internal serial loopback (or barring that, a physical connection from a third UART interface[the chatpad lives on UART1]), however that would disconnect me from the host computer, right? I'd really rather not do that.

Is there any way to inject keystrokes into the REPL from the background(using _Thread probably) with python code?

I have a sneaking suspicion I'm going to have to change this from a .py file to a C module, or even dive into the uPython source code.

Thanks for any help in advance!

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: Keystroke injection in REPL

Post by jimmo » Mon Dec 16, 2019 4:48 am

hi,

The simple answer is you want to use os.dupterm() to make a second stream attached to the REPL.

Here's a rough sketch of how this might work (based on micropython/examples/bluetooth/ble_uart_repl.py which has to do a similar thing).

So it's a stream that doesn't support writing (i.e. receiving chars from the REPL), but allows you to inject data. You can call inject from your keyboard handling code. If you want to adapt this to make a display later, then implement the write method.

Code: Select all

import io
import os
import machine

_MP_STREAM_POLL = const(3)
_MP_STREAM_POLL_RD = const(0x0001)

_timer = machine.Timer(-1)

# Simple buffering stream to support the dupterm requirements.
class InjectStream(io.IOBase):
    def __init__(self):
        self._data = bytearray()

    def inject(self, data):
        self._data += data

        # Needed for ESP32.
        if hasattr(os, 'dupterm_notify'):
            os.dupterm_notify(None)

    def readinto(self, buf):
        if not self._data:
            return None
        b = min(len(buf), len(self._data))
        buf[:b] = self._data[:b]
        self._data = self._data[b:]
        return b

    def read(self, sz=None):
        d = self._data
        self._data[:] = b''
        return d

    def ioctl(self, op, arg):
        if op == _MP_STREAM_POLL:
            if self._data:
                return _MP_STREAM_POLL_RD
        return 0

    def write(self, buf):
        pass

stream = InjectStream()
os.dupterm(stream)

# call this from your key handler (e.g. pin interrupt?)
# stream.inject('input to type')

Trekintosh
Posts: 5
Joined: Fri Dec 06, 2019 7:10 pm

Re: Keystroke injection in REPL

Post by Trekintosh » Mon Dec 16, 2019 7:03 am

jimmo wrote:
Mon Dec 16, 2019 4:48 am
hi,

The simple answer is you want to use os.dupterm() to make a second stream attached to the REPL.

Here's a rough sketch of how this might work (based on micropython/examples/bluetooth/ble_uart_repl.py which has to do a similar thing).

So it's a stream that doesn't support writing (i.e. receiving chars from the REPL), but allows you to inject data. You can call inject from your keyboard handling code. If you want to adapt this to make a display later, then implement the write method.

Code: Select all

import io
import os
import machine

_MP_STREAM_POLL = const(3)
_MP_STREAM_POLL_RD = const(0x0001)

_timer = machine.Timer(-1)

# Simple buffering stream to support the dupterm requirements.
class InjectStream(io.IOBase):
    def __init__(self):
        self._data = bytearray()

    def inject(self, data):
        self._data += data

        # Needed for ESP32.
        if hasattr(os, 'dupterm_notify'):
            os.dupterm_notify(None)

    def readinto(self, buf):
        if not self._data:
            return None
        b = min(len(buf), len(self._data))
        buf[:b] = self._data[:b]
        self._data = self._data[b:]
        return b

    def read(self, sz=None):
        d = self._data
        self._data[:] = b''
        return d

    def ioctl(self, op, arg):
        if op == _MP_STREAM_POLL:
            if self._data:
                return _MP_STREAM_POLL_RD
        return 0

    def write(self, buf):
        pass

stream = InjectStream()
os.dupterm(stream)

# call this from your key handler (e.g. pin interrupt?)
# stream.inject('input to type')
Great! Thanks! This is a huge step forward for me. Imported your code and just testing it worked just fine!
Image

Now I can inject simple characters, which is great, but what about things like enter, arrow keys, and ctrl+c for example? I read the documentation for dupeterm, but it was pretty unclear to me, as was the actual code snippet you sent with regards to how it actually sends data.

I apologize for my newbishness, I only started using Python about a week ago. Still though, got my keyboard code running in under 10k of RAM! Not too shabby if I do say so myself.

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: Keystroke injection in REPL

Post by jimmo » Mon Dec 16, 2019 10:28 am

It works the same way as any other terminal. So Ctrl-C is a byte with value 3. (i.e. the same as it's ascii value for C (i.e. 67) but without the "64" place bit set). In other words Ctrl-C is the "end of text" value in an ascii table. Ctrl-D is "end transmission".

Newline is CRLF (13,10).

Have a look at tools/pyboard.py in the main MicroPython GitHub repo for some examples of sending commands to the REPL.

Arrow keys are more complicated, as they involve vt100 escape sequences. Either start with the code that handles them -- https://github.com/micropython/micropyt ... readline.c -- or look up the commands e.g. http://ascii-table.com/ansi-escape-sequences-vt-100.php

Trekintosh
Posts: 5
Joined: Fri Dec 06, 2019 7:10 pm

Re: Keystroke injection in REPL

Post by Trekintosh » Mon Dec 16, 2019 5:52 pm

jimmo wrote:
Mon Dec 16, 2019 10:28 am
It works the same way as any other terminal. So Ctrl-C is a byte with value 3. (i.e. the same as it's ascii value for C (i.e. 67) but without the "64" place bit set). In other words Ctrl-C is the "end of text" value in an ascii table. Ctrl-D is "end transmission".

Newline is CRLF (13,10).

Have a look at tools/pyboard.py in the main MicroPython GitHub repo for some examples of sending commands to the REPL.

Arrow keys are more complicated, as they involve vt100 escape sequences. Either start with the code that handles them -- https://github.com/micropython/micropyt ... readline.c -- or look up the commands e.g. http://ascii-table.com/ansi-escape-sequences-vt-100.php
Thank you, I believe I understand. But when I try to put, for the example of sending ctrl+c, stream.inject(0x03) or stream.inject(b'0x03') it just types those text characters into the REPL, it doesn't actually send the terminal command. Is there something I'm missing here or do I have to modify the stream code instead?

Pyboard.py lists b'\x03' as the ctrl+c command, and I haven't tried that yet (at work) but is that the right way to go?

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: Keystroke injection in REPL

Post by jimmo » Mon Dec 16, 2019 9:16 pm

Yes, you need b'\x03' (this is how you escape bytes in a string in Python)

stream.inject(3) is sending a number to a function that takes a string.

inject('0x03') is sending the characters 0 x 0 3

Trekintosh
Posts: 5
Joined: Fri Dec 06, 2019 7:10 pm

Re: Keystroke injection in REPL

Post by Trekintosh » Sun Dec 22, 2019 6:34 pm

Thank you, thank you! I now have the chatpad injecting into the REPL.

I'm trying to use keystrokes besides letters now, and am getting some odd behavior. I am using the arrow key commands and backspace command built into upycraft itself, but while they work normally if I were to type them on my computer keyboard, if I use my injected ones, I get this garbage text:

Image

Those are a series of arrow keys and backspaces. If I were to type something legible and hit enter, then it would come out correctly on the output, but the garbage text makes it hard to work with. Observe:

Image
that block of garbage text is the backspace, and as you can see it did delete the 'o', but the garbage text is there. I'm just using b'\x08' for the backspace comand. The arrow keys are a bit more complicated, but behave the same way. They function as they should but leave nasty garbage text behind in REPL.

EDIT: Scratch that! It's just an oddity in how upycraft handles its terminal, if I use the device in PUTTY it works perfectly fine!

Awesome!!!! If it's okay with you, jimmo, I'd like to fold your injection code into my driver python file. I will be giving you credit and linking directly to this thread in the comments.

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: Keystroke injection in REPL

Post by jimmo » Mon Dec 23, 2019 12:42 am

Trekintosh wrote:
Sun Dec 22, 2019 6:34 pm
Awesome!!!! If it's okay with you, jimmo, I'd like to fold your injection code into my driver python file. I will be giving you credit and linking directly to this thread in the comments.
Not necessary at all :)

Great to hear it's working!

Post Reply