S-meter: segmented block bar with S-level labels (colour displays)

Rework the header RSSI bar (uiUtilityDrawRSSIBarGraph) from a solid two-stage
bar into discrete blocks, one per S-level, on colour displays:

- 9 green blocks for S1..S9 (4 dB per S-unit) plus 6 red blocks for the S9+
  region (10 dB per block, S9+10..S9+60), drawn with 2 px gaps so the header
  background shows between them.
- Proportional fill: full blocks below the level, leading block partially filled
  to reflect the exact signal.
- Bar height raised 4 -> 6 px; the FM/DMR mic-level bars follow the same height
  so nothing is left behind when switching RX <-> TX.
- Scale labels below the blocks: S1/S3/S5/S7/S9 under the green zone and
  +20/+40/+60 under the red, centred under their blocks (font_6x8). The label
  row is blanked while transmitting.

Monochrome displays keep the original solid bar. Existing analog S9+ RSSI
compression is preserved, so calibration is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marcus Kida 2026-07-06 11:58:46 +02:00
parent 0867742901
commit 009d3f7bdf

View file

@ -2262,62 +2262,119 @@ static void drawHeaderBar(int *barWidth, int16_t barHeight)
displayThemeResetToDefault(); displayThemeResetToDefault();
} }
// Segmented block S-meter (colour displays): 9 green blocks for S1..S9 (4 dB per
// S-unit) plus 6 red blocks for S9+10..S9+60 (10 dB per block), drawn with gaps so
// the header background shows between them. The leading block is partially filled to
// reflect the exact level.
#define SMETER_GREEN_BLOCKS 9
#define SMETER_RED_BLOCKS 6
#define SMETER_TOTAL_BLOCKS (SMETER_GREEN_BLOCKS + SMETER_RED_BLOCKS)
#define SMETER_BLOCK_GAP 2
#if defined(HAS_COLOURS)
#define HEADER_BAR_HEIGHT 6
#else
#define HEADER_BAR_HEIGHT 4
#endif
#if defined(HAS_COLOURS)
// S-meter scale labels drawn below the blocks. Each label is centred under the
// block whose upper edge corresponds to that level (S1..S9 in the green zone,
// +20/+40/+60 dB in the red zone).
static const struct { uint8_t block; const char *text; } smeterLabels[] = {
{ 0, "S1" }, { 2, "S3" }, { 4, "S5" }, { 6, "S7" }, { 8, "S9" },
{ 10, "+20" }, { 12, "+40" }, { 14, "+60" },
};
// The label row sits just below the bar; blank it (e.g. while transmitting, when
// the mic level replaces the S-meter and the labels no longer apply).
static void clearHeaderLabelRow(void)
{
displayThemeApply(THEME_ITEM_FG_HEADER_TEXT, THEME_ITEM_BG_HEADER_TEXT);
displayFillRect(0, (DISPLAY_Y_POS_BAR + HEADER_BAR_HEIGHT), DISPLAY_SIZE_X, FONT_SIZE_1_HEIGHT, true);
displayThemeResetToDefault();
}
#endif
void uiUtilityDrawRSSIBarGraph(void) void uiUtilityDrawRSSIBarGraph(void)
{ {
int rssi = trxGetRSSIdBm(RADIO_DEVICE_PRIMARY); int rssi = trxGetRSSIdBm(RADIO_DEVICE_PRIMARY);
if ((rssi > SMETER_S9) && (trxGetMode() == RADIO_MODE_ANALOG)) if ((rssi > SMETER_S9) && (trxGetMode() == RADIO_MODE_ANALOG))
{ {
// In Analog mode, the max RSSI value from the hardware is over S9+60. // In Analog mode the hardware RSSI reads well above S9+60, so compress the
// So scale this to fit in the last 30% of the display // over-S9 portion to keep the calibration consistent with the previous meter.
rssi = ((rssi - SMETER_S9) / STRONG_SIGNAL_RESCALE) + SMETER_S9; rssi = ((rssi - SMETER_S9) / STRONG_SIGNAL_RESCALE) + SMETER_S9;
// in Digital mode. The maximum signal is around S9+10 dB.
// So no scaling is required, as the full scale value is approximately S9+10dB
} }
#if defined(HAS_COLOURS)
// Fractional block position, in 1/100 block units (0 .. SMETER_TOTAL_BLOCKS*100).
// S0..S9 spans the 9 green blocks (4 dB per S-unit); S9..S9+60 spans the 6 red
// blocks (10 dB per block).
int blockPos;
if (rssi <= SMETER_S9)
{
blockPos = ((rssi - SMETER_S0) * 100) / 4;
}
else
{
blockPos = (SMETER_GREEN_BLOCKS * 100) + (((rssi - SMETER_S9) * 100) / 10);
}
blockPos = CLAMP(blockPos, 0, (SMETER_TOTAL_BLOCKS * 100));
// Scale the entire bar by 2. // Clear the whole bar band so the gaps (and unlit blocks) show the background.
// Because above S9 the values are scaled to 1/5. This results in the signal below S9 being doubled in scale displayThemeApply(THEME_ITEM_FG_RSSI_BAR, THEME_ITEM_BG_HEADER_TEXT);
// Signals above S9 the scales is compressed to 2/5. displayFillRect(0, DISPLAY_Y_POS_BAR, DISPLAY_SIZE_X, HEADER_BAR_HEIGHT, true);
for (int i = 0; i < SMETER_TOTAL_BLOCKS; i++)
{
int fill = blockPos - (i * 100); // 0..100 = how much of this block is lit
if (fill <= 0)
{
continue; // not reached yet: leave as background
}
if (fill > 100)
{
fill = 100;
}
int xStart = (i * DISPLAY_SIZE_X) / SMETER_TOTAL_BLOCKS;
int xEnd = ((i + 1) * DISPLAY_SIZE_X) / SMETER_TOTAL_BLOCKS;
int blockWidth = (xEnd - xStart) - SMETER_BLOCK_GAP;
if (blockWidth <= 0)
{
continue;
}
int fillWidth = (blockWidth * fill) / 100;
if (fillWidth <= 0)
{
continue;
}
displayThemeApply(((i < SMETER_GREEN_BLOCKS) ? THEME_ITEM_FG_RSSI_BAR : THEME_ITEM_FG_RSSI_BAR_S9P), THEME_ITEM_BG_HEADER_TEXT);
displayFillRect(xStart, DISPLAY_Y_POS_BAR, fillWidth, HEADER_BAR_HEIGHT, false);
}
// Draw the S-meter scale labels just below the blocks.
displayThemeApply(THEME_ITEM_FG_HEADER_TEXT, THEME_ITEM_BG_HEADER_TEXT);
for (int l = 0; l < (int)(sizeof(smeterLabels) / sizeof(smeterLabels[0])); l++)
{
int b = smeterLabels[l].block;
int blockCentre = (((b * DISPLAY_SIZE_X) / SMETER_TOTAL_BLOCKS) + (((b + 1) * DISPLAY_SIZE_X) / SMETER_TOTAL_BLOCKS)) / 2;
int textWidth = ((int)strlen(smeterLabels[l].text)) * 6; // FONT_SIZE_1 is 6 px wide
int textX = CLAMP((blockCentre - (textWidth / 2)), 0, (DISPLAY_SIZE_X - textWidth));
displayPrintAt(textX, (DISPLAY_Y_POS_BAR + HEADER_BAR_HEIGHT), smeterLabels[l].text, FONT_SIZE_1);
}
displayThemeResetToDefault();
#else
// Monochrome displays: keep the original solid bar.
rssi = (rssi - SMETER_S0) * 2; rssi = (rssi - SMETER_S0) * 2;
int barWidth = ((rssi * rssiMeterHeaderBarNumUnits) / rssiMeterHeaderBarDivider); int barWidth = ((rssi * rssiMeterHeaderBarNumUnits) / rssiMeterHeaderBarDivider);
drawHeaderBar(&barWidth, 4); drawHeaderBar(&barWidth, HEADER_BAR_HEIGHT);
#if defined(HAS_COLOURS)
int xPos = 0;
if (rssi > SMETER_S9)
{
xPos = (rssiMeterHeaderBar[9] * 2);
if (barWidth > xPos)
{
displayThemeApply(THEME_ITEM_FG_RSSI_BAR_S9P, THEME_ITEM_BG_HEADER_TEXT);
displayFillRect(xPos, DISPLAY_Y_POS_BAR, (barWidth - xPos), 4, false);
displayThemeResetToDefault();
}
}
#endif
#if 0 // Commented for now, maybe an option later.
int currentMode = trxGetMode();
for (uint8_t i = 1; ((i < 10) && (xPos <= barWidth)); i += 2)
{
if ((i <= 9) || (currentMode == RADIO_MODE_DIGITAL))
{
xPos = rssiMeterHeaderBar[i];
}
else
{
xPos = ((rssiMeterHeaderBar[i] - rssiMeterHeaderBar[9]) / STRONG_SIGNAL_RESCALE) + rssiMeterHeaderBar[9];
}
xPos *= 2;
ucDrawFastVLine(xPos, (DISPLAY_Y_POS_BAR + 1), 2, false);
}
#endif #endif
} }
@ -2343,13 +2400,19 @@ void uiUtilityDrawFMMicLevelBarGraph(void)
#endif #endif
, DISPLAY_SIZE_X); , DISPLAY_SIZE_X);
drawHeaderBar(&barWidth, 4); drawHeaderBar(&barWidth, HEADER_BAR_HEIGHT);
#if defined(HAS_COLOURS)
clearHeaderLabelRow();
#endif
} }
void uiUtilityDrawDMRMicLevelBarGraph(void) void uiUtilityDrawDMRMicLevelBarGraph(void)
{ {
int barWidth = ((uint16_t)(sqrt(micAudioSamplesTotal) * 1.5)); int barWidth = ((uint16_t)(sqrt(micAudioSamplesTotal) * 1.5));
drawHeaderBar(&barWidth, 4); drawHeaderBar(&barWidth, HEADER_BAR_HEIGHT);
#if defined(HAS_COLOURS)
clearHeaderLabelRow();
#endif
} }
void setOverrideTGorPC(uint32_t tgOrPc, bool privateCall) void setOverrideTGorPC(uint32_t tgOrPc, bool privateCall)