[jump to content]

nxt screenshot

There is no screenshot utility for Linux, and I like automated tools, so I decided to make my own.

As many things in NXT, the current screen content is accessible using a IO map. So, one more time, let's have a look to NXT firmware code. This is the DISPLAY module IO map:

typedef   struct
{
  void    (*pFunc)(UBYTE,UBYTE,UBYTE,UBYTE,UBYTE,UBYTE);    // Simple draw entry
  ULONG   EraseMask;                                        // Section erase mask   (executed first)
  ULONG   UpdateMask;                                       // Section update mask  (executed next)
  FONT    *pFont;                                           // Pointer to font file
  UBYTE   *pTextLines[TEXTLINES];                           // Pointer to text strings
  UBYTE   *pStatusText;                                     // Pointer to status text string
  ICON    *pStatusIcons;                                    // Pointer to status icon collection file
  BMPMAP  *pScreens[SCREENS];                               // Pointer to screen bitmap file
  BMPMAP  *pBitmaps[BITMAPS];                               // Pointer to free bitmap files
  UBYTE   *pMenuText;                                       // Pointer to menu icon text
  UBYTE   *pMenuIcons[MENUICONS];                           // Pointer to menu icon images
  ICON    *pStepIcons;                                      // Pointer to step icon collection file
  UBYTE   *Display;                                         // Display content copied to physical display
  UBYTE   StatusIcons[STATUSICONS];                         // Index in status icon collection file
  UBYTE   StepIcons[STEPICONS];                             // Index in step icon collection file
  UBYTE   Flags;                                            // Update flags enumerated above
  UBYTE   TextLinesCenterFlags;                             // Mask to center TextLines
  UBYTE   Normal[DISPLAY_HEIGHT / 8][DISPLAY_WIDTH];        // Raw display memory for normal screen
  UBYTE   Popup[DISPLAY_HEIGHT / 8][DISPLAY_WIDTH];         // Raw display memory for popup screen
}
IOMAPDISPLAY;

Interesting fields are:

  • Normal: display memory, containing the image displayed on screen.
  • Flags and Popup: I did not used them, but you could test for the DISPLAY_POPUP bit in Flags to know whether the image should be read from Popup rather than Normal.

As you can see, DISPLAY_HEIGHT is divided by 8. This is because the NXT stores 8 vertical pixels in one byte. In each byte, the LSB (least significant bit) corresponds to the top pixel, and the MSB (most significant bit) corresponds to the bottom pixel.

Here is a illustration of this format:

pixel format in the NXT

(Source SVG file)

I have everything I need to implement the screenshot tool:

#!/usr/bin/env python
#
# screenshot.py - Capture the NXT screen content.
#
# Copyright (C) 2010 Nicolas Schodet
#
# <download file for full MIT license>
#
import nxt.locator
import nxt.brick
from struct import unpack, pack
import Image
from optparse import OptionParser

# Thoses are extracted from firmware sources.
DISPLAY_MODULE_ID = 0x000a0001
DISPLAY_SCREEN_OFFSET = 119
DISPLAY_WIDTH = 100
DISPLAY_HEIGHT = 64

# Read no more than 32 bytes per request.
IOM_CHUNK = 32

def screenshot (b):
    '''Take a screenshot, return a PIL image.'''
    # Read pixels.
    pixels = []
    for i in xrange (0, DISPLAY_WIDTH * DISPLAY_HEIGHT / 8, IOM_CHUNK):
        mod_id, n_bytes, contents = b.read_io_map (DISPLAY_MODULE_ID,
                DISPLAY_SCREEN_OFFSET + i, IOM_CHUNK)
        pixels += unpack ('32B', contents)
    # Transform to a PIL format.
    pilpixels = []
    bit = 1
    linebase = 0
    for y in xrange (0, DISPLAY_HEIGHT):
        # Read line by line.
        for x in xrange (0, DISPLAY_WIDTH):
            if pixels[linebase + x] & bit:
                pilpixels.append (0)
            else:
                pilpixels.append (255)
        bit <<= 1
        # When 8 lines have been read, go on with the next byte line.
        if bit == (1 << 8):
            bit = 1
            linebase += DISPLAY_WIDTH
    # Return a PIL image.
    pilbuffer = pack ('%dB' % DISPLAY_WIDTH * DISPLAY_HEIGHT, *pilpixels)
    pilimage = Image.frombuffer ('L', (DISPLAY_WIDTH, DISPLAY_HEIGHT),
            pilbuffer, 'raw', 'L', 0, 1)
    return pilimage

# Get filename from command line.
parser = OptionParser (usage = "usage: %prog [options] image_file")
(options, args) = parser.parse_args ()
if len (args) != 1:
    parser.error ("no image filename provided")
filename = args[0]

# Find the brick and take the screenshot.
sock = nxt.locator.find_one_brick ()
if sock:
    image = screenshot (sock.connect ())
    image.save (filename)
    sock.close ()

(Download source)

Here is the result:

Screenshot of the NXT menu