Skip to content

jnDis Platform Sample

Philipp M. Scholl edited this page Jan 17, 2014 · 2 revisions

This tutorial is about controlling a µOLED display manufactured by 4d Systems with the Jennic JN5139 microcontroller. The Jennic module provides the means to interface with the display through a wireless tcp/ipv6 6LoWPAN connection. More specifically the Jennic and display module are connected through a serial port. A tcp server on the Jennic module then forwards all data on the tcp connection to the serial port (i.e. the connected display) and vice-versa - providing a remote serial port server.

The µOLED display itself is controlled by binary commands posted to its serial port. These commands are documented in Goldex Command Interface Specifications and are directly sent to the display via the tcp connection. These display are pretty nifty as besides the ability to be controlled over a simple protocol, they also provide the ability to load image and video data previously stored on a µSD-card. Power consumption for the display only is between 13.5mA to 115mA, which make it suitable for battery-operated designs. For more details about the µOLED displays, take a look at its datasheet.

The idea behind these miniature wireless displays is to augment dynamic workspaces with situated glyphs, in order to

...present real time in-situ information to support multiple interleaved activities involving multiple individuals and different types of equipment in complex workplaces...We aim to support (both cognitive and physically) demanding real-world activities, such as nursing tasks in a hospital, by mapping visual representations of activity-specific information to the physical environment using situated glyphs.

and is similar to the idea of the MemoClip, developed at TecO some time ago:

...the MemoClip (a small clip), that reminds a user of things he should do depending on where he is. A user can associate information to be remembered with a description of a location, download it onto the MemoClip, and then gets notified accordingly when entering the selected location.

We will now look into the custom designed pcb and hardware for these minituare wireless displays, the firmware running on the Jennic nodes and the pc software used to control the displays. The general idea is to use the µSD-card controller on the display modules as a storage device for the glyphs, to enable uploading of pictures onto the µSD-card from a pc through the wireless link and to control the display of these glyphs from the pc.

Hardware

Jennic and display module run on different operating voltages. The display modules needs to be provided with 5v, while the jennic module needs to be powered with just 3.3v. Luckily the display module already contains a step-down converter, so we just need to add one 5v step-up converter and power the jennic via the display module. Here we used the following components:

The hardware has been designed with eagle and is available freely, and has been added to the Jennic Contiki port as a new platform called: jndis, where the eagle design files can be found. The hardware version published there is incomplete and is in need of some redesign, get in touch if you wannt to use this design.

Firmware

The goal of the firmware design is, despite the contiki operating system, to keep the whole system as simple as possible. For this reason it was decided to only provide a serial tunnel to the display module. The Jennic provides a tcp server which tunnels data from the tcp connection to the serial port and vice-versa. The sourcecode for this is available on github.

Network connections with the pc are created by using our ethbridge, also available on github, firmware, hardware. The ethbridge is a usb-stick, that provides any linux machine with a usb-cdc compatible ieee802.15.4 wireless stick. This stick emulates an ethernet card, so the standard tcp/ip suite of the linux host machine can be used to interface with the displays (they will be available as ipv6-adressable nodes).

So in the end, the displays are controlled by opening a tcp-stream to an ipv6-node and posting the binary commands documented in the µOLED user guide on the tcp channel. This as well handles writing to the µSD-card.

Software

As said before, the displays are controlled by connecting to ipv6-nodes on the network connection of the ethbridge-stick. What we want to achieve is to:

  1. scan the network for available display nodes.
  2. provide some means to upload images to the µSD-card.
  3. provide some means to show a saved image from the µSD-card.
  4. upload pictures directly for display.
  5. turn the display on/off.

To achieve this we used python to communicate the with the display nodes:

 # -*- coding: utf8 -*-
 #
 # Helper classes to interact with the display via TCP/ipv6

 from socket import *
 from subprocess import check_output
 from time import sleep, time
 from struct import pack
 from sys import argv,stderr,exit
 import re

This imports all the necessary python modules to work with binary commands, sockets and subprocesses. Next we define a function that ping-scans the network on a specific network interface on the local machine. As the ethbridge creates a local network interface on the host machine we can just to do a ping to the "link-local" ipv6-broadcast address to get a list of available nodes on that link (i.e. network interface). Only the displays reachable by our ethbridge will answer this ping, which is the way we get their ipv6-address. This is achieved by executing ping6 ff02::1%eth1, where eth1 is the interface name of the ethbridge. The following code snippet does that:

def networkQuery(interface="eth1"):
    """ three seconds ping scan of the network and checking the abilities
        of each ipv6 node by trying to connect to it.

        Only the local network on the specified interface will be scanned.
    """
    print ("ping scanning for nodes on %s"%interface)
    output = check_output(["ping6", "ff02::1%%%s"%interface, "-c 4"])
    nodes  = list(set([node.split(' ')[3][:-1] for node in output.split("\n") if node.startswith("64 bytes")]))

    localhost = check_output(("ifconfig %s"%interface).split())
    localhost = re.findall(".*inet6 addr: (.*)/", localhost.split("\n")[1])[0]
    nodes.remove(localhost)

    # add the interface to the address to form local addresses
    nodes  = ["%s%%%s"%(n,interface) for n in nodes]

    displays = []

    for node in nodes:
        d = None
        try: d = Display(node); displays.append(d)
        except error: pass

    return displays

This functions returns a list of Display Objects, which is the surrogate for communicating with the wireless display. It connects to the display via TCP/IP, sets up the serial port for communicating with the µOLED display, queries its resolution and capabilities and is then ready to execute specific commands, like clearing the display, uploading to the sdcard etc. Note that this class only implements a subset of the commands supported by the µOLED:

class Display():
    def __init__(self, ip=None):
        print ("connecting to %s"%ip)
        af,typ,proto,name,sa = getaddrinfo(ip,4404,AF_UNSPEC,SOCK_STREAM)[0]
        self.s = socket(af,typ,proto)
        self.s.connect(sa)
        self.ip = ip
        self.outstanding = 0

        # setup the uart connection
        # wait until Greeting has been received, we don't rely care what's in there
        while self.s.recv(1024) == 0:
            pass

        # Select remote uart mode, we use a really slow baudrate to have a stable uart
        # connection.
        self.s.send("UART1 115200 8N1\r\n")
        mode = self.s.recv(len("UART1 R115200 8N1\n"), MSG_WAITALL)

        # prepare the display with the AUTOBAUD command
        sleep(.2)
        self._talk("U", 1)

        # identify the display
        v = self._talk("V\x00", 5)

        if v[0] == 0x00:
            self.device_type = "µOLED"
        elif v[0] == 0x01:
            self.device_type = "µLCD"
        elif v[0] == 0x02:
            self.device_type = "µVGA"
        else:
            stderr.write("ERROR: unknown device type")
            exit(-1)

        def tores(x):
            if x == 0x22: res = 220
            elif x == 0x28: res = 128
            elif x == 0x32: res = 320
            elif x == 0x60: res = 160
            elif x == 0x64: res = 64
            elif x == 0x76: res = 176
            elif x == 0x96: res = 96
            else:
                stderr.write("ERROR: unknown resolution")
                exit(-1)

            return res

        self.hardware_rev, self.firmware_rev = v[1], v[2]
        self.horizontal_res, self.vertical_res = tores(v[3]), tores(v[4])

        print("Connected to %s with %dx%d at %s"%(self.device_type, self.horizontal_res,
            self.vertical_res,ip))

    def _talk(self,cmd, resp_len=0, need_reply=False):
        sleep(.05)
        self.s.send(cmd)
        d = map(ord,self.s.recv(resp_len, MSG_WAITALL))
        return d

    def clear(self, color=0x0000):
        cmd_bg = "".join(['K', pack('>H', color)])
        self._talk(cmd_bg, 1)
        cmd_clear = "E"
        return self._talk(cmd_clear, 1)

    def upload(self, im):
        im  = im.resize((self.horizontal_res, self.vertical_res))
        cmd = "".join(['I\x00\x00',chr(self.horizontal_res),chr(self.vertical_res),chr(0x10)])

        # prepare message using the draw-image icon command. Scale
        # each colour values to 5-6-5 bits.
        for y in range(0,self.vertical_res):
            for x in range(0,self.horizontal_res):
                try: r,g,b = im.getpixel((x,y))
                except: r,g,b,a = im.getpixel((x,y))
                r,g,b = r>>3, g>>2, b>>3
                cmd += pack('>H', (r<<11) | (g<<5) | b)

        return self._talk(cmd,1)

    def sdcard_there(self):
        cmd = "@i"
        return self._talk(cmd,1)[0] == 0x06

    def sdcard_imgupload(self, im, slot=1):
        if not self.sdcard_there():
            return 0x01

        self.upload(im)

        cmd  = "@C\x00\x00"
        cmd += "".join([chr(x) for x in [self.horizontal_res, self.vertical_res]])

        imgsize_in_sectors = (self.horizontal_res*self.vertical_res*2) / 512
        if (self.horizontal_res*self.vertical_res*2) % 512 != 0:
            imgsize_in_sectors += 1

        addr = slot * imgsize_in_sectors
        cmd += "".join([chr(x) for x in [addr>>16, (addr&0x00ff00)>>8, addr&0xff]])

        return self._talk(cmd, 1)

    def sdcard_display(self, slot=1):
        if not self.sdcard_there():
            return 0x01

        cmd  = "@I\x00\x00"
        cmd += "".join([chr(x) for x in [self.horizontal_res, self.vertical_res]])
        cmd += "\x10"

        imgsize_in_sectors = (self.horizontal_res*self.vertical_res*2) / 512
        if (self.horizontal_res*self.vertical_res*2) % 512 != 0:
            imgsize_in_sectors += 1

        addr = slot * imgsize_in_sectors
        cmd += "".join([chr(x) for x in [addr>>16, (addr&0x00ff00)>>8, addr&0xff]])

        return self._talk(cmd, 1)

    def __str__(self):
        return "%s with %dx%d at %s"%(self.device_type, self.horizontal_res,
                self.vertical_res, self.ip)

The next code snippets is a demo script to upload an image to the µSD-card. It uses the PIL imaging library to access pixel-data. The function sdcard_imgupload scales the image to display-size and reduces its color to 65k format:

# -*- coding: utf8 -*-
#
# upload image to sdcard into slot x

from PIL import Image
from sys import argv,stderr,exit
from wDisplay import Display

ip, image, slot = None, None, None

if len(argv) == 3:
    image = open(argv[1], "r")
    slot  = int(argv[2])
elif len(argv) == 4:
    image = argv[1]
    slot  = int(argv[2])
    ip    = argv[3]
else:
    stderr.write("usage: python2 %s <image-file> <slot-number> [ip-addr]\n"%argv[0])
    exit(-1)

im = Image.open(image)

if ip == None: di = Display()
else:          di = Display(ip)

if di.sdcard_imgupload(im,slot)[0]==0x06:
    print "upload complete, %s in slot %d"%(image,slot)
else:
    print "upload failed"

The µOLED-display are not using any filesystem on the µSD-cards, rather writing and loading directly from the card. So storage for images is just allocated on sector-size-bounded slots, i.e. images allocate slot number x and are restored from this slot number. The script above takes such a slot number and an image file as arguments and stores the image there at this slots (Display.sdcard_imgupload()). Afterwards the following script can be used to display the image. This script calls the Display.sdcard_display() function with the supplied slot number:

# -*- coding: utf8 -*-
#
# show image stored in slot x on the display

from wDisplay import Display
from sys import argv,stderr,exit

slot,ip=None,None

if len(argv) == 2:
    slot = int(argv[1])
elif len(argv) == 3:
    slot = int(argv[1])
    ip   = argv[2]
else:
    stderr.write("usage: python2 %s <slot-number> [ip-addr]\n"%argv[0])
    exit(-1)

if ip == None: di = Display()
else:          di = Display(ip)

#di.clear()
di.sdcard_display(slot)

Naturally its best to store those python scripts as executable on your machine. The whole package of those script is available for download here.

As you hopefully have seen it is pretty easy to work with the wireless network these displays provide, as they're fully standard compliant and you can just use standard TCP/IP socket, on which there are plenty of tutorial out on the net.