Making a PIL Gauge#

If the tkinter drawing is too slow we can use PIL, so as not to have endless backgrounds I have limited the choice to four, one colour for each of the main ranges with a standard size. Making the background is very similar to the tkinter gauge. When inserting the text for the gauge type, the width given required a different multiplier dependant on how many lines of text was used. In order to make a better looking background it has been enlarged then reduced.

Show/Hide Code class_pil.py

from PIL import Image, ImageDraw, ImageFont
from math import pi, sin, cos
from DialUtils import colour_choice,create_pieslice,create_chord,create_circle,\
        draw_text,draw_delta,draw_tick

class LCDpil:
    def __init__(self,select,enlargement=9): # ,unit='Temp C'
        self.select=select
        self.enlargement=enlargement
        #self.unit=unit

        # lcd gauge type; min,max scale extent; st,end start end degrees;
        # upt units per tick; w width pixels; tw tick width; farbe colour
        # mess gauge type
        self.lcds=lcds={
            1:{'lcd':'lcd70','min':-30,'max':70,'st':120,'end':60,'upt':1,
                    'w':201,'tw':3,'mess':'temperature','farbe': 'blue'},
            2:{'lcd':'lcd100','min':0,'max':100,'st':120,'end':60,'upt':1,
                    'w':201,'tw':3,'mess':'general\npurpose','farbe': 'purple'},
            3:{'lcd':'lcd255','min':0,'max':270,'st':135,'end':45,'upt':2,
                    'w':201,'tw':2,'mess':'analogue\n output','farbe': 'red'},
            4:{'lcd':'lcd1023','min':0,'max':1030,'st':117,'end':63,'upt':10,
                    'w':201,'tw':4,'mess':'analogue\n input','farbe': 'green'}
                    }

        self.e=e=enlargement
        we=lcds[select]['w']*e
        he=lcds[select]['w']*e
        self.ce=ce=we//2,he//2
        self.re=re=we//2
        lwe=re/25*e
        background='#090B0E'
        self.max_value=max_value=int(lcds[select]['lcd'].strip('lcd')) #70
        self.min_value=min_value=lcds[select]['min'] #-30
        val=(max_value+min_value)//2
        #print(val)
        max_scale=lcds[select]['max']
        self.unitspertick=unitspertick=lcds[select]['upt']
        self.dial=lcds[select]['lcd']
        mess=lcds[select]['mess']
        # angular calculations
        # 300° scale extent, 100 divisions therefore 1 tick interval 3 degrees
        degree_extent=360-lcds[select]['st']+lcds[select]['end'] # 300
        scale_extent=max_scale-min_value # 100
        self.tick_extent=tick_extent=degree_extent/scale_extent
        self.tick_width=tick_width=lcds[select]['tw']
        # trig_start=240 # scale degrees corresponding to 0° trig
        self.start_scale=start_scale=lcds[select]['st'] # physical start scale in degrees
        self.end_scale=end_scale=lcds[select]['end'] # physical end scale in degrees
        colour=lcds[select]['farbe']
        bdial,fdial,gdial,sdial=colour_choice(colour)
        self.fdial=fdial
        self.dfont=dfont = 'digital-7 (mono italic)'

        img = Image.new('RGBA', (we,he), '#00000000') # need transparency
        idraw = ImageDraw.Draw(img)

        # create bezel, add 20 degrees to scale extent
        create_pieslice(idraw,ce,re,fill=sdial,outline=None,start=start_scale-10,
                    end=end_scale+10)
        create_pieslice(idraw,ce,re-4*e,fill=bdial,outline=None,
                start=start_scale-10,end=end_scale+10)
        create_chord(idraw,ce,re,fill=sdial,outline=None,start=start_scale-10,
                    end=end_scale+10)
        create_chord(idraw,ce,re-4*e,fill=bdial,outline=None,start=start_scale-10,
                    end=end_scale+10)

        for j in range(0,scale_extent//unitspertick+1):
            angle=j*tick_extent*unitspertick + start_scale # measured angle
            draw_tick(idraw,ce,re-8*e,2*e,angle,fill=fdial,width=2*e)
            create_pieslice(idraw,ce,re-10*e,fill=gdial,outline=None,
                start=angle-0.5,end=angle+0.5)

        create_chord(idraw,ce,re-18*e,fill=bdial,outline=None,start=start_scale,
                    end=end_scale)

        # inner ticks need to be before any deltas
        if max_value != 1023:
            for j in range(0,scale_extent//unitspertick+1):
                angle=j*tick_extent*unitspertick + start_scale
                draw_tick(idraw,ce,re-20*e,2*e,angle,fill=sdial,width=1*e)
        else:
            for j in range(0,scale_extent//unitspertick+1):
                angle=j*tick_extent*unitspertick + start_scale
                draw_tick(idraw,ce,re-20*e,2*e,angle,fill=sdial,width=1*e)

        # deltas and scale numbers
        for j in range(0,scale_extent//unitspertick+1):
            angle=j*tick_extent*unitspertick + start_scale
            if j % 10==0:
                tangle=str(int(j+min_value)*unitspertick)
                draw_delta(idraw,angle,ce,re-19*e,e,fill=fdial)
                draw_text(idraw,tangle,ce,re-29*e,angle,fill=fdial,
                    font=dfont,size=10*e)

        # ghost value
        if max_value != 1023:
            draw_text(idraw,'888',ce,0,0,fill=gdial,font=dfont,size=60*e)
        else:
            draw_text(idraw,'8888',ce,0,0,fill=gdial,font=dfont,size=45*e)

        # gauge type
        y=(re*4//10 if max_value in (70,255,1023) else re//2)
        ttffont = ImageFont.truetype(dfont+'.ttf', size=10*e)
        (width, height), (offset_x, offset_y) = ttffont.font.getsize(mess)
        dx=(-width//2 if max_value==70 else -width//4)
        idraw.text((ce[0]+dx-offset_x,ce[1]-y-height//2-offset_y),
            mess,fill=fdial,font=ttffont)

        w=lcds[select]['w']
        gtype=lcds[select]['lcd']
        # better looking than just drawing original size
        imgr=img.resize((w,w),resample=Image.Resampling.LANCZOS)
        imgr.save('../figures/'+gtype+'.png')

if __name__ =='__main__':
    LCDpil(4)


You should see background images as follows:-

Background Choices#

Temperature

General

Analogue Output

Analogue Input

blue digital gauge -30 to 70

mauve digital gauge 0 to 100

red digital gauge 0 to 255

green digital gauge 0 to 1023

-30 to 70

0 to 100

0 to 255

0 to 1023

Show/Hide Code lcd_pil.py

from PIL import Image, ImageDraw, ImageTk
from tkinter import Tk, Label, PhotoImage
from DialUtils import colour_choice,create_circle,draw_text,create_pieslice

def pilpointer(select,val,unit='Temp C'):

    # lcd gauge type; min,max scale extent; st,end start end degrees;
    # upt units per tick; w width pixels; tw tick width; farbe colour
    # mess gauge type
    lcds={
            1:{'lcd':'lcd70','min':-30,'max':70,'st':120,'end':60,'upt':1,
                    'w':201,'tw':3,'mess':'temperature','farbe': 'blue'},
            2:{'lcd':'lcd100','min':0,'max':100,'st':120,'end':60,'upt':1,
                    'w':201,'tw':3,'mess':'general\npurpose','farbe': 'purple'},
            3:{'lcd':'lcd255','min':0,'max':270,'st':135,'end':45,'upt':2,
                    'w':201,'tw':2,'mess':'analogue\n output','farbe': 'red'},
            4:{'lcd':'lcd1023','min':0,'max':1030,'st':117,'end':63,'upt':10,
                    'w':201,'tw':4,'mess':'analogue\n input','farbe': 'green'}
                    }

    h=w=lcds[select]['w']
    c=w//2,h//2
    r=w//2
    max_value=lcds[select]['max']
    min_value=lcds[select]['min']
    max_scale=lcds[select]['max']
    unitspertick=lcds[select]['upt']
    # 300° scale extent, 100 divisions therefore 1 tick interval 3 degrees
    degree_extent=360-lcds[select]['st']+lcds[select]['end'] # 300
    scale_extent=max_scale-min_value # 100
    tick_extent=degree_extent/scale_extent
    # trig_start=240 # scale degrees corresponding to 0° trig
    start_scale=lcds[select]['st'] # physical start scale in degrees
    colour=lcds[select]['farbe']
    fdial=colour_choice(colour)[1]

    dfont = 'digital-7 (mono italic)'

    img = Image.new('RGBA', (w,h), '#00000000') # need transparency
    idraw = ImageDraw.Draw(img)

    for j in range(0,(val-min_value)//unitspertick+1): # pointer leds, add dial starting value
        angle=j*tick_extent*unitspertick + start_scale # measured angle
        create_pieslice(idraw,c,r-10,fill=fdial,outline=None,start=angle-0.5,
                end=angle+0.5)

    create_circle(idraw,c,r-18,fill='#00000000',outline=None)

    # display draw_text needs 3 or 4 letters or spaces
    sval=str(val)
    if max_value != 1023:
        pinput=(3-len(sval))*' '+sval
        draw_text(idraw,pinput,c,0,0,fill=fdial,font=dfont,
                size=60)

    else:
        pinput=(4-len(sval))*' '+sval
        draw_text(idraw,pinput,c,0,0,fill=fdial,font=dfont,
                size=45)

    y=(r*7//10 if max_value==255 else r*3//5)
    draw_text(idraw,unit,(c[0],c[1]+y),0,0,fill=fdial,font=dfont,size=10)

    gtype=lcds[select]['lcd']
    bimg=Image.open('../figures/'+gtype+'.png')
    cimg=Image.alpha_composite(bimg,img)

    # cimg.save('lcd_comp_point_test.png', quality=95)
    return cimg


if __name__ =='__main__':
    root=Tk()
    # select 1,2,3,4 -30:70,0:100,0:255,0:1023
    select=1
    # val value to be shown
    val=25
    image=pilpointer(select,val)
    tki=ImageTk.PhotoImage(image)
    l=Label(root,image=tki)
    l.image=tki
    l.grid()
    root.mainloop()


After the background is saved we can retreive it and superimpose the displayed value and any unit description, This means that the realtime operations are limited, but the downside is that we need to first work with an image, then retreive the background image from disk, combine these two images before taking the result and load into tkinter to display. With so many image operations including a disk read the overall speed was almost 3 times slower than using tkinter direct.

Say we open the background file, then keep it open and make a copy and use this copy to work with, then everytime the value changed, close the old copy and create a new copy. This would save the disk read, (apart from the initial time) , we would draw on the copy, so there is no alpha composite operation. This in turn means we cannot use pieslice for the large ticks, but must use draw_tick instead - this is because we cannot clean up the centres of background required when using pieslices.

Show/Hide Code lcd_pil_rev.py

from PIL import Image, ImageDraw, ImageTk
from tkinter import Tk, Label, PhotoImage
from DialUtils import colour_choice,draw_text,draw_tick

class lcd_pil:
    def __init__(self,select,unit='Temp C'):

        # lcd gauge type; min,max scale extent; st,end start end degrees;
        # upt units per tick; w width pixels; tw tick width; farbe colour
        # mess gauge type
        lcds={
            1:{'lcd':'lcd70','min':-30,'max':70,'st':120,'end':60,'upt':1,
                    'w':201,'tw':3,'mess':'temperature','farbe': 'blue'},
            2:{'lcd':'lcd100','min':0,'max':100,'st':120,'end':60,'upt':1,
                    'w':201,'tw':3,'mess':'general\npurpose','farbe': 'purple'},
            3:{'lcd':'lcd255','min':0,'max':270,'st':135,'end':45,'upt':2,
                    'w':201,'tw':2,'mess':'analogue\n output','farbe': 'red'},
            4:{'lcd':'lcd1023','min':0,'max':1030,'st':117,'end':63,'upt':10,
                    'w':201,'tw':4,'mess':'analogue\n input','farbe': 'green'}
                    }

        self.unit=unit
        h=w=lcds[select]['w']
        self.c=w//2,h//2
        self.r=w//2
        #max_value=lcds[select]['max']
        self.min_value=lcds[select]['min']
        self.max_scale=max_scale=lcds[select]['max']
        self.unitspertick=lcds[select]['upt']
        # 300° scale extent, 100 divisions therefore 1 tick interval 3 degrees
        degree_extent=360-lcds[select]['st']+lcds[select]['end'] # 300
        scale_extent=max_scale-self.min_value # 100
        self.tick_extent=degree_extent/scale_extent
        self.tick_width=lcds[select]['tw']
        # trig_start=240 # scale degrees corresponding to 0° trig
        self.start_scale=lcds[select]['st'] # physical start scale in degrees
        colour=lcds[select]['farbe']
        self.fdial=colour_choice(colour)[1]

        self.dfont = 'digital-7 (mono italic)'

        gtype=lcds[select]['lcd']
        self.bimg=Image.open('../figures/'+gtype+'.png')

    def pilpointer(self,val):
        img = self.bimg
        dfont=self.dfont
        c=self.c
        r=self.r
        fdial=self.fdial

        #img = Image.new('RGBA', (w,h), '#00000000') # need transparency
        idraw = ImageDraw.Draw(img)

        for j in range(0,(val-self.min_value)//self.unitspertick+1): # pointer leds, add dial starting value
            angle=j*self.tick_extent*self.unitspertick + self.start_scale # measured angle
            draw_tick(idraw,c,r-17,8,angle,fill=self.fdial,
                width=self.tick_width)

        #create_circle(idraw,c,r-18,fill='#00000000',outline=None)

        # display draw_text needs 3 or 4 letters or spaces
        sval=str(val)
        if self.max_scale != 1023:
            pinput=(3-len(sval))*' '+sval
            draw_text(idraw,pinput,c,0,0,fill=fdial,font=dfont,size=60)

        else:
            pinput=(4-len(sval))*' '+sval
            draw_text(idraw,pinput,c,0,0,fill=fdial,font=dfont,size=45)

        y=(r*7//10 if self.max_scale==255 else r*3//5)
        draw_text(idraw,self.unit,(c[0],c[1]+y),0,0,fill=fdial,font=dfont,size=10)

        #gtype=lcds[select]['lcd']
        #bimg=Image.open('../figures/'+gtype+'.png')
        #cimg=Image.alpha_composite(bimg,img)

        #img.save('lcd_comp_point_test.png', quality=95)
        return img


if __name__ =='__main__':
    root=Tk()
    # select 1,2,3,4 -30:70,0:100,0:255,0:1023
    select=1
    # val value to be shown
    val=35
    lp=lcd_pil(select)
    im=lp.pilpointer(val)
    tki=ImageTk.PhotoImage(im)
    l=Label(root,image=tki)
    l.image=tki
    l.grid()
    root.mainloop()


Note

Only the major changes have been highlighted, the changes needed for creating a class are not shown.

After all that the revised PIL script took about 60% of the time for tkinter to draw on a canvas just the changes associated with a new value.

It should be emphasized that whether we wish to use any python program the interface to the Arduino remains much the same, in fact we could use the inbuilt serial window or plotter instead of the python programs. This gives us a useful testing point, if the serial window or plotter work, then we can use the sketch to hook up to another program.