Zum Inhalt

voltmeter2.tc

voltmeter2.tc — smooth-sweep analog gauge using the TinyC canvas API.

Source on GitHub

// voltmeter2.tc — smooth-sweep analog gauge using the TinyC canvas API.
//
// Architecture:
//   * A single 320×320 PSRAM canvas (`face`) holds the pre-rendered gauge
//     background (bezel, coloured zones, ticks, labels). It never redraws
//     after startup.
//   * The needle is drawn directly on the PANEL (not on the canvas) each
//     frame. To erase the previous needle we push the matching sub-rect
//     from the clean canvas to the panel via `dspPushImageRect` — the
//     battle-tested path that the earlier voltmeter.tc validated.
//   * Animation runs from the built-in Every50ms() callback (20×/sec),
//     stepping `value` one unit at a time toward `target`. No blocking
//     delay() calls, no spawnTask — everything on the main VM path.
//
// Canvas API used:
//   imgCreate, imgBeginDraw/imgEndDraw, imgClear          (procedural face)
//   dspPushImageRect                                      (canvas -> panel)
//
// The newer imgBlit/imgFlush calls are demonstrated in jpg_on_canvas.tc;
// for this high-frequency needle animation, dspPushImageRect is proven
// smooth on the ILI9488 (tested 20 fps without frame drops).

#define FACE_SIZE     320
#define PANEL_X        80
#define PANEL_Y         0
#define CX            160
#define CY            160
#define NEEDLE_LEN    110
#define HUB_R          16

#define COL_BEZEL     0xBDF7
#define COL_RIM       0x632C
#define COL_FACE      0xFFFF
#define COL_GREEN     0x3605
#define COL_YELLOW    0xFCC0
#define COL_RED       0xC1A3
#define COL_TICK      0x18E3
#define COL_LABEL     0x18E3
#define COL_NEEDLE    0xE041
#define COL_HUB       0x2AB9

// NOTE: ANG_MIN / ANG_SPAN used to be #defines, but TinyC 1.4.3 has a
// preprocessor/codegen bug where #define float constants can be silently
// corrupted inside some function bodies (observed: needleXY producing
// ang = -1.0e9 instead of -135.0 even though frac was correct). Using
// literal -135.0 / 270.0 directly works around it. Reported separately.

int   face;                       // pristine gauge face canvas (never redrawn)
int   hx_out; int hy_out;

int   value  = 80;                // currently displayed (animated)
int   target = 80;                // desired position; Every50ms chases it
int   prev_v = -1;

void needleXY(int v, int len) {
    float frac; float ang; float rad;
    frac = (float)v / 100.0;
    if (frac < 0.0) { frac = 0.0; }
    if (frac > 1.0) { frac = 1.0; }
    ang  = -135.0 + frac * 270.0;
    rad  = ang * 3.14159 / 180.0;
    hx_out = CX + (int)(sin(rad) * (float)len);
    hy_out = CY - (int)(cos(rad) * (float)len);
}

void thickLine(int x0, int y0, int x1, int y1, int color, int width) {
    int d; int half;
    half = width / 2;
    dspColor(color, 0);
    d = 0 - half;
    while (d <= half) {
        dspPos(x0 + d, y0);    dspLine(x1 + d, y1);
        dspPos(x0, y0 + d);    dspLine(x1, y1 + d);
        d = d + 1;
    }
}

void drawArcBand(int r_out, int r_in, float a_from, float a_to, int color) {
    float a; float step;
    int x1; int y1; int x2; int y2;
    step = 0.02;
    a = a_from;
    dspColor(color, 0);
    while (a <= a_to) {
        x1 = CX + (int)(sin(a) * (float)r_in);
        y1 = CY - (int)(cos(a) * (float)r_in);
        x2 = CX + (int)(sin(a) * (float)r_out);
        y2 = CY - (int)(cos(a) * (float)r_out);
        dspPos(x1, y1);
        dspLine(x2, y2);
        a = a + step;
    }
}

void drawTick(int v, int r_out, int length, int color) {
    float frac; float ang; float rad;
    int x1; int y1; int x2; int y2;
    frac = (float)v / 100.0;
    ang  = -135.0 + frac * 270.0;
    rad  = ang * 3.14159 / 180.0;
    x1 = CX + (int)(sin(rad) * (float)r_out);
    y1 = CY - (int)(cos(rad) * (float)r_out);
    x2 = CX + (int)(sin(rad) * (float)(r_out - length));
    y2 = CY - (int)(cos(rad) * (float)(r_out - length));
    dspColor(color, 0);
    dspPos(x1, y1);
    dspLine(x2, y2);
}

void drawLabel(int v, int r) {
    float frac; float ang; float rad;
    int x; int y;
    char buf[8];
    frac = (float)v / 100.0;
    ang  = -135.0 + frac * 270.0;
    rad  = ang * 3.14159 / 180.0;
    x = CX + (int)(sin(rad) * (float)r) - 6;
    y = CY - (int)(cos(rad) * (float)r) - 6;
    if (v >= 100) { x = x - 6; }
    else if (v >= 10) { x = x - 2; }
    sprintfInt(buf, "%d", v);
    dspColor(COL_LABEL, COL_FACE);
    dspPos(x, y);
    dspDraw(buf);
}

void drawFace() {
    int v;
    dspColor(COL_BEZEL, 0);
    dspPos(CX, CY);   dspFillCircle(155);
    dspColor(COL_RIM, 0);
    dspPos(CX, CY);   dspCircle(150);
    dspColor(COL_FACE, 0);
    dspPos(CX, CY);   dspFillCircle(145);

    drawArcBand(142, 128, -2.356, 0.0,   COL_GREEN);
    drawArcBand(142, 128,  0.0,   0.942, COL_YELLOW);
    drawArcBand(142, 128,  1.414, 2.356, COL_RED);

    v = 0;
    while (v <= 100) {
        if (v % 10 == 0) {
            drawTick(v, 128, 12, COL_TICK);
        } else {
            drawTick(v, 128,  6, COL_TICK);
        }
        v = v + 2;
    }

    dspText("[f1s1]");
    v = 0;
    while (v <= 100) {
        drawLabel(v, 108);
        v = v + 10;
    }
}

// Compute a bounding box that covers both old & new needle line segments
// plus the centre hub cap. Out params via globals (TinyC has no structs).
int bx0; int by0; int bw; int bh;
void needleBBox() {
    int hx_old; int hy_old;
    int hx_new; int hy_new;
    int margin;
    int rx; int by;

    needleXY(prev_v, NEEDLE_LEN); hx_old = hx_out; hy_old = hy_out;
    needleXY(value,  NEEDLE_LEN); hx_new = hx_out; hy_new = hy_out;

    margin = 6;
    bx0 = CX - HUB_R - margin;
    by0 = CY - HUB_R - margin;
    if (hx_old - margin < bx0) { bx0 = hx_old - margin; }
    if (hy_old - margin < by0) { by0 = hy_old - margin; }
    if (hx_new - margin < bx0) { bx0 = hx_new - margin; }
    if (hy_new - margin < by0) { by0 = hy_new - margin; }

    rx = (hx_old > hx_new) ? hx_old : hx_new;
    if (CX + HUB_R > rx) { rx = CX + HUB_R; }
    rx = rx + margin;
    by = (hy_old > hy_new) ? hy_old : hy_new;
    if (CY + HUB_R > by) { by = CY + HUB_R; }
    by = by + margin;

    if (bx0 < 0) { bx0 = 0; }
    if (by0 < 0) { by0 = 0; }
    if (rx > FACE_SIZE) { rx = FACE_SIZE; }
    if (by > FACE_SIZE) { by = FACE_SIZE; }
    bw = rx - bx0;
    bh = by - by0;
}

void drawNeedle() {
    needleXY(value, NEEDLE_LEN);
    thickLine(CX + PANEL_X, CY + PANEL_Y,
              hx_out + PANEL_X, hy_out + PANEL_Y,
              COL_NEEDLE, 3);
    dspColor(COL_HUB, 0);
    dspPos(CX + PANEL_X, CY + PANEL_Y);
    dspFillCircle(HUB_R);
    dspColor(COL_FACE, 0);
    dspPos(CX + PANEL_X, CY + PANEL_Y);
    dspCircle(HUB_R);
}

// Erase old needle by pushing the clean face sub-rect, then paint the new
// needle on the panel. One call per animation frame.
void updateNeedle() {
    needleBBox();
    // Canvas -> panel blit (source coords = dest coords inside panel, shifted by PANEL_X/Y)
    dspPushImageRect(face, bx0, by0, bx0 + PANEL_X, by0 + PANEL_Y, bw, bh);
    drawNeedle();
}

// Step value one unit toward target. Returns 1 if anything changed.
int stepNeedle() {
    int delta; int gap;
    if (value == target) { return 0; }
    gap = target - value;
    if (gap < 0) { gap = -gap; }
    delta = 1;
    if (gap > 8) { delta = 2; }
    if (target < value) { delta = -delta; }
    value = value + delta;
    if ((delta > 0 && value > target) ||
        (delta < 0 && value < target)) {
        value = target;
    }
    updateNeedle();
    prev_v = value;
    return 1;
}

void main() {
    addCommand("VM");

    face = imgCreate(FACE_SIZE, FACE_SIZE);
    if (face < 0) {
        addLog("voltmeter2: imgCreate failed (PSRAM?)");
        return;
    }

    imgClear(face, COL_FACE);
    imgBeginDraw(face);
        drawFace();
    imgEndDraw();

    // Full-screen blit of the face, then draw the initial needle.
    dspPushImageRect(face, 0, 0, PANEL_X, PANEL_Y, FACE_SIZE, FACE_SIZE);
    drawNeedle();
    prev_v = value;
}

// Random-walk the target once per second.
void EverySecond() {
    int step;
    if (face < 0) { return; }
    step = ((tasm_second * 13) % 21) - 10;
    target = target + step;
    if (target < 0)   { target = 0;   }
    if (target > 100) { target = 100; }
}

// 20 Hz animation tick — one step per call, no blocking delay.
void Every50ms() {
    if (face < 0) { return; }
    stepNeedle();
}

void Command(char cmd[]) {
    char msg[64];
    char arg[16];
    if (strFind(cmd, "SET") == 0) {
        int v;
        if (strlen(cmd) > 4) {
            strSub(arg, cmd, 4, 0);
            v = atoi(arg);
        } else {
            v = 0;
        }
        if (v < 0)   { v = 0;   }
        if (v > 100) { v = 100; }
        target = v;
        sprintf(msg, "{\"VM\":{\"target\":%d,\"value\":%d}}", target, value);
        responseCmnd(msg);
    } else {
        responseCmnd("{\"VM\":\"? VMSET <0..100>\"}");
    }
}