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:
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)
Here is the result: