ANSI escape codes and DSR timing issues

ANSI Escape Codes

Introduction

In the days of the mainframe, a "terminal" was a physical box with a screen and a keyboard, and all the computation took place at a distance. It was occasionally (read: often) useful for the mainframe to be able to manipulate the terminal in ways other than simply sending streams of alphanumeric characters to be displayed. A program could display a primitive GUI with editable cells, clickable buttons and the like, simply by varying the display properties of certain parts of the screen and editing them in response to the user's typing.

This manipulation was done with ANSI escape codes. The mainframe would send a control character sequence, followed by a string of characters which composed an instruction, as opposed to data to be displayed to the user. Nowadays, we mostly use terminal emulators like PuTTY, xterm and TeraTerm, but they still mostly support the ANSI escape sequences. As a result, it's possible for an mbed to send a "cursor up" command over the USB serial connection, and the cursor on the terminal emulator will move up one row (for example).

Implementation

I'm currently writing a library that contains a set of helper functions for generating ANSI escape sequences. There's a function that wraps each major sequence, and a set of higher level functions to clear the screen, position the cursor, change the display mode and so on. The whole thing extends Serial, so it can just be substituted into an existing project with minimal difficulty.

(I'm aware of Simon Ford's VT100-Terminal library. I've taken a somewhat different approach, but it's the same basic concept.)

DSR

Oh, DSR. The majority of the ANSI sequences are almost trivially easy to implement, being a case of "emit these characters, job done". DSR and SGR are more complicated, the first because it involves a response and the second because it's a very overloaded function that tries to do a lot of things. DSR first.

The DSR sequence is a request that the terminal report back where the cursor is: the correct response is another escape sequence that contains the appropriate x- and y-coordinates. I implemented the DSR request like all the others, and used a simple scanf() to pull the returned coordinates out. However, calling scanf added about 4kB to a 20kB demo program, and I believe that it's quite RAM-heavy too. Since I don't need float parsing and all of scanf's bells and whistles, I rolled my own stateful parser and dropped it into the DSR function in place of the scanf call. The memory image dropped back to about 20kB, and ... no data came back. My stateful parser was fine, and passed all the tests I threw at it with ease. Something subtle was going on.

Eventually, it became obvious. The LPC1768 is sufficiently fast, and the USB serial emulation sufficiently slow, that I was sending a four-byte escape sequence and checking for a reply before the massively more powerful PC at the other end of the connection was able to receive the message and send a response. Apparently, the scanf() implementation on mbed was just about slow enough that the response came back while it was still initialising. I measured the round-trip time at about 15ms, and someone more knowledgeable than me about serial emulation tells me that fully ten of those milliseconds are spent waiting for a USB receive buffer on the PC to flush.

So, for now, my DSR function has a wait(0.2) statement in it, just before the parser. Hardly ideal. I plan to write an RTOS-compatible variation that uses Thread.wait() and a mutex, but it seems I'm largely stuck with it. Bit of a pity, but I don't think there's another way round it.

SGR

SGR is a bit of a horror, because it's one escape sequence with an awful lot of potential functionality. This one escape sequence can make text bold, italic, underlined, inverted-video, crossed-out, blinking AND set the text and background colours, all in one call. This is probably a bit excessive, but nonetheless, it's in the standard and so the ANSITerm library supports it.

I eventually implemented this by creating an SGR function that takes four arguments:

  • reset (boolean: true to return the terminal to default settings, normally false)
  • text_style (char flagset: an OR of SGR_BOLD, SGR_ITALIC, SGR_UNDERLINE, SGR_BLINK_SLOW, SGR_BLINK_RAPID, SGR_IMAGE_NEGATIVE and SGR_CROSSED_OUT. Flags that are set cause the named feature to be turned on. If a flag is clear, then the named feature will be turned off. This means that an application can hold a single (or a small set of) named style and pass it in to all SGR calls to maintain a consistent text style.)
  • text_colour (char, one of the eight colours specified in the ANSITerm header)
  • background_colour (char, one of the eight colours specified in the ANSITerm header)

The current implementation is fairly solid, but has only been tested under PuTTY. Bold, Faint and the Blink sequences are a bit unpredictable, depending on terminal settings, but the library is emitting the correct escapes according to the standard.

Library

(I'll add a download link to the library once I think it's ready: SGR's done, so just some helper functions and encapsulation now.)


Please log in to post comments.