Electric Locomotive control system. Touch screen driver control, includes regenerative braking, drives 4 brushless motors, displays speed MPH, system volts and power

Dependencies:   BSP_DISCO_F746NG FastPWM LCD_DISCO_F746NG SD_DISCO_F746NG TS_DISCO_F746NG mbed

Revision:
0:23cc72b18e74
Child:
1:8ef34deb5177
diff -r 000000000000 -r 23cc72b18e74 graphics.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graphics.cpp	Sun Nov 12 06:26:29 2017 +0000
@@ -0,0 +1,614 @@
+#include "mbed.h"
+#include "TS_DISCO_F746NG.h"
+#include "LCD_DISCO_F746NG.h"
+#include "dro.h"
+
+#define VOLTMETER_X 68      //  Voltmeter screen position
+#define VOLTMETER_Y 68
+#define AMMETER_X   68      //  Ammeter screen position - Now replaced by Power meter
+#define AMMETER_Y   202
+#define SPEEDO_X    274     //  Speedometer screen position
+#define SPEEDO_Y    135
+#define V_A_SIZE    54      //  Size of voltmeter and ammeter
+#define SPEEDO_SIZE 112
+
+#define SPEEDO_BODY_COLOUR  LCD_COLOR_BLACK
+#define SPEEDO_DIAL_COLOUR  LCD_COLOR_WHITE
+#define SPEEDO_TEXT_COLOUR  LCD_COLOR_BLUE
+
+#define VMETER_BODY_COLOUR  LCD_COLOR_BLACK
+#define VMETER_DIAL_COLOUR  LCD_COLOR_WHITE
+#define VMETER_TEXT_COLOUR  LCD_COLOR_BLUE
+
+#define AMETER_BODY_COLOUR  LCD_COLOR_BLACK
+#define AMETER_DIAL_COLOUR  LCD_COLOR_WHITE
+#define AMETER_TEXT_COLOUR  LCD_COLOR_BLUE
+
+extern  LCD_DISCO_F746NG    lcd;
+extern  TS_DISCO_F746NG     touch_screen;
+extern  Serial pc;
+
+static const int    char_widths[]   = {5, 7, 11, 14, 17, 17}    ,
+                    meter_radius_min = 30, meter_radius_max = 120;
+
+
+//  Uses our own generated sine and cosines from lookup table.  For some unexplained reason, using inbuilt sin and cos fns cause display flicker !
+extern double  jcos  (double angle);    //  Used in DrawNeedle, plain sin and cos functions cause display flicker !!
+extern double  jsin  (double angle);
+
+/*void    costabgen  (int points)  {
+    double  angle = 0.0;
+    while   (angle < 2.1 * PI)  {
+        pc.printf   ("Angle %f, my cos %+f, c cos %+f\r\n", angle, jcos(angle), cos(angle));
+//        pc.printf   ("Angle %f, my sin %+f, c sin %+f\r\n", angle, jsin(angle), sin(angle));
+        angle += PI / 24;
+    }
+//    double  angle;
+*//*    int step, perline = 0;
+    double  interval = PI / 2.0 / (double)points;
+    pc.printf   ("//At costabgen with %d points\r\n", points);
+    pc.printf   ("static const double costab[] = {\r\n");
+    for (step = 0; step <= points; step++)  {
+        angle   = interval * (double)step;
+//        pc.printf   ("cos %+.3f = %+.3f\r\n", angle, cos(angle));
+        if  (++perline == 8)  {
+            pc.printf   ("%+.6f,\r\n", cos(angle));
+            perline = 0;
+        }
+        else
+            pc.printf   ("%+.6f, ", cos(angle));
+        wait    (0.025);
+    }
+    pc.printf   ("0.0\t}\t;\r\n//End of costab\r\n");
+*/
+//}
+
+/**
+  * @brief  Fills a triangle (between 3 points).
+  * @param  x1: Point 1 X position
+  * @param  y1: Point 1 Y position
+  * @param  x2: Point 2 X position
+  * @param  y2: Point 2 Y position
+  * @param  x3: Point 3 X position
+  * @param  y3: Point 3 Y position
+  * @retval None
+  */
+static void FillTriangle(uint16_t x1, uint16_t x2, uint16_t x3, uint16_t y1, uint16_t y2, uint16_t y3)
+{
+    int16_t deltax = 0, deltay = 0, x = 0, y = 0, xinc1 = 0, xinc2 = 0,
+            yinc1 = 0, yinc2 = 0, den = 0, num = 0, num_add = 0, num_pixels = 0,
+            curpixel = 0;
+
+    deltax = abs(x2 - x1);        /* The difference between the x's */
+    deltay = abs(y2 - y1);        /* The difference between the y's */
+    x = x1;                       /* Start x off at the first pixel */
+    y = y1;                       /* Start y off at the first pixel */
+
+    if (x2 >= x1) {               /* The x-values are increasing */
+        xinc1 = 1;
+        xinc2 = 1;
+    } else {                      /* The x-values are decreasing */
+        xinc1 = -1;
+        xinc2 = -1;
+    }
+
+    if (y2 >= y1) {               /* The y-values are increasing */
+        yinc1 = 1;
+        yinc2 = 1;
+    } else {                      /* The y-values are decreasing */
+        yinc1 = -1;
+        yinc2 = -1;
+    }
+
+    if (deltax >= deltay) {       /* There is at least one x-value for every y-value */
+        xinc1 = 0;                  /* Don't change the x when numerator >= denominator */
+        yinc2 = 0;                  /* Don't change the y for every iteration */
+        den = deltax;
+        num = deltax / 2;
+        num_add = deltay;
+        num_pixels = deltax;         /* There are more x-values than y-values */
+    } else {                      /* There is at least one y-value for every x-value */
+        xinc2 = 0;                  /* Don't change the x for every iteration */
+        yinc1 = 0;                  /* Don't change the y when numerator >= denominator */
+        den = deltay;
+        num = deltay / 2;
+        num_add = deltax;
+        num_pixels = deltay;         /* There are more y-values than x-values */
+    }
+
+    for (curpixel = 0; curpixel <= num_pixels; curpixel++) {
+        lcd.DrawLine(x, y, x3, y3);
+
+        num += num_add;              /* Increase the numerator by the top of the fraction */
+        if (num >= den) {           /* Check if numerator >= denominator */
+            num -= den;               /* Calculate the new numerator value */
+            x += xinc1;               /* Change the x as appropriate */
+            y += yinc1;               /* Change the y as appropriate */
+        }
+        x += xinc2;                 /* Change the x as appropriate */
+        y += yinc2;                 /* Change the y as appropriate */
+    }
+}
+
+double  anglefix    (double a)  {       //  Ensures 0.0 <= angle <= + two PI
+    while   (a > PI) a   -= 2.0 * PI;
+    while   (a < 0.0) a   += 2.0 * PI;
+    return  a;
+}
+
+class    moving_coil_meter
+{
+    int     meter_radius, cent_x, cent_y, needle_len, scale_ticks,
+            disc_colour,    needle_colour,  scale_colour,   text_colour, body_colour, dec_places;
+    double  start_angle, end_angle, old_angle, value_min, value_max, rad_per_value, swept_angle, value_range;
+    double  Value;  //  This is the one that determines pointer angle
+
+    void    DrawNeedle          (double alpha, int colour)  ;
+    void    DrawScaleGraduations(int colour)  ;
+    double  get_pointer_angle   (double value)  ;
+    int     get_font            ()  ;
+
+public:
+
+    moving_coil_meter   ()  {   //  constructor
+        meter_radius = 100;
+        value_min = -1.0;
+        value_max = 1.0;
+        cent_x = cent_y = 150;
+        disc_colour     = LCD_COLOR_BLACK;
+        needle_colour   = LCD_COLOR_WHITE;
+        scale_colour    = LCD_COLOR_MAGENTA;
+        text_colour     = LCD_COLOR_RED;
+        body_colour     = LCD_COLOR_CYAN;
+        old_angle       = 0.0;
+    }
+
+    bool    setup   (int cx, int cy, int size, double lo, double hi, double start_ang, double end_ang, int scaleticks, char * units, int decimal_places)  ;
+    void    set_colours   (int bod_colour, int bgcol, int needlecol, int textcolour, int scalecol)  ;
+    void    set_value   (double v)  ;
+}   Voltmeter,  Powermeter,    Speedo;     //  3 instances of moving coil meter graphic
+
+void    moving_coil_meter::set_colours   (int bod_col, int bgcol, int needlecol, int textcol, int scalecol)  {
+    body_colour = bod_col;
+    disc_colour = bgcol;
+    needle_colour   = needlecol;
+    text_colour = textcol;
+    scale_colour    = scalecol;
+}
+
+void    moving_coil_meter::DrawNeedle  (double alpha, int colour)
+{
+    point  pixpts[4];
+    int save_colour, ssa, sca;
+    alpha   = anglefix  (alpha);
+    double  shortln = (needle_len / 18.7),
+            sina    = jsin(alpha),
+            cosa    = jcos(alpha);
+
+    save_colour = lcd.GetTextColor  ();
+    ssa = (int)(shortln * sina);
+    sca = (int)(shortln * cosa);
+    old_angle = alpha;
+    pixpts[0].x = cent_x - ssa;//(int)(shortln * sin(alpha));
+    pixpts[0].y = cent_y - sca;//(int)(shortln * cos(alpha));
+    pixpts[1].x = cent_x + (int)(needle_len * cosa);
+    pixpts[1].y = cent_y - (int)(needle_len * sina);  //  - as increasing y is downwards
+    pixpts[2].x = cent_x + ssa;//(int)(shortln * sin(alpha));
+    pixpts[2].y = cent_y + sca;//(int)(shortln * cos(alpha));
+    lcd.SetTextColor    (colour);
+    lcd.FillCircle      (cent_x, cent_y, (int)(needle_len / 15.0));
+    FillTriangle    (pixpts[0].x, pixpts[1].x, pixpts[2].x, pixpts[0].y, pixpts[1].y, pixpts[2].y);
+    lcd.SetTextColor    (save_colour);
+}
+
+void    moving_coil_meter::DrawScaleGraduations (int colour)
+{
+    int save_colour = lcd.GetTextColor  ();
+    int i, radius_inner = (int) meter_radius - 2, radius_outer = (int) (meter_radius * 0.9);
+    double  ang, cosang, sinang, angle_step;
+    lcd.SetTextColor    (colour);
+    ang = start_angle;
+    angle_step  = (start_angle - end_angle) / scale_ticks;
+    for (i = 0; i <= scale_ticks; i++)   {   //
+        cosang  = cos(ang);
+        sinang  = sin(ang);
+        lcd.DrawLine    (cent_x + radius_outer * cosang, cent_y - radius_outer * sinang, cent_x + radius_inner * cosang, cent_y - radius_inner * sinang);
+        ang -= angle_step;
+    }
+    lcd.SetTextColor    (save_colour);
+}
+
+void    displaytext    (int x, int y, const int font, char * txt)   ;
+
+bool    moving_coil_meter::setup   (int cx, int cy, int size, double lo, double hi, double start_ang, double end_ang, int scaleticks, char * units, int decimal_places)
+{
+    bool    retval = true;
+    int font, charwid, x_offset;
+    if  (size < meter_radius_min || size > meter_radius_max)
+        return  false;
+    meter_radius = size;
+    if  (meter_radius > cx || meter_radius > cy)
+        return  false;
+    int corner_rad          = meter_radius / 6,
+        screw_hole_offset   = meter_radius * 92 / 100,
+        screw_rad           = meter_radius / 13;
+    cent_x = cx;
+    cent_y = cy;
+
+    start_angle = start_ang;
+    end_angle = end_ang;
+    value_min = lo;
+    value_max = hi;
+    scale_ticks = scaleticks;
+    swept_angle = abs(start_angle - end_angle);
+    value_range = (value_max - value_min);
+    rad_per_value = swept_angle / value_range;
+    dec_places  = decimal_places;
+
+    needle_len = (int)(0.87 * (double)meter_radius);
+    int oldcolour1 = lcd.GetTextColor   ();
+    int oldcolour2 = lcd.GetBackColor   ();
+    lcd.SetTextColor    (body_colour);
+    //  Draw meter body as solid square with rounded corners, complete with mounting screw holes !
+    lcd.FillRect    (cent_x - meter_radius, cent_y - meter_radius - corner_rad, meter_radius * 2, corner_rad);
+    lcd.FillRect    (cent_x - meter_radius, cent_y + meter_radius, meter_radius * 2, corner_rad + 1);
+    lcd.FillRect    (cent_x - meter_radius - corner_rad, cent_y - meter_radius, 1 +(meter_radius + corner_rad) * 2, meter_radius * 2);
+    lcd.FillCircle  (cent_x - meter_radius, cent_y - meter_radius, corner_rad);  //  meter box has rounded corners
+    lcd.FillCircle  (cent_x - meter_radius, cent_y + meter_radius, corner_rad);
+    lcd.FillCircle  (cent_x + meter_radius, cent_y - meter_radius, corner_rad);
+    lcd.FillCircle  (cent_x + meter_radius, cent_y + meter_radius, corner_rad);
+    lcd.SetTextColor    (LCD_COLOR_DARKGRAY);
+    lcd.FillCircle  (cent_x - screw_hole_offset, cent_y - screw_hole_offset, screw_rad);  //  panel mounting screw holes near corners
+    lcd.FillCircle  (cent_x - screw_hole_offset, cent_y + screw_hole_offset, screw_rad);
+    lcd.FillCircle  (cent_x + screw_hole_offset, cent_y - screw_hole_offset, screw_rad);
+    lcd.FillCircle  (cent_x + screw_hole_offset, cent_y + screw_hole_offset, screw_rad);
+    lcd.SetTextColor    (disc_colour);
+    lcd.FillCircle      (cent_x, cent_y, meter_radius);
+    DrawScaleGraduations (scale_colour);   //drew the green trace around active needle-sweep angle
+
+    font = get_font ();
+    charwid = char_widths[font];
+    x_offset = charwid * strlen(units) / 2;
+    lcd.SetTextColor    (text_colour);
+    lcd.SetBackColor    (disc_colour);
+//    displaytext (cent_x - x_offset, cent_y + (meter_radius * 7) / 19, font, units);
+    displaytext (cent_x - x_offset, cent_y + (meter_radius * 6) / 19, font, units);
+    lcd.SetBackColor    (oldcolour2);
+    lcd.SetTextColor    (oldcolour1);
+    return  retval;
+}
+
+int     moving_coil_meter::get_font ()
+{
+    int font = meter_radius - meter_radius_min;
+    font /= 17;
+    if  (font > 4)
+        font = 4;
+    if  (font < 2)
+        font = 2;
+    return  font;
+}
+
+double  moving_coil_meter::get_pointer_angle    (double v)
+{
+    double   vabvmin, retval;
+    if   (v < value_min) v = value_min;
+    if   (v > value_max) v = value_max;
+    Value = v;   //  clipped copy of supplied value
+    vabvmin = v - value_min;
+    retval   = start_angle - (vabvmin * rad_per_value);
+    return   anglefix   (retval);
+}
+
+void    moving_coil_meter::set_value   (double meter_read_value)
+{
+    char    txt[32];
+    int x_offset, font, charwid, lenchk;//,
+    DrawNeedle  (old_angle, disc_colour);                   //  un-draw needle
+    DrawNeedle  (get_pointer_angle   (meter_read_value), needle_colour)  ; //  re-draw needle
+    if  (dec_places == ONE_DP)
+        sprintf (txt, " %+.1f \0", meter_read_value);
+    else
+        sprintf (txt, " %+.0f \0", meter_read_value);
+    lenchk = strlen(txt);
+    font = get_font();
+    charwid = char_widths[font];
+    x_offset = charwid * lenchk / 2;
+    lcd.SetTextColor    (text_colour);
+    lcd.SetBackColor    (disc_colour);
+    if  (lenchk > 0 && lenchk < 9)
+        displaytext (cent_x - x_offset, cent_y + (meter_radius * 11) / 19, font, txt);
+}
+//bool    moving_coil_meter::setup   (int cx, int cy, int size, double lo, double hi, double start_ang, double end_ang,
+//                                    int scale_ticks, char * units)
+void    vm_set  ()   //x   y  size minv  maxv  min angle   max angle,
+{
+    Speedo.set_colours  (SPEEDO_BODY_COLOUR, SPEEDO_DIAL_COLOUR, LCD_COLOR_RED, SPEEDO_TEXT_COLOUR, LCD_COLOR_BLACK);
+    Speedo.setup    (SPEEDO_X, SPEEDO_Y, SPEEDO_SIZE, 0.0, 12.0, 1.25 * PI, -0.25 * PI , 12, "MPH", ONE_DP);
+    Voltmeter.set_colours  (LCD_COLOR_BLACK, LCD_COLOR_WHITE, LCD_COLOR_RED, LCD_COLOR_BLUE, LCD_COLOR_MAGENTA);
+    Voltmeter.setup (VOLTMETER_X, VOLTMETER_Y, V_A_SIZE, 22.0, 59.0, 1.25 * PI, -0.25 * PI , 30, "V", ONE_DP);
+    Powermeter.set_colours  (LCD_COLOR_BLACK, LCD_COLOR_WHITE, LCD_COLOR_RED, LCD_COLOR_BLUE, LCD_COLOR_BLUE);
+    Powermeter.setup   (AMMETER_X, AMMETER_Y, V_A_SIZE, -1400.0, 1400.0, 1.25 * PI, -0.25 * PI , 14, "Watt", NO_DPS);
+}
+
+void    update_meters  (double speed, double current, double voltage)
+{
+    Powermeter.set_value(voltage * current);
+    Voltmeter.set_value (voltage);
+    Speedo.set_value    (speed);
+}
+
+
+
+struct  rect    {   struct point a, b; }   ;
+
+struct  butt_on  {
+    struct  rect    area;
+    int border_colour,  body_colour;
+    bool    in_use, pressed;//, released;
+    char txt1[12];
+    char txt2[12];
+}   ;
+
+struct  butt_on   button[NUMOF_BUTTONS];
+
+int get_button_press    (struct point & pt) ;
+int get_but_p   (int x, int y)
+{
+    struct  point   p;
+    p.x = x;
+    p.y = y;
+    return  get_button_press    (p);
+}
+
+
+void    read_keypresses    (struct ky_bd & a)
+{
+    int x;
+    a.count = 0;
+    a.sli   = false;
+    for (x = 0; x < MAX_TOUCHES; x++)
+        a.ky[x].keynum = -1;
+    int touches, but;
+    TS_StateTypeDef TS_State;
+    touch_screen.GetState(&TS_State);
+    touches = TS_State.touchDetected;
+    for (int h = 0; h < touches; h++)   {
+        but = get_but_p  (TS_State.touchX[h], TS_State.touchY[h]);
+        if  (but > - 1) {
+            a.ky[a.count].keynum = but;
+            a.ky[a.count].x     = TS_State.touchX[h];
+            a.ky[a.count].y     = TS_State.touchY[h];
+            if  (but == SLIDER) {
+                a.sli   = true;
+                a.slider_y  = a.ky[a.count].y;
+            }
+            a.count++;
+        }
+    }
+}
+
+
+void    displaytext    (int x, int y, char * txt)
+{
+    lcd.DisplayStringAt(x, y, (uint8_t *)txt, LEFT_MODE);
+}
+
+void    displaytext    (int x, int y, const int font, char * txt)
+{
+    sFONT * const fp[] = {&Font8, &Font12, &Font16, &Font20, &Font24};
+    lcd.SetFont(fp[font]);
+    displaytext (x, y, txt);
+}
+
+void    displaytext    (int x, int y, const int font, uint32_t BCol, uint32_t TCol, char * txt)
+{
+    uint32_t otc, obc;
+    otc = lcd.GetTextColor();
+    obc = lcd.GetBackColor();
+    lcd.SetTextColor(TCol);
+    lcd.SetBackColor(BCol);
+    displaytext (x, y, font, txt);
+    lcd.SetTextColor(otc);
+    lcd.SetBackColor(obc);
+}
+
+void    draw_button (struct butt_on & bu)
+{
+    int oldbgcolour;
+    lcd.SetTextColor    (bu.body_colour);
+    lcd.FillRect(bu.area.a.x + 2, bu.area.a.y + 2, bu.area.b.x - bu.area.a.x - 2, bu.area.b.y - bu.area.a.y - 2);   //, bu.body_colour);
+    oldbgcolour = lcd.GetBackColor();
+    lcd.SetBackColor(bu.body_colour);
+    lcd.SetTextColor(LCD_COLOR_BLACK);
+    if  (strlen(bu.txt2) == 0)   {
+        displaytext     (bu.area.a.x + 4, bu.area.a.y + 14, 4, bu.txt1); //  largest font 4
+    } else    {
+        displaytext     (bu.area.a.x + 4, bu.area.a.y + 4, 3, bu.txt1); //  not so large font 3
+        displaytext     (bu.area.a.x + 4, bu.area.a.y + 26, bu.txt2);
+    }
+    lcd.SetBackColor(LCD_COLOR_BLACK);
+    lcd.SetTextColor(bu.border_colour);
+    lcd.DrawRect(bu.area.a.x, bu.area.a.y, bu.area.b.x - bu.area.a.x, bu.area.b.y - bu.area.a.y);   //, bu.border_colour);
+    lcd.DrawRect(bu.area.a.x + 1, bu.area.a.y + 1, bu.area.b.x - bu.area.a.x - 1, bu.area.b.y - bu.area.a.y - 1);   //, bu.border_colour);
+    lcd.SetBackColor(oldbgcolour);
+}
+
+void    draw_button_hilight     (int but, int colour)
+{
+    if  (but < 0 || but > NUMOF_BUTTONS)    {
+        pc.printf   ("Button out of range in draw_button_hilight %d\r\n", but)  ;
+    } else    {
+        struct   butt_on * bu = &button[but];
+        int oldbgcolour = lcd.GetBackColor();//, minx, miny, maxx, maxy;
+        lcd.SetTextColor(colour);
+        lcd.DrawRect(bu->area.a.x - 1, bu->area.a.y - 1, bu->area.b.x - bu->area.a.x + 2, bu->area.b.y - bu->area.a.y + 2);
+        lcd.DrawRect(bu->area.a.x - 2, bu->area.a.y - 2, bu->area.b.x - bu->area.a.x + 4, bu->area.b.y - bu->area.a.y + 4);
+        lcd.DrawRect(bu->area.a.x - 2, bu->area.a.y - 3, bu->area.b.x - bu->area.a.x + 5, bu->area.b.y - bu->area.a.y + 6);
+        lcd.SetBackColor(oldbgcolour);
+    }
+}
+
+void    draw_button (struct butt_on & bu, int body_colour)
+{
+    bu.body_colour = body_colour;
+    draw_button (bu);
+}
+
+void    setup_button    (struct butt_on & bu, int x1, int y1, int dx, int dy, int bord, int body, char * txt1, char * txt2)
+{
+    static const int margin = 3;
+    int xsize = lcd.GetXSize();
+    int ysize = lcd.GetXSize();
+    int x2 = x1 + dx, y2 = y1 + dy;
+    if  (x1 < margin) x1 = margin;
+    if  (y1 < margin) y1 = margin;
+    if  (x2 > xsize - margin)    x2 = xsize - margin;
+    if  (y2 > ysize - margin)    y2 = ysize - margin;
+    bu.area.a.x = x1;
+    bu.area.a.y = y1;
+    bu.area.b.x = x2;
+    bu.area.b.y = y2;
+    bu.border_colour = bord;
+    bu.body_colour = body;
+    strcpy  (bu.txt1, txt1);
+    strcpy  (bu.txt2, txt2);
+    bu.in_use = true;
+    bu.pressed = false;
+    draw_button(bu);
+}
+
+bool    ifpressed   (int key)
+{
+    return  button[key].pressed;
+}
+
+bool    is_button_pressed   (struct point & pt, struct butt_on & bu)
+{
+    if  (bu.in_use)  {
+        if  (bu.area.a.x < pt.x && bu.area.b.x > pt.x
+                && bu.area.a.y < pt.y && bu.area.b.y > pt.y)
+            return  true;
+    }
+    return  false;
+}
+
+bool    keyrelease  (int key)
+{
+    bool    rv = false;
+    if  (button[key].pressed)   {
+        rv = true;
+        button[key].pressed = false;
+    }
+    return  rv;
+}
+void    setpressed  (int key, bool torf)
+{
+    button[key].pressed = torf;
+}
+void    setinuse    (int key, bool torf)
+{
+    button[key].in_use = torf;
+}
+
+
+int get_button_press    (struct point & pt)
+{
+    for (int j = 0; j < NUMOF_BUTTONS; j++)
+        if  (button[j].in_use && is_button_pressed   (pt, button[j]))
+            return  j;
+    return  -1;
+}
+
+void    setup_buttons   ()
+{
+    setup_button    (button[SPEEDO_BUT],
+                    SPEEDO_X - SPEEDO_SIZE, SPEEDO_Y - SPEEDO_SIZE,
+                    SPEEDO_SIZE * 2, SPEEDO_SIZE * 2, SPEEDO_BODY_COLOUR,   LCD_COLOR_RED,   " X", "")  ;
+    setup_button    (button[VMETER_BUT],
+                    VOLTMETER_X - V_A_SIZE, VOLTMETER_Y - V_A_SIZE, V_A_SIZE * 2, V_A_SIZE * 2, VMETER_BODY_COLOUR,   LCD_COLOR_RED,   " Y", "")  ;
+    setup_button    (button[AMETER_BUT],
+                    AMMETER_X - V_A_SIZE, AMMETER_Y - V_A_SIZE, V_A_SIZE * 2, V_A_SIZE * 2, AMETER_BODY_COLOUR,   LCD_COLOR_RED,   " Z", "")  ;
+    setup_button    (button[SLIDER],   SLIDERX, SLIDERY, SLIDERW, SLIDERH, LCD_COLOR_BLUE,   LCD_COLOR_MAGENTA,   "", "")  ;
+}
+
+
+void SliderGraphic (struct slide & q)   {
+    int
+    colr,
+    oldbgcolr   = lcd.GetBackColor   (),
+    oldtxtcolr  = lcd.GetTextColor   ();
+    char    txt[4];
+    txt[1] = 0;
+    if  (q.position > MAX_POS)
+        q.position = MAX_POS;
+    if  (q.position < MIN_POS)
+        q.position = MIN_POS;
+    if  (q.position == NEUTRAL_VAL)
+        q.state = NEUTRAL_DRIFT;
+    if  (q.position > NEUTRAL_VAL)
+        q.state = REGEN_BRAKE;
+    if  (q.position < NEUTRAL_VAL)
+        if  (q.state == REGEN_BRAKE)    {   //  Ensure transition from BRAKE to RUN passes through NEUTRAL
+            q.position = NEUTRAL_VAL;
+            q.state = NEUTRAL_DRIFT;
+        }
+        else
+            q.state = RUN;
+    if  (q.position == MAX_POS) {
+        if  (q.loco_speed < LOCO_HANDBRAKE_ESCAPE_SPEED)
+            q.state = PARK;
+        else    {
+            q.state = REGEN_BRAKE;
+            q.position--;
+        }
+    }
+    if  (q.position != q.oldpos)    {
+        //  Draw slider background colour rectangle overwriting previous circles
+        //  Redraw black vertical
+        //  Draw new circles
+        //  Write text char
+        lcd.SetTextColor(LCD_COLOR_MAGENTA);
+        lcd.FillRect    (SLIDERX + 1, q.oldpos - BUTTON_RAD, SLIDERW - 2, SLIDERW);
+        lcd.SetTextColor(LCD_COLOR_BLACK);
+        lcd.FillRect    (SLIDERX + (SLIDERW / 2) - 3, 6, 7, SLIDERH - 8);
+        q.oldpos = q.position;
+        lcd.SetTextColor(LCD_COLOR_WHITE);
+        lcd.DrawCircle  (CIRC_CTR, q.position, BUTTON_RAD);  //  seel also FillCircle
+        lcd.DrawCircle  (CIRC_CTR, q.position, BUTTON_RAD - 1);
+        switch  (q.state)  {
+            case    RUN:
+                txt[0] = 'R';
+                colr = LCD_COLOR_GREEN;
+                break;
+            case    NEUTRAL_DRIFT:
+                txt[0] = 'N';
+                colr = LCD_COLOR_BLUE;
+                break;
+            case    REGEN_BRAKE:
+                txt[0] = 'B';
+                colr = LCD_COLOR_ORANGE;
+                break;
+            case    PARK:
+                txt[0] = 'P';
+                colr = LCD_COLOR_RED;
+                break;
+            default:
+                txt[0] = 'X';
+                colr = LCD_COLOR_CYAN;
+        }   //  End of switch
+        lcd.SetTextColor(colr);
+        lcd.FillCircle  (CIRC_CTR, q.position, BUTTON_RAD - 2);
+        lcd.SetBackColor  (colr);
+        lcd.SetTextColor(LCD_COLOR_YELLOW);
+        displaytext(SLIDERX + 17, q.position - 10, 4, txt);   //  largest font
+        lcd.SetBackColor  (LCD_COLOR_BLACK);
+    }           //  End of else
+    lcd.SetTextColor (oldtxtcolr);
+    lcd.SetBackColor (oldbgcolr);
+//    pc.printf   ("SliderG %d, %d, %d\r\n", q.position, q.oldpos, q.state);
+}
+
+
+
+