/**
  ******************************************************************************
  * @file    display.c
  * @brief   This file provides code for the
  *          display MAIN, AUX, ANNUNCIATORS & SPLASH.
  ******************************************************************************
*/

/* Includes ------------------------------------------------------------------*/
#include "spi.h"
#include "main.h"
#include "timer.h"
#include "lcd.h"
#include "lt7680.h"
#include "display.h"
#include <string.h>  // For strchr, strncpy
#include <stdio.h>   // For debugging (optional)

//#define DURATION_MS 5000     // 5 seconds in milliseconds
#define TIMER_INTERVAL_MS 35 // The interval of your timed sub in milliseconds

// Display colours default
uint32_t MainColourFore = 0xFFFFFF;			// White FFFFFF
uint32_t AnnunColourFore = 0x00FF00;		// Green 00FF00
uint32_t BackgroundColour = 0x000000;		// Black 000000
uint32_t SplashIanJColourFore = 0xFFFF00;	// Yellow FFFF00
uint32_t TriggerColourFore = 0x00FF00;	    // Green 00FF00
extern volatile uint8_t dpFarRight;



//************************************************************************************************************************************************************

void DisplayMain(void)
{
	SetTextColors(MainColourFore, BackgroundColour); // Foreground, Background
	ConfigureFontAndPosition(
		0b00,    // Internal CGROM
		0b10,    // Font size
		0b00,    // ISO 8859-1
		0,       // Full alignment enabled
		0,       // Chroma keying disabled
		1,       // Rotate 90 degrees counterclockwise
		0b11,    // Width multiplier
		0b11,    // Height multiplier
		1,       // Line spacing
		4,       // Character spacing
		Xpos_MAIN,     // Cursor X
		Ypos_MAIN      // Cursor Y
	);

	// Always draw exactly 14 characters
	char text1[15];   // 14 chars + terminator
	int i;

	// Copy up to 14 source characters (some frames really have 14)
	for (i = 0; i < 14; i++) {
		char c = displayWithPunct[i];
		text1[i] = (c == '\0') ? ' ' : c;
	}

	text1[14] = '\0';

	ShiftUnitsRight(text1);		// Shift last 4 chars to the right if match criteria

	FixUnitText(text1);			// Fix units

	FixRandomStrings(text1);

	DrawText(text1);
}


// Fix random strings and text anomolies
void FixRandomStrings(char* text1)
{
	int i;
	static char prevText1[15] = "              ";  // 14 spaces + '\0'
	int reject = 0;

	// Reject any string containing '?'
	if (strchr(text1, '?') != NULL) {
		strcpy(text1, prevText1);
		reject = 1;
	}

	if (!reject) {

		// Only perform S->5 replacement if this is NOT the SELF TEST message
		if (strstr(text1, "5ELF TE5T") == NULL) {
			for (i = 0; i < 9; i++) {
				if (text1[i] == 'S') {
					text1[i] = '5';
				}
			}
		}

		// Fix random text anomalies
		if (strstr(text1, ":,:,:,:,:,:,:") != NULL) {
			strcpy(text1, "##############");
		}
		else if (strstr(text1, "5ELF TE5T") != NULL) {
			strcpy(text1, "SELF TEST OK  ");
		}

		// Fix ADR5 -> ADRS
		{
			char* p = strstr(text1, "ADR5");
			if (p != NULL) {
				p[3] = 'S';
			}
		}

		// Fix "C:" -> "C" (terminate string at colon)
		//{
		//	char* p = strstr(text1, "C:");
		//	if (p != NULL) {
		//		p[1] = '\0';   // end string after 'C'
		//	}
		//}

		// Save as last good string
		strcpy(prevText1, text1);
	}

	return;
}


// Shift chars right - We have more space on the TFT so can afford to do this
// Enter the original four chars to be shifted
void ShiftUnitsRight(char* text1)
{
	static const char* unit4[] = {
		" VDC", "MVDC",
		"KOHM", " OHM", "MOHM", "GOHM",
		"MAAC", "UAAC", "UADC", "MADC",
		" ADC", " AAC", "MVAC", " VAC",
		"MSEC", " SEC",
		"  HZ", " MHZ",
		"  DB"
	};

	for (int u = 0; u < (int)(sizeof(unit4) / sizeof(unit4[0])); u++) {
		const char* p = unit4[u];

		if (text1[9] == p[0] &&
			text1[10] == p[1] &&
			text1[11] == p[2] &&
			text1[12] == p[3]) {

			// shift ONLY the 4-char unit suffix right by one
			text1[13] = text1[12];
			text1[12] = text1[11];
			text1[11] = text1[10];
			text1[10] = text1[9];
			text1[9] = ' ';
			return;
		}
	}
}



// Replace characters
// Enter the before & after of the 4 chat text to be replaced
void FixUnitText(char* text1)
{
	static const struct {
		const char from[5];   // 4 chars + '\0'
		const char to[5];     // 4 chars + '\0'
	} rules[] = {
		{ "MSEC", "  ms" },
		{ " SEC", "   s" },
		{ "  HZ", "  Hz" },
		{ " MHZ", " MHz" },
		{ "  DB", "  dB" },
		{ "MVAC", "mVAC" },
		{ "MVDC", "mVDC" },
		{ "KOHM", "kohm" },
		{ " OHM", " ohm" },
		{ "GOHM", "Gohm" },
		{ "MOHM", "Mohm" },
		{ "MAAC", "mAAC" },
		{ "MADC", "mADC" },
		{ "ADR5", "ADRS" },
		{ "MADC", "mADC" },
		{ "UADC", "\xB5""ADC" }   // µADC (0xB5 in ISO-8859-1)
	};

	for (int i = 0; text1[i + 3] != '\0'; i++) {
		for (int r = 0; r < (int)(sizeof(rules) / sizeof(rules[0])); r++) {
			if (text1[i] == rules[r].from[0] &&
				text1[i + 1] == rules[r].from[1] &&
				text1[i + 2] == rules[r].from[2] &&
				text1[i + 3] == rules[r].from[3]) {

				text1[i] = rules[r].to[0];
				text1[i + 1] = rules[r].to[1];
				text1[i + 2] = rules[r].to[2];
				text1[i + 3] = rules[r].to[3];
				return; // only one unit expected
			}
		}
	}
}


void DisplayTrigger(void)
{
	SetTextColors(TriggerColourFore, BackgroundColour); // Foreground, Background
	ConfigureFontAndPosition(
		0b00,    // Internal CGROM
		0b10,    // Font size
		0b00,    // ISO 8859-1
		0,       // Full alignment enabled
		0,       // Chroma keying disabled
		1,       // Rotate 90 degrees counterclockwise
		0b01,    // Width multiplier
		0b01,    // Height multiplier
		1,       // Line spacing
		4,       // Character spacing
		Xpos_TRIGGER,     // Cursor X
		Ypos_TRIGGER      // Cursor Y
	);

	if (dpFarRight == 1) {
		char sym[2];
		sym[0] = 0x04;   // H0,L4 - diamond
		sym[1] = '\0';
		DrawText(sym);
	}
	else {
		DrawText(" ");
	}
}


void DisplayAnnunciators() {

	// ANNUNCIATORS - Print or clear text on the LCD
	const char* AnnuncNames[12] = {
		"SRQ", "LSTN", "TLK", "RMT", "MATH", "AZOFF", "2ohm", "4ohm", "MRNG", "STRG", "CAL", "SHIFT"
	};


	// Set Y-position of the annunciators
	int AnnuncYCoords[12] = {
		10,   // SRQ
		77,   // LSTN
		157,  // TLK
		220,  // RMT
		285,  // MATH
		365,  // AZOFF
		454,  // 2ohm
		532,  // 4ohm
		616,  // MRNG
		693,  // STRG
		770,  // CAL
		830   // SHIFT
	};


	for (int i = 0; i < 12; i++) {
		if (Annunc[12 - i] == 1) {  // Turn the annunciator ON
			// Continuity indicator (added feature): while the beeper is active the
			// lit 2ohm/4ohm annunciator is drawn as inverted "CONT" (black text in
			// a green box) instead of its name. Ohm mode is a precondition of the
			// beep, so one of the two slots is always lit when this fires. Same
			// 4-char cell, chroma keying off = full cell repaint, so the normal
			// draw restores the name cleanly the moment contBeeping clears.
			const char* label = AnnuncNames[i];
			uint32_t fg = AnnunColourFore;     // Foreground: Green
			uint32_t bg = BackgroundColour;    // Background: Black
			if (contBeeping && (i == 6 || i == 7)) {   // "2ohm" / "4ohm" slot
				label = "CONT";
				fg = BackgroundColour;         // black text...
				bg = AnnunColourFore;          // ...in a green box
			}
			SetTextColors(fg, bg);
			ConfigureFontAndPosition(
				0b00,    // Internal CGROM
				0b00,    // 16-dot font size
				0b00,    // ISO 8859-1
				0,       // Full alignment enabled
				0,       // Chroma keying disabled
				1,       // Rotate 90 degrees counterclockwise
				0b01,    // Width X0
				0b01,    // Height X0
				5,       // Line spacing
				0,       // Character spacing
				Xpos_ANNUNC,  // Cursor X (fixed)
				AnnuncYCoords[i] // Cursor Y (from array)
			);

			// TEMP TEST: force all annunciators ON (flash)
			//for (int k = 1; k <= 12; k++) {
			//	Annunc[k] = 1;
			//}

			DrawText(label); // Print the name (or inverted "CONT", see above)
		}
		else {  // Turn the annunciator OFF
			SetTextColors(BackgroundColour, BackgroundColour); // Foreground: Black, Background: Black
			ConfigureFontAndPosition(
				0b00,    // Internal CGROM
				0b00,    // 16-dot font size
				0b00,    // ISO 8859-1
				0,       // Full alignment enabled
				0,       // Chroma keying disabled
				1,       // Rotate 90 degrees counterclockwise
				0b01,    // Width X0
				0b01,    // Height X0
				5,       // Line spacing
				0,       // Character spacing
				Xpos_ANNUNC,  // Cursor X (fixed)
				AnnuncYCoords[i] // Cursor Y (from array)
			);
			DrawText(AnnuncNames[i]); // Clear the text by drawing in black
		}
	}

}


void DisplaySplash() {

	uint32_t originalColour = MainColourFore;

	// Colour sequence (no black or white)
	uint32_t splashCols[10] = {
		0xFF0000, // red
		0x00FF00, // green
		0x0000FF, // blue
		0xFFFF00, // yellow
		0xFF00FF, // magenta
		0x00FFFF, // cyan
		0xFF8000, // orange
		0x8000FF, // purple
		0x00FF80, // spring green
		0xFFFFFF  // white
	};

	for (int pass = 0; pass < 10; pass++) {

		MainColourFore = splashCols[pass];
		AnnunColourFore = splashCols[pass];

		// Main
		SetTextColors(MainColourFore, BackgroundColour); // Foreground, Background
		ConfigureFontAndPosition(
			0b00,    // Internal CGROM
			0b10,    // Font size
			0b00,    // ISO 8859-1
			0,       // Full alignment enabled
			0,       // Chroma keying disabled
			1,       // Rotate 90 degrees counterclockwise
			0b11,    // Width multiplier
			0b11,    // Height multiplier
			1,       // Line spacing
			4,       // Character spacing
			Xpos_MAIN,     // Cursor X
			Ypos_MAIN      // Cursor Y
		);

		// Always draw exactly 14 characters (13 source + 1 added)
		char text1[15];   // 14 chars + terminator
		int i;

		// Copy the 13 source characters (displayWithPunct is always 13 chars)
		for (i = 0; i < 13; i++) {
			char c = displayWithPunct[i];
			text1[i] = (c == '\0') ? ' ' : c;
		}

		// Default: add trailing space as the 14th character
		text1[13] = ' ';
		text1[14] = '\0';

		memcpy(text1, "##############", 14);
		text1[14] = '\0';

		DrawText(text1);

		HAL_Delay(10);

		// Annunciators
		const char* AnnuncNames[12] = {
		   "SRQ", "LSTN", "TLK", "RMT", "MATH", "AZOFF", "2ohm", "4ohm", "MRNG", "STRG", "CAL", "SHIFT"
		};

		// Set Y-position of the annunciators
		int AnnuncYCoords[12] = {
			10,   // SRQ
			77,   // LSTN
			157,  // TLK
			220,  // RMT
			285,  // MATH
			365,  // AZOFF
			454,  // 2ohm
			532,  // 4ohm
			616,  // MRNG
			693,  // STRG
			770,  // CAL
			830   // SHIFT
		};

		for (int i = 0; i < 12; i++) {

			SetTextColors(AnnunColourFore, BackgroundColour); // ON
			ConfigureFontAndPosition(
				0b00,    // Internal CGROM
				0b00,    // 16-dot font size
				0b00,    // ISO 8859-1
				0,       // Full alignment enabled
				0,       // Chroma keying disabled
				1,       // Rotate 90 degrees counterclockwise
				0b01,    // Width X0
				0b01,    // Height X0
				5,       // Line spacing
				0,       // Character spacing
				Xpos_ANNUNC,
				AnnuncYCoords[i]
			);

			DrawText(AnnuncNames[i]);
			HAL_Delay(5);
		}

		// Diamond
		SetTextColors(TriggerColourFore, BackgroundColour); // Foreground, Background
		ConfigureFontAndPosition(
			0b00,    // Internal CGROM
			0b10,    // Font size
			0b00,    // ISO 8859-1
			0,       // Full alignment enabled
			0,       // Chroma keying disabled
			1,       // Rotate 90 degrees counterclockwise
			0b01,    // Width multiplier
			0b01,    // Height multiplier
			1,       // Line spacing
			4,       // Character spacing
			Xpos_TRIGGER,     // Cursor X
			Ypos_TRIGGER      // Cursor Y
		);

		char sym[2];
		sym[0] = 0x04;   // H0,L4 - diamond
		sym[1] = '\0';
		DrawText(sym);

		HAL_Delay(1);  // Required after each full run
	}

	// Restore white at the end
	AnnunColourFore = 0x00FF00;
	MainColourFore = 0xFFFFFF;

	// --- Final redraw of annunciators in green so they actually end up green ---

	// Annunciators
	const char* AnnuncNames[12] = {
	   "SRQ", "LSTN", "TLK", "RMT", "MATH", "AZOFF", "2ohm", "4ohm", "MRNG", "STRG", "CAL", "SHIFT"
	};

	// Set Y-position of the annunciators
	int AnnuncYCoords[12] = {
		10,   // SRQ
		77,   // LSTN
		157,  // TLK
		220,  // RMT
		285,  // MATH
		365,  // AZOFF
		454,  // 2ohm
		532,  // 4ohm
		616,  // MRNG
		693,  // STRG
		770,  // CAL
		830   // SHIFT
	};

	for (int i = 0; i < 12; i++) {

		SetTextColors(AnnunColourFore, BackgroundColour); // ON
		ConfigureFontAndPosition(
			0b00,    // Internal CGROM
			0b00,    // 16-dot font size
			0b00,    // ISO 8859-1
			0,       // Full alignment enabled
			0,       // Chroma keying disabled
			1,       // Rotate 90 degrees counterclockwise
			0b01,    // Width X0
			0b01,    // Height X0
			5,       // Line spacing
			0,       // Character spacing
			Xpos_ANNUNC,
			AnnuncYCoords[i]
		);

		DrawText(AnnuncNames[i]);
		HAL_Delay(5);
	}
}


// Original splash not used.
void DisplaySplash2() {

	// Main
	SetTextColors(MainColourFore, BackgroundColour); // Foreground, Background
	ConfigureFontAndPosition(
		0b00,    // Internal CGROM
		0b10,    // Font size
		0b00,    // ISO 8859-1
		0,       // Full alignment enabled
		0,       // Chroma keying disabled
		1,       // Rotate 90 degrees counterclockwise
		0b11,    // Width multiplier
		0b11,    // Height multiplier
		1,       // Line spacing
		4,       // Character spacing
		Xpos_MAIN,     // Cursor X
		Ypos_MAIN      // Cursor Y
	);

	// Always draw exactly 14 characters (13 source + 1 added)
	char text1[15];   // 14 chars + terminator
	int i;

	// Copy the 13 source characters (displayWithPunct is always 13 chars)
	for (i = 0; i < 13; i++) {
		char c = displayWithPunct[i];
		text1[i] = (c == '\0') ? ' ' : c;
	}

	// Default: add trailing space as the 14th character
	text1[13] = ' ';
	text1[14] = '\0';

	memcpy(text1, "##############", 14);
	text1[14] = '\0';

	DrawText(text1);

	HAL_Delay(10);

	// Annunciators
	const char* AnnuncNames[12] = {
	   "SRQ", "LSTN", "TLK", "RMT", "MATH", "AZOFF", "2ohm", "4ohm", "MRNG", "STRG", "CAL", "SHIFT"
	};

	// Set Y-position of the annunciators
	int AnnuncYCoords[12] = {
		10,   // SRQ
		77,   // LSTN
		157,  // TLK
		220,  // RMT
		285,  // MATH
		365,  // AZOFF
		454,  // 2ohm
		532,  // 4ohm
		616,  // MRNG
		693,  // STRG
		770,  // CAL
		830   // SHIFT
	};

	for (int i = 0; i < 12; i++) {

		SetTextColors(AnnunColourFore, BackgroundColour); // ON
		ConfigureFontAndPosition(
			0b00,    // Internal CGROM
			0b00,    // 16-dot font size
			0b00,    // ISO 8859-1
			0,       // Full alignment enabled
			0,       // Chroma keying disabled
			1,       // Rotate 90 degrees counterclockwise
			0b01,    // Width X0
			0b01,    // Height X0
			5,       // Line spacing
			0,       // Character spacing
			Xpos_ANNUNC,
			AnnuncYCoords[i]
		);

		DrawText(AnnuncNames[i]);
		HAL_Delay(10);
	}

	// Diamond
	SetTextColors(TriggerColourFore, BackgroundColour); // Foreground, Background
	ConfigureFontAndPosition(
		0b00,    // Internal CGROM
		0b10,    // Font size
		0b00,    // ISO 8859-1
		0,       // Full alignment enabled
		0,       // Chroma keying disabled
		1,       // Rotate 90 degrees counterclockwise
		0b01,    // Width multiplier
		0b01,    // Height multiplier
		1,       // Line spacing
		4,       // Character spacing
		Xpos_TRIGGER,     // Cursor X
		Ypos_TRIGGER      // Cursor Y
	);

	char sym[2];
	sym[0] = 0x04;   // H0,L4 - diamond
	sym[1] = '\0';
	DrawText(sym);

}


//************************************************************************************************************************************************************
// Continuity beeper (added feature)
//
// Reads the already-decoded 3478A display + annunciators (sniffed from the meter's
// internal display bus in timer.c) and sounds an active buzzer on BEEPER_Pin when:
//   * the meter is in resistance mode  (ohm unit shown, 2- or 4-wire)
//   * the SHIFT annunciator is lit  (press SHIFT on the meter to arm continuity)
//   * the |reading| is <= CONTINUITY_THRESHOLD_OHMS
//
// Call once per main-loop pass. Globals used (from timer.c, via timer.h):
//   displayWithPunct[32]  - reading with inline punctuation - value parse only
//   displayStr[13]        - punctuation-free reading - all string matches
//   Annunc[13]            - Annunc[1]="SHIFT" indicator (names run SRQ..SHIFT
//                           against Annunc[12] down to Annunc[1])

// Live-watch friendly status
volatile float   contLastOhms = 0.0f;
volatile uint8_t contIsResistance = 0;
volatile uint8_t contShift = 0;
volatile uint8_t contBeeping = 0;

// The 3478A's character ROM shares one code between the digit 0 and the letter
// 'O', and one between 5 and 'S' (verified on hardware 2026-07-20 by dumping
// displayWithPunct over SWD: a 0.1885 ohm short arrives as " OO.188S  OHM").
// On the TFT this is invisible (O renders like 0, and FixRandomStrings patches
// S->5 in DisplayMain's local copy) but the raw globals keep the letters, so the
// value parser must translate them back or any reading containing a 0 or a 5 -
// essentially every shorted-leads reading - never parses and never beeps.
// Applied per-character inside the digit scan only, so the 'O' in " OHM" and in
// "OVLD" is untouched for the unit/overload string matches.
static char HP3478_LetterDigit(char c)
{
	if (c == 'O') return '0';
	if (c == 'S') return '5';
	return c;
}

void ContinuityCheck(void)
{
	// --- snapshot the decoded display + annunciators (an ISR updates these) ---
	// Two views of the same reading: displayWithPunct has the inline '.' the value
	// parser needs; displayStr is punctuation-free and is what ALL string matches
	// must use. In overload the range's DP segment stays lit, so displayWithPunct
	// reads "  O.VLD  MOHM." (verified by SWD dump 2026-07-20) - a strstr for
	// "OVLD" on it misses, and the letter-digit mapping then parses "O.VLD" as
	// 0.0 ohm, latching a phantom beep on open leads.
	char buf[32];       // displayWithPunct - for the numeric parse only
	char chars[13];     // displayStr - for the '?' / OVLD / unit matches
	uint8_t annShift;

	__disable_irq();
	memcpy(buf, (const void*)displayWithPunct, sizeof(buf));
	memcpy(chars, (const void*)displayStr, sizeof(chars));
	annShift = Annunc[1];   // "SHIFT" indicator
	__enable_irq();
	buf[sizeof(buf) - 1] = '\0';
	chars[sizeof(chars) - 1] = '\0';

	// Ignore obviously corrupted frames (the decoder marks unknown cells with '?').
	// Leave the buzzer in its current state until a clean frame arrives.
	if (strchr(chars, '?') != NULL) {
		return;
	}

	// Reject over-range / open-circuit so an "OVLD" reading never beeps.
	if (strstr(chars, "OVLD") != NULL || strstr(chars, "OVERLOAD") != NULL) {
		contBeeping = 0;
		__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 0);   // PB9 tone off (pin low)
		HAL_GPIO_WritePin(BEEPER_GPIO_Port, BEEPER_DC_Pin, GPIO_PIN_RESET);   // PB8 DC gate off
		return;
	}

	// --- resistance mode + unit multiplier from the unit text ---
	// (raw decoded units are uppercase: " OHM", "KOHM", "MOHM", "GOHM")
	float mult = 0.0f;                                    // 0 => not a resistance reading
	if      (strstr(chars, "KOHM") != NULL) mult = 1.0e3f;
	else if (strstr(chars, "MOHM") != NULL) mult = 1.0e6f;
	else if (strstr(chars, "GOHM") != NULL) mult = 1.0e9f;
	else if (strstr(chars, "OHM")  != NULL) mult = 1.0f;  // plain ohms

	contIsResistance = (mult > 0.0f) ? 1u : 0u;

	// Continuity is armed by pressing SHIFT on the meter: the SHIFT annunciator
	// lights and stays lit until pressed again, so it doubles as an on/off switch.
	contShift = (annShift != 0) ? 1u : 0u;

	uint8_t beep = 0;

	if (contIsResistance && contShift) {
		// Parse the (fixed-point) value by hand to avoid pulling newlib's heavy
		// strtod/float-formatting into flash. Format is: spaces, optional sign,
		// digits, optional single '.', digits. Sign is irrelevant for continuity.
		const char* s = buf;
		while (*s == ' ') s++;
		if (*s == '+' || *s == '-') s++;

		uint8_t sawDigit = 0;
		float val = 0.0f;
		for (;;) {
			char c = HP3478_LetterDigit(*s);   // 'O'->'0', 'S'->'5' (charset quirk)
			if (c < '0' || c > '9') break;
			val = val * 10.0f + (float)(c - '0');
			sawDigit = 1;
			s++;
		}
		if (*s == '.') {
			s++;
			float frac = 0.1f;
			for (;;) {
				char c = HP3478_LetterDigit(*s);   // 'O'->'0', 'S'->'5' (charset quirk)
				if (c < '0' || c > '9') break;
				val += (float)(c - '0') * frac;
				frac *= 0.1f;
				sawDigit = 1;
				s++;
			}
		}

		if (sawDigit) {                                // a real number was present
			float ohms = val * mult;
			contLastOhms = ohms;
			if (ohms <= CONTINUITY_THRESHOLD_OHMS) beep = 1;
		}
	}

	// Debounce the ON transition by one frame so a single bad decode can't blip the
	// buzzer; release immediately when the condition clears.
	static uint8_t onCount = 0;
	if (beep) { if (onCount < 2) onCount++; }
	else        onCount = 0;

	uint8_t out = (onCount >= 2) ? 1u : 0u;
	contBeeping = out;
	// Drive both beeper outputs (pick per buzzer type, both low when idle):
	// PB9 = 1 kHz tone via TIM4_CH4's compare register (500/1000 = 50% duty
	// square wave while beeping, 0 = PWM output permanently inactive = pin low)
	// for a passive transducer; PB8 = static high while beeping, for an active
	// (self-oscillating) buzzer.
	__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, out ? 500u : 0u);
	HAL_GPIO_WritePin(BEEPER_GPIO_Port, BEEPER_DC_Pin, out ? GPIO_PIN_SET : GPIO_PIN_RESET);
}


// The on-screen "CONT" indicator is drawn by DisplayAnnunciators(): while
// contBeeping is set, the lit 2ohm/4ohm annunciator is rendered as inverted
// "CONT" (black text in a green box) in place of its name. Two earlier attempts
// at a standalone label both failed on hardware: (110,10) sat inside the main
// reading field and was overdrawn every pass, and the band below the annunciator
// row is clipped by the bezel at roughly X >= 222 - there is no usable free
// strip on the panel.


