Controlling 4DSystems Diablo16 and Picaso displays from Python
A few week ago I had a really good time testing 4DSystems 4Duino-24 board. One of the things I noticed is that the Serial Command Set interface is really flexible. You can easily drive the display from an 8-bit microcontroller. But you can also use more powerful controllers like an ESP8266 or an ARM machine like a Raspberry Pi or even my laptop.
4DSystems provide libraries for all those platforms and others. Most of those libraries share a common language: C (they have also developed libraries in Basic for PicAxe and Pascal). But even thou I spend a lot of time write C code, when I'm on my laptop a prefer higher level languages like Node.js or Python. So why not using Python to control these displays?
Actually, Python being written in C itself has a great support to wrap C libraries so you can use them from the language. Using Python to develop has several advantages:
- Powerful language with complex but easy-to-use data structures
- Rapid development since it's an interpreted language
- Mostly platform independent (you still need to compile the C libraries for your platform, but the wrapper and example should work without modifications)
- It's cool
Since the 4Duino-24 uses a Picaso chip I asked 4DSystems for a sample of one of their Diablo16 products. A gen4-uLCD-32DCT-CLB display arrived shortly after but I had no time to play with it until this week.
Compiling the C library
So what I wanted to do was a wrapper library in Python for the Diablo16 Serial Library. So first thing was to have a library to wrap.
The easiest way is to checkout the Diablo16 Serial Linux Library repository and build it. It is meant for Raspberry Pi but does work on my x86_64 Linux laptop. Once you have checked it out and “cd” to the folder it should be as simple as typing “make”. But it isn't.
$ make [Compile] diabloSerial.c diabloSerial.c: In function ‘OpenComm’: diabloSerial.c:2240:14: warning: ignoring return value of ‘write’, declared with attribute warn_unused_result [-Wunused-result] write(cPort, (unsigned char *)&ch, 1); ^ [Link (Dynamic)] /usr/bin/ld: diabloSerial.o: relocation R_X86_64_32 against `.rodata.str1.1' can not be used when making a shared object; recompile with -fPIC diabloSerial.o: error adding symbols: Bad value collect2: error: ld returned 1 exit status make: *** [diabloSerial.so] Error 1
It complains about you trying to link a dynamic library to a static one. But, good enough, it gives you the solution. Just edit the “Makefile” to add the -fPIC flag to the compile options:
CFLAGS = $(DEBUG) -Wall $(INCLUDE) -Winline -pipe -fPIC
$ make clean; make rm -f diabloSerial.o *~ core tags *.bak Makefile.bak libdiabloSerial.* [Compile] diabloSerial.c diabloSerial.c: In function ‘OpenComm’: diabloSerial.c:2240:14: warning: ignoring return value of ‘write’, declared with attribute warn_unused_result [-Wunused-result] write(cPort, (unsigned char *)&ch, 1); ^ [Link (Dynamic)]
The result is a “libdiabloSerial.so” you will have to copy somewhere the python wrapper could find it. More about this soon.
Wrapping it up
The Diablo16 and Picaso wrapper libraries are released as **free open software **and can be checked out at my 4DSystems Python repository on Bitbucket.
The trick is to use the ctypes library for Python. ctypes is a “foreign function library (that) provides C compatible data types, and allows calling functions in DLLs or shared libraries. It can be used to wrap these libraries in pure Python”.
It's actually really magic when you call your first C function from python code. So I spent a few hours wrapping the Diablo16 Serial library. I found a way to preserve most of the API in a simple and fast to code way. I only changed the way the wrapper returns values in some cases, like when returning strings or arrays of values.
Since it was the first time I was writing a wrapper with ctypes I spent some time trying to find a way to avoid writing thousands of lines of function definitions. I'm quite happy with the solution I found. Most of the code has been moved to configuration, and only those functions that deal with pointers (strings, arrays,…) or byref variables have their own wrapper function in the library.
I also realised that the differences between Diablo16 and Picaso APIs are really minimal so I created two libraries that both inherit from the same class and only define those calls that are different.
So this is the DiabloSerial.py library, that inherits from the BaseSerial.py and defines only those calls that are specific for the Diablo16 controller and also the relative path to the dynamic library to wrap.
from ctypes import * from BaseSerial import BaseSerial class DiabloSerial(BaseSerial): def library(self): return cdll.LoadLibrary("libs/libdiabloSerial.so") def definitions(self): definitions = super(DiabloSerial, self).definitions() definitions['bus_Read8'] = [c_uint16, ] definitions['bus_Write8'] = [None, [c_uint16]] definitions['putstr'] = [c_uint16, [c_char_p]] return definitions
There is little error check and I'm not sure all the calls would work, specially the error callbacks. I'm testing the wrapper as I'm writing sample code and so far so good. But if you happen to find a bug or have a suggestion please tell me.
Wiring the display
In the box, aside from the display, there was a small documentation booklet, a 30 pin flat cable and a gen4-IB interface board. This board brings out the minimum pins required to communicate with the display controller: 5V, TX, RX, GND and RES for reset.
So I grabbed my FTDI based USB to UART board and wired them together using 5V logic and crossing RX and TX lines. I didn't have to wire the RES pin.
A simple example
So let's test the wrapper.
import sys from DiabloSerial import DiabloSerial from DiabloConstants import * def callback(errcode, errbyte): print "ERROR: ", errcode, errbyte sys.exit(1) if __name__ == "__main__": diablo = DiabloSerial(500, True, callback) diablo.OpenComm('/dev/ttyUSB0', 9600) diablo.gfx_ScreenMode(PORTRAIT); diablo.gfx_Cls() print "Model:", diablo.sys_GetModel() print "Version:", diablo.sys_GetVersion() diablo.putCH(ord('A')) diablo.putCH(10) diablo.putstr("This is a string\n") diablo.putstr(str(10.3)) diablo.gfx_Button(1, 20, 40, GRAY, WHITE, FONT1, 3, 3, "Button") diablo.gfx_Circle(60, 160, 10, GRAY) diablo.gfx_Polyline(4, [140, 140, 150, 160], [10, 20, 10, 30], BLUE)
You can see that the API is very close to the C one. Function names and constants have been preserved and only when returning values, like when calling sys_GetModel, it wraps all the pointer related stuff to simply return a string.
A more complex example
Now I can use all the python resources around very easily. And all the modules already available, like for instance the MQTT Paho project, so I can show a button in the display that switches on and off my studio light sending MQTT messages to my local broker.
import sys import paho.mqtt.client as mqtt from DiabloSerial import DiabloSerial from DiabloConstants import * # ------------------------------------------------------------------------------ # CONFIGURATION # ------------------------------------------------------------------------------ MQTT_BROKER = '192.168.1.10' MQTT_PORT = 1883 MQTT_TOPIC = '/home/studio/lamp' # ------------------------------------------------------------------------------ buttonState = BUTTON_UP def on_connect(client, userdata, flags, rc): print("Connected to broker at %s" % MQTT_BROKER) client.subscribe(MQTT_TOPIC) def on_message(client, userdata, msg): global buttonState if msg.topic == MQTT_TOPIC: ''' I'm checking both conditions since I have a third option at home If payload == '2' it means toggle ''' if msg.payload == '1': buttonState = BUTTON_DOWN if msg.payload == '0': buttonState = BUTTON_UP show_button_state() def callback(errcode, errbyte): print "ERROR: ", errcode, errbyte sys.exit(1) def show_button_state(): global buttonState diablo.gfx_Button(buttonState, 20, 40, GRAY if buttonState == BUTTON_UP else RED, WHITE, FONT3, 3, 3, "Press Me") diablo.txt_Width(2); diablo.txt_Height(2); diablo.gfx_MoveTo(50,120); diablo.putstr("Light %s" % ("OFF" if buttonState == BUTTON_UP else "ON ")); def send_state(): global buttonState client.publish(MQTT_TOPIC, "1" if buttonState == BUTTON_DOWN else "0") if __name__ == "__main__": # Initialize display diablo = DiabloSerial(500, True, callback) diablo.OpenComm('/dev/ttyUSB0', 9600) diablo.touch_Set(TOUCH_ENABLE) diablo.gfx_Cls() show_button_state() # Initialize MQTT connection client = mqtt.Client() client.on_connect = on_connect client.on_message = on_message client.connect(MQTT_BROKER, MQTT_PORT, 60) x = y = 0 while True: client.loop(timeout=0.1) state = diablo.touch_Get(TOUCH_STATUS) if ((state == TOUCH_PRESSED) or (state == TOUCH_MOVING)): x = diablo.touch_Get(TOUCH_GETX) y = diablo.touch_Get(TOUCH_GETY) if (state == TOUCH_RELEASED): if ((x>=20) and (x<=220) and (y>=40) and (y<=100)): buttonState = not buttonState show_button_state() send_state()
"Controlling 4DSystems Diablo16 and Picaso displays from Python " was first posted on 19 October 2016 by Xose Pérez on tinkerman.cat under Projects and tagged 4d systems, 4duino-24, ctypes, diablo16, gen4-ib, picaso, python.