voltmeter2.tc¶
voltmeter2.tc — smooth-sweep analog gauge using the TinyC canvas API.
// 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>\"}");
}
}