[jump to content]

nxt screenshot

Updated for NXT-Python 3.0.

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 python3
#
"""Capture the NXT screen content."""
#
# Copyright (C) 2010  Nicolas Schodet
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

import argparse
import logging
import struct

from PIL import Image

import nxt.locator

# Those 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.

    See https://ni.fr.eu.org/lego/nxt_screenshot/ for explanations.
    """
    # Read pixels.
    pixels = []
    for i in range(0, DISPLAY_WIDTH * DISPLAY_HEIGHT // 8, IOM_CHUNK):
        mod_id, contents = b.read_io_map(
            DISPLAY_MODULE_ID, DISPLAY_SCREEN_OFFSET + i, IOM_CHUNK
        )
        pixels += contents
    # Transform to a PIL format.
    pilpixels = []
    bit = 1
    linebase = 0
    for y in range(0, DISPLAY_HEIGHT):
        # Read line by line.
        for x in range(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 = struct.pack("%dB" % DISPLAY_WIDTH * DISPLAY_HEIGHT, *pilpixels)
    pilimage = Image.frombuffer(
        "L", (DISPLAY_WIDTH, DISPLAY_HEIGHT), pilbuffer, "raw", "L", 0, 1
    )
    return pilimage


if __name__ == "__main__":
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument("image", help="image file name to write to")
    nxt.locator.add_arguments(p)
    levels = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
    p.add_argument("--log-level", type=str.upper, choices=levels, help="set log level")
    options = p.parse_args()

    if options.log_level:
        logging.basicConfig(level=options.log_level)

    with nxt.locator.find_with_options(options) as brick:
        image = screenshot(brick)
        image.save(options.image)

(Download source)

Here is the result:

Screenshot of the NXT menu