added colour and fixed set tx adc not sticking

This commit is contained in:
Matthew Burton
2026-06-29 20:22:52 +02:00
parent a7ff39352c
commit 43f455393e
16 changed files with 684 additions and 33 deletions
+65
View File
@@ -248,6 +248,71 @@ void LoRaRadioBase::buildModemConfig(struct lora_modem_config &cfg, bool tx)
cfg.cad.mode = LORA_CAD_MODE_LBT;
}
uint32_t LoRaRadioBase::getActiveFrequencyHz() const
{
float freq_mhz = _has_radio_override ? _override_freq
: (_prefs ? _prefs->freq : (LoRaConfig::FREQ_HZ / 1000000.0f));
return (uint32_t)(freq_mhz * 1000000.0f + 0.5f);
}
uint16_t LoRaRadioBase::getActiveBandwidthKHzX10() const
{
float bw_khz = _has_radio_override ? _override_bw
: (_prefs ? _prefs->bw : (float)LoRaConfig::BANDWIDTH);
return (uint16_t)(bw_khz * 10.0f + 0.5f);
}
uint8_t LoRaRadioBase::getActiveSpreadingFactor() const
{
return _has_radio_override ? _override_sf
: (_prefs ? _prefs->sf : LoRaConfig::SPREADING_FACTOR);
}
uint8_t LoRaRadioBase::getActiveCodingRate() const
{
return _has_radio_override ? _override_cr
: (_prefs ? _prefs->cr : LoRaConfig::CODING_RATE);
}
uint16_t LoRaRadioBase::getActivePreambleLength() const
{
return preambleLengthForSF(getActiveSpreadingFactor());
}
uint8_t LoRaRadioBase::getActiveSyncWord() const
{
/* buildModemConfig() currently sets public_network=false, which maps
* Zephyr's LoRa API to the Semtech private sync word. */
return 0x12;
}
int8_t LoRaRadioBase::getConfiguredTxPower() const
{
int power = _prefs ? _prefs->tx_power_dbm : LoRaConfig::TX_POWER_DBM;
#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM
if (power > CONFIG_ZEPHCORE_MAX_TX_POWER_DBM) {
power = CONFIG_ZEPHCORE_MAX_TX_POWER_DBM;
}
#endif
if (power < -9) {
power = -9;
}
return (int8_t)power;
}
int8_t LoRaRadioBase::getEffectiveTxPower() const
{
int power = (int)getConfiguredTxPower() - (int)_tx_power_reduction_db;
if (power < -9) {
power = -9;
}
return (int8_t)power;
}
/**
* Compare radio-relevant fields of two modem configs.
* Ignores the tx flag — that only selects TX vs RX mode, the actual
+13
View File
@@ -81,6 +81,19 @@ public:
void setRxBoost(bool enable);
bool isRxBoostEnabled() const { return _rx_boost_enabled; }
/* Read-only view of the modem config currently used by buildModemConfig().
* These honor temporary radio overrides for freq/bw/sf/cr and the same TX
* clamps/APC reduction as the actual lora_config() path. */
uint32_t getActiveFrequencyHz() const;
uint16_t getActiveBandwidthKHzX10() const;
uint8_t getActiveSpreadingFactor() const;
uint8_t getActiveCodingRate() const;
uint16_t getActivePreambleLength() const;
uint8_t getActiveSyncWord() const;
int8_t getConfiguredTxPower() const;
int8_t getEffectiveTxPower() const;
bool isTxActive() const { return atomic_get(&_tx_active) != 0; }
/* Duty-cycle preamble false-positive counter.
* Incremented by the driver whenever RX_TX_TIMEOUT fires in
* duty-cycle mode and the chip is silently re-armed. High
+25
View File
@@ -152,6 +152,31 @@ public:
*/
void setPinChangeCallback(PinChangeCallback cb) { _pin_change_cb = cb; }
#ifdef CONFIG_ZEPHCORE_APC
/* Adaptive Power Control hooks used by the USB text CLI. */
int8_t getAPCReduction() const {
return getPowerController().getPowerReduction();
}
float getAPCMargin() const {
return getPowerController().getMarginEstimate();
}
bool isAPCEnabled() const {
return getPowerController().isEnabled();
}
void setAPCEnabled(bool en) {
getPowerController().setEnabled(en);
if (!en) {
_radio->setTxPowerReduction(0);
}
}
uint8_t getAPCTargetMargin() const {
return getPowerController().getTargetMargin();
}
void setAPCTargetMargin(uint8_t margin_db) {
getPowerController().setTargetMargin(margin_db);
}
#endif
/**
* Continue contact iteration (call each main loop iteration).
* Returns true if contacts are still being sent.
@@ -10,6 +10,7 @@
# - Button: P1.10 (active-LOW)
# - ST7735S 0.96" 160x80 TFT (SPI3, MIPI-DBI, mono-tft wrapper)
# - External SPI flash footprint present but device is not identified/enabled
# - No buzzer/audio hardware
# Board identification (matches Arduino MeshCore variant name)
CONFIG_ZEPHCORE_BOARD_NAME="Heltec T096"
@@ -35,3 +36,6 @@ CONFIG_HEAP_MEM_POOL_SIZE=4096
# Single-button board with longpress filter: a "double long-press" is
# physically slower than 500 ms, so widen the confirmation window.
CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS=3000
# T096 has no buzzer. Keep notification audio disabled and omit the buzzer UI page.
CONFIG_ZEPHCORE_UI_BUZZER=n
+21 -5
View File
@@ -489,12 +489,26 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else {
strcpy(reply, "> strict");
}
} else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
} else if (strcmp(config, "tx apc") == 0) {
if (_callbacks->isAPCEnabled()) {
int8_t apc = _callbacks->getAPCReduction();
float margin = _callbacks->getAPCMargin();
int effective = (int)_prefs->tx_power_dbm - (int)apc;
snprintf(reply, CLI_REPLY_SIZE, "> %ddBm (max=%d apc=-%d margin=%.1f target=%d)",
snprintf(reply, CLI_REPLY_SIZE,
"> apc=on effective=%ddBm max=%d reduction=%d margin=%.1f target=%d",
effective, (int)_prefs->tx_power_dbm, (int)apc, (double)margin,
(int)_callbacks->getAPCTargetMargin());
} else {
snprintf(reply, CLI_REPLY_SIZE, "> apc=off max=%ddBm target=%d",
(int)_prefs->tx_power_dbm, (int)_callbacks->getAPCTargetMargin());
}
} else if (strcmp(config, "tx") == 0) {
if (_callbacks->isAPCEnabled()) {
int8_t apc = _callbacks->getAPCReduction();
float margin = _callbacks->getAPCMargin();
int effective = (int)_prefs->tx_power_dbm - (int)apc;
snprintf(reply, CLI_REPLY_SIZE,
"> %ddBm (apc=on max=%d reduction=%d margin=%.1f target=%d)",
effective, (int)_prefs->tx_power_dbm, (int)apc, (double)margin,
(int)_callbacks->getAPCTargetMargin());
} else {
@@ -784,21 +798,23 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(reply, "Error: range 6-30 dB");
}
} else if (memcmp(config, "tx ", 3) == 0) {
if (memcmp(&config[3], "apc", 3) == 0) {
if (strcmp(&config[3], "apc") == 0) {
_prefs->apc_enabled = 1;
_callbacks->setAPCEnabled(true);
savePrefs();
snprintf(reply, CLI_REPLY_SIZE, "OK - tx power=%d dBm (apc=on)",
(int)_prefs->tx_power_dbm);
} else {
int val = atoi(&config[3]);
char *end = nullptr;
long parsed = strtol(&config[3], &end, 10);
int max_tx = 30;
#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM
max_tx = CONFIG_ZEPHCORE_MAX_TX_POWER_DBM;
#endif
if (val < -9 || val > max_tx) {
if (end == &config[3] || *end != '\0' || parsed < -9 || parsed > max_tx) {
snprintf(reply, CLI_REPLY_SIZE, "Error: range -9 to %d dBm, or 'apc'", max_tx);
} else {
int val = (int)parsed;
_prefs->apc_enabled = 0;
_prefs->tx_power_dbm = (int8_t)val;
savePrefs();
+95 -17
View File
@@ -95,7 +95,9 @@ static const enum ui_page active_pages[] = {
UI_PAGE_BLUETOOTH,
UI_PAGE_ADVERT,
UI_PAGE_GPS,
#ifdef CONFIG_ZEPHCORE_UI_BUZZER
UI_PAGE_BUZZER,
#endif
UI_PAGE_LEDS,
UI_PAGE_SENSORS,
UI_PAGE_OFFGRID,
@@ -234,6 +236,16 @@ static void draw_centered(int y, const char *text)
mc_display_text(x, y, text, false);
}
static void draw_color_segments(int y, const char *label,
const char *value, uint16_t value_color)
{
int x = 0;
mc_display_color_text(x, y, label, MC_COLOR_CYAN);
x += (int)strlen(label) * 6;
mc_display_color_text(x, y, value, value_color);
}
/* ========== Page Renderers ========== */
static void render_messages(void)
@@ -294,42 +306,108 @@ static void render_recent(void)
static void render_radio(void)
{
char buf[28];
char buf[32];
int y = CONTENT_Y;
/* Line 1: FQ, SF, BW */
uint32_t freq_mhz = state.lora_freq_hz / 1000000;
uint32_t freq_frac = (state.lora_freq_hz % 1000000 + 500) / 1000;
uint16_t bw_int = state.lora_bw_khz_x10 / 10;
uint16_t bw_frac = state.lora_bw_khz_x10 % 10;
const char *packet_state = state.lora_tx_active ? "TX" :
(state.lora_in_rx ? "RX" :
(state.lora_radio_ready ? "RDY" : "WAIT"));
const char *rx_mode = state.lora_rx_duty_cycle ? "DC" : "CONT";
uint16_t apc_color = state.lora_apc_enabled ? MC_COLOR_GREEN : MC_COLOR_GRAY;
uint16_t warn_color = (state.lora_apc_enabled && state.lora_apc_reduction > 0)
? MC_COLOR_ORANGE : MC_COLOR_GREEN;
if (mc_display_has_color()) {
mc_display_color_fill_rect(0, y - 1, DISP_W, FONT_H + 2, MC_COLOR_BLUE);
snprintf(buf, sizeof(buf), "companion %s/%s", packet_state, rx_mode);
mc_display_color_text(2, y, "RAD", MC_COLOR_YELLOW);
mc_display_color_text(26, y, buf, MC_COLOR_WHITE);
y += LINE_H;
if (bw_frac) {
snprintf(buf, sizeof(buf), "%u.%03u BW%u.%u",
freq_mhz, freq_frac, bw_int, bw_frac);
} else {
snprintf(buf, sizeof(buf), "%u.%03u BW%u",
freq_mhz, freq_frac, bw_int);
}
draw_color_segments(y, "RF ", buf, MC_COLOR_WHITE);
y += LINE_H;
snprintf(buf, sizeof(buf), "SF%u CR%u SW%02X P%u",
state.lora_sf, state.lora_cr, state.lora_sync_word,
state.lora_preamble_len);
draw_color_segments(y, "LoRa ", buf, MC_COLOR_WHITE);
y += LINE_H;
if (state.lora_apc_enabled) {
snprintf(buf, sizeof(buf), "%d/%ddBm APC on",
state.lora_effective_tx_power, state.lora_tx_power);
} else {
snprintf(buf, sizeof(buf), "%ddBm APC off", state.lora_tx_power);
}
draw_color_segments(y, "TX ", buf, apc_color);
y += LINE_H;
snprintf(buf, sizeof(buf), "red %d M%d.%d T%u",
state.lora_apc_reduction,
state.lora_apc_margin_x10 / 10,
abs(state.lora_apc_margin_x10 % 10),
state.lora_apc_target_margin);
draw_color_segments(y, "APC ", buf, warn_color);
y += LINE_H;
snprintf(buf, sizeof(buf), "NF%d R%lu T%lu E%lu",
state.lora_noise_floor,
(unsigned long)state.lora_packets_rx,
(unsigned long)state.lora_packets_tx,
(unsigned long)state.lora_packets_err);
draw_color_segments(y, "PKT ", buf,
state.lora_packets_err ? MC_COLOR_RED : MC_COLOR_WHITE);
return;
}
if (bw_frac) {
snprintf(buf, sizeof(buf), "%u.%03u SF%u BW%u.%u",
freq_mhz, freq_frac, state.lora_sf, bw_int, bw_frac);
snprintf(buf, sizeof(buf), "%u.%03u BW%u.%u",
freq_mhz, freq_frac, bw_int, bw_frac);
} else {
snprintf(buf, sizeof(buf), "%u.%03u SF%u BW%u",
freq_mhz, freq_frac, state.lora_sf, bw_int);
snprintf(buf, sizeof(buf), "%u.%03u BW%u",
freq_mhz, freq_frac, bw_int);
}
mc_display_text(0, y, buf, false);
y += LINE_H;
/* Line 2: CR, TX Power */
snprintf(buf, sizeof(buf), "CR:%u TX:%ddBm", state.lora_cr, state.lora_tx_power);
snprintf(buf, sizeof(buf), "SF%u CR%u SW%02X P%u",
state.lora_sf, state.lora_cr, state.lora_sync_word,
state.lora_preamble_len);
mc_display_text(0, y, buf, false);
y += LINE_H;
/* Line 3: Noise floor */
snprintf(buf, sizeof(buf), "Noise: %ddBm", state.lora_noise_floor);
if (state.lora_apc_enabled) {
snprintf(buf, sizeof(buf), "TX:%d/%ddBm APC:on",
state.lora_effective_tx_power, state.lora_tx_power);
} else {
snprintf(buf, sizeof(buf), "TX:%ddBm APC:off", state.lora_tx_power);
}
mc_display_text(0, y, buf, false);
y += LINE_H;
/* Uptime */
uint32_t up_s = (uint32_t)(k_uptime_get() / 1000);
uint32_t days = up_s / 86400;
uint32_t hours = (up_s % 86400) / 3600;
uint32_t mins = (up_s % 3600) / 60;
snprintf(buf, sizeof(buf), "R%d M%d.%d T%u %s/%s",
state.lora_apc_reduction,
state.lora_apc_margin_x10 / 10,
abs(state.lora_apc_margin_x10 % 10),
state.lora_apc_target_margin, packet_state, rx_mode);
mc_display_text(0, y, buf, false);
y += LINE_H;
snprintf(buf, sizeof(buf), "Up: %ud %uh %um", days, hours, mins);
snprintf(buf, sizeof(buf), "NF%d R%lu T%lu E%lu",
state.lora_noise_floor,
(unsigned long)state.lora_packets_rx,
(unsigned long)state.lora_packets_tx,
(unsigned long)state.lora_packets_err);
mc_display_text(0, y, buf, false);
}
+14
View File
@@ -64,6 +64,20 @@ struct ui_state {
uint8_t lora_cr;
int8_t lora_tx_power;
int16_t lora_noise_floor;
int8_t lora_effective_tx_power;
bool lora_apc_enabled;
int8_t lora_apc_reduction;
int16_t lora_apc_margin_x10;
uint8_t lora_apc_target_margin;
uint8_t lora_sync_word;
uint16_t lora_preamble_len;
bool lora_rx_duty_cycle;
bool lora_radio_ready;
bool lora_in_rx;
bool lora_tx_active;
uint32_t lora_packets_rx;
uint32_t lora_packets_tx;
uint32_t lora_packets_err;
/* Bluetooth page */
bool ble_enabled; /* true = BLE active, false = serial mode */
+31
View File
@@ -887,6 +887,37 @@ void ui_set_radio_params(uint32_t freq_hz, uint8_t sf, uint16_t bw_khz_x10,
s->lora_noise_floor = noise_floor;
}
void ui_set_radio_runtime(int8_t effective_tx_power, bool apc_enabled,
int8_t apc_reduction, int16_t apc_margin_x10,
uint8_t apc_target_margin, uint8_t sync_word,
uint16_t preamble_len, bool rx_duty_cycle,
bool radio_ready, bool in_rx, bool tx_active)
{
struct ui_state *s = get_state();
s->lora_effective_tx_power = effective_tx_power;
s->lora_apc_enabled = apc_enabled;
s->lora_apc_reduction = apc_reduction;
s->lora_apc_margin_x10 = apc_margin_x10;
s->lora_apc_target_margin = apc_target_margin;
s->lora_sync_word = sync_word;
s->lora_preamble_len = preamble_len;
s->lora_rx_duty_cycle = rx_duty_cycle;
s->lora_radio_ready = radio_ready;
s->lora_in_rx = in_rx;
s->lora_tx_active = tx_active;
}
void ui_set_radio_stats(uint32_t packets_rx, uint32_t packets_tx,
uint32_t packets_err)
{
struct ui_state *s = get_state();
s->lora_packets_rx = packets_rx;
s->lora_packets_tx = packets_tx;
s->lora_packets_err = packets_err;
}
void ui_set_gps_data(bool has_fix, uint8_t sats,
int32_t lat_mdeg, int32_t lon_mdeg, int32_t alt_mm)
{
@@ -218,6 +218,24 @@ extern "C" void ui_set_radio_params(
}
}
extern "C" void ui_set_radio_runtime(int8_t effective_tx_power, bool apc_enabled,
int8_t apc_reduction, int16_t apc_margin_x10,
uint8_t apc_target_margin, uint8_t sync_word,
uint16_t preamble_len, bool rx_duty_cycle,
bool radio_ready, bool in_rx, bool tx_active)
{
(void)effective_tx_power; (void)apc_enabled; (void)apc_reduction;
(void)apc_margin_x10; (void)apc_target_margin; (void)sync_word;
(void)preamble_len; (void)rx_duty_cycle; (void)radio_ready;
(void)in_rx; (void)tx_active;
}
extern "C" void ui_set_radio_stats(uint32_t pkt_recv, uint32_t pkt_sent,
uint32_t pkt_errors)
{
ui_notify_radio_stats(pkt_recv, pkt_sent, pkt_errors);
}
extern "C" void ui_notify_radio_stats(uint32_t pkt_recv, uint32_t pkt_sent, uint32_t pkt_errors)
{
if (s_task) {
+8
View File
@@ -970,6 +970,14 @@ static const uint8_t cfb_font_0608[224][6] = {
{ 0x0C, 0x51, 0x50, 0x51, 0x3C, 0x00 },
};
const uint8_t *zephcore_font_6x8_glyph(uint8_t c)
{
if (c < 32) {
c = '?';
}
return cfb_font_0608[c - 32];
}
FONT_ENTRY_DEFINE(cfb_font_6x8,
6, 8,
CFB_FONT_MONO_VPACKED,
+217
View File
@@ -22,6 +22,7 @@
#include <zephyr/drivers/display.h>
#include <zephyr/drivers/regulator.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/byteorder.h>
#include <string.h>
#include <stdio.h>
#include <limits.h>
@@ -54,6 +55,39 @@ static uint16_t disp_height;
static uint8_t font_w;
static uint8_t font_h;
static bool is_epd; /* true for e-paper displays */
static bool has_color; /* true when a raw RGB565 TFT is available */
const uint8_t *zephcore_font_6x8_glyph(uint8_t c);
#define COLOR_TEXT_MAX_CHARS 32
#define COLOR_MAX_OPS 72
#define COLOR_MAX_WIDTH 320
enum color_op_type {
COLOR_OP_TEXT,
COLOR_OP_RECT,
};
struct color_op {
enum color_op_type type;
int16_t x;
int16_t y;
int16_t w;
int16_t h;
uint16_t color;
char text[COLOR_TEXT_MAX_CHARS];
};
static struct color_op color_ops[COLOR_MAX_OPS];
static uint8_t color_op_count;
static uint16_t color_line[COLOR_MAX_WIDTH];
#if DT_NODE_EXISTS(DT_NODELABEL(tft))
static const struct device *color_dev =
DEVICE_DT_GET_OR_NULL(DT_NODELABEL(tft));
#else
static const struct device *color_dev;
#endif
/* Optional symmetric inset (pixels). Shrinks reported width/height and
* offsets all draw primitives so panels with edge artefacts can hide them
@@ -131,6 +165,159 @@ static inline void panel_vdd_enable(void)
}
}
static void color_overlay_probe(void)
{
has_color = false;
if (!color_dev || color_dev == disp_dev || !device_is_ready(color_dev)) {
return;
}
struct display_capabilities caps;
display_get_capabilities(color_dev, &caps);
if ((caps.current_pixel_format == PIXEL_FORMAT_RGB_565 ||
caps.current_pixel_format == PIXEL_FORMAT_RGB_565X ||
(caps.supported_pixel_formats & (PIXEL_FORMAT_RGB_565 | PIXEL_FORMAT_RGB_565X))) &&
!(caps.screen_info & SCREEN_INFO_EPD)) {
has_color = true;
LOG_INF("display: color overlay enabled");
}
}
static bool color_queue(enum color_op_type type, int x, int y, int w, int h,
const char *text, uint16_t color)
{
if (!has_color || color_op_count >= COLOR_MAX_OPS) {
return false;
}
struct color_op *op = &color_ops[color_op_count++];
op->type = type;
op->x = (int16_t)x;
op->y = (int16_t)y;
op->w = (int16_t)w;
op->h = (int16_t)h;
op->color = color;
op->text[0] = '\0';
if (text) {
strncpy(op->text, text, sizeof(op->text) - 1);
op->text[sizeof(op->text) - 1] = '\0';
}
return true;
}
static void color_write_rect_now(int x, int y, int w, int h, uint16_t color)
{
if (!has_color || w <= 0 || h <= 0) {
return;
}
x += DISP_INSET;
y += DISP_INSET;
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
if (x + w > (int)disp_width) {
w = (int)disp_width - x;
}
if (y + h > (int)disp_height) {
h = (int)disp_height - y;
}
if (w <= 0 || h <= 0) {
return;
}
if (w > COLOR_MAX_WIDTH) {
w = COLOR_MAX_WIDTH;
}
uint16_t be = sys_cpu_to_be16(color);
for (int i = 0; i < w; i++) {
color_line[i] = be;
}
const struct display_buffer_descriptor desc = {
.buf_size = (uint32_t)w * 2U,
.width = (uint16_t)w,
.height = 1U,
.pitch = (uint16_t)w,
};
for (int row = 0; row < h; row++) {
display_write(color_dev, (uint16_t)x, (uint16_t)(y + row),
&desc, color_line);
}
}
static void color_write_char_now(int x, int y, uint8_t c, uint16_t color)
{
uint16_t glyph_buf[6 * 8];
const uint8_t *glyph = zephcore_font_6x8_glyph(c);
uint16_t fg = sys_cpu_to_be16(color);
uint16_t bg = sys_cpu_to_be16(MC_COLOR_BLACK);
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 6; col++) {
bool on = (glyph[col] >> row) & 0x01;
glyph_buf[row * 6 + col] = on ? fg : bg;
}
}
const struct display_buffer_descriptor desc = {
.buf_size = sizeof(glyph_buf),
.width = 6,
.height = 8,
.pitch = 6,
};
display_write(color_dev, (uint16_t)(x + DISP_INSET),
(uint16_t)(y + DISP_INSET), &desc, glyph_buf);
}
static void color_write_text_now(int x, int y, const char *text, uint16_t color)
{
if (!has_color || !text) {
return;
}
for (const char *p = text; *p; p++, x += 6) {
uint8_t c = (uint8_t)*p;
if (c < 32) {
c = '?';
}
if (x + 6 > (int)mc_display_width() || y + 8 > (int)mc_display_height()) {
break;
}
color_write_char_now(x, y, c, color);
}
}
static void color_flush_ops(void)
{
if (!has_color) {
color_op_count = 0;
return;
}
for (uint8_t i = 0; i < color_op_count; i++) {
struct color_op *op = &color_ops[i];
if (op->type == COLOR_OP_RECT) {
color_write_rect_now(op->x, op->y, op->w, op->h, op->color);
} else {
color_write_text_now(op->x, op->y, op->text, op->color);
}
}
color_op_count = 0;
}
/* Auto-off work */
static struct k_work_delayable auto_off_work;
@@ -228,6 +415,7 @@ int mc_display_init(void)
LOG_INF("display: %ux%u%s", disp_width, disp_height,
is_epd ? " (e-paper)" : "");
color_overlay_probe();
/* OLED: blank before CFB init so stale VRAM isn't visible while we
* build the first frame. EPD: driver init already performed a clean
@@ -395,12 +583,18 @@ bool mc_display_is_epd(void)
return is_epd;
}
bool mc_display_has_color(void)
{
return has_color;
}
void mc_display_clear(void)
{
if (!disp_initialized) {
return;
}
color_op_count = 0;
if (is_epd) {
epd_frame_hash = 2166136261u;
}
@@ -430,6 +624,17 @@ void mc_display_text(int x, int y, const char *text, bool invert)
}
}
void mc_display_color_text(int x, int y, const char *text, uint16_t color)
{
if (!disp_initialized || !text) {
return;
}
if (!color_queue(COLOR_OP_TEXT, x, y, 0, 0, text, color)) {
mc_display_text(x, y, text, false);
}
}
void mc_display_fill_rect(int x, int y, int w, int h)
{
if (!disp_initialized) {
@@ -452,6 +657,17 @@ void mc_display_fill_rect(int x, int y, int w, int h)
}
}
void mc_display_color_fill_rect(int x, int y, int w, int h, uint16_t color)
{
if (!disp_initialized) {
return;
}
if (!color_queue(COLOR_OP_RECT, x, y, w, h, NULL, color)) {
mc_display_fill_rect(x, y, w, h);
}
}
void mc_display_hline(int x, int y, int w)
{
if (!disp_initialized) {
@@ -564,6 +780,7 @@ void mc_display_finalize(void)
}
cfb_framebuffer_finalize(disp_dev);
color_flush_ops();
if (is_epd) {
epd_last_frame_hash = epd_frame_hash;
+31
View File
@@ -83,6 +83,12 @@ bool mc_display_is_on(void);
*/
bool mc_display_is_epd(void);
/**
* @return true when a raw RGB565-capable color panel is available for
* optional color overlays. Monochrome displays return false.
*/
bool mc_display_has_color(void);
/**
* Clear the framebuffer (fill with black).
* Call before rendering a new frame.
@@ -99,6 +105,25 @@ void mc_display_clear(void);
*/
void mc_display_text(int x, int y, const char *text, bool invert);
/* Common RGB565 colors for optional color-capable pages. */
#define MC_COLOR_BLACK 0x0000
#define MC_COLOR_WHITE 0xffff
#define MC_COLOR_GREEN 0x07e0
#define MC_COLOR_CYAN 0x07ff
#define MC_COLOR_YELLOW 0xffe0
#define MC_COLOR_ORANGE 0xfd20
#define MC_COLOR_RED 0xf800
#define MC_COLOR_BLUE 0x001f
#define MC_COLOR_GRAY 0x8410
/**
* Draw text using RGB565 color when supported. On non-color displays this
* falls back to mc_display_text(..., invert=false).
*
* Color overlays are flushed after the normal CFB frame in mc_display_finalize().
*/
void mc_display_color_text(int x, int y, const char *text, uint16_t color);
/**
* Draw a filled rectangle.
*
@@ -109,6 +134,12 @@ void mc_display_text(int x, int y, const char *text, bool invert);
*/
void mc_display_fill_rect(int x, int y, int w, int h);
/**
* Draw a filled rectangle using RGB565 color when supported. On non-color
* displays this falls back to mc_display_fill_rect().
*/
void mc_display_color_fill_rect(int x, int y, int w, int h, uint16_t color);
/**
* Draw a horizontal line.
*/
+19
View File
@@ -49,6 +49,25 @@ WEAK void ui_set_radio_params(uint32_t freq_hz, uint8_t sf,
ARG_UNUSED(cr); ARG_UNUSED(tx_power); ARG_UNUSED(noise_floor);
}
WEAK void ui_set_radio_runtime(int8_t effective_tx_power, bool apc_enabled,
int8_t apc_reduction, int16_t apc_margin_x10,
uint8_t apc_target_margin, uint8_t sync_word,
uint16_t preamble_len, bool rx_duty_cycle,
bool radio_ready, bool in_rx, bool tx_active)
{
ARG_UNUSED(effective_tx_power); ARG_UNUSED(apc_enabled);
ARG_UNUSED(apc_reduction); ARG_UNUSED(apc_margin_x10);
ARG_UNUSED(apc_target_margin); ARG_UNUSED(sync_word);
ARG_UNUSED(preamble_len); ARG_UNUSED(rx_duty_cycle);
ARG_UNUSED(radio_ready); ARG_UNUSED(in_rx); ARG_UNUSED(tx_active);
}
WEAK void ui_set_radio_stats(uint32_t packets_rx, uint32_t packets_tx,
uint32_t packets_err)
{
ARG_UNUSED(packets_rx); ARG_UNUSED(packets_tx); ARG_UNUSED(packets_err);
}
WEAK void ui_set_gps_data(bool has_fix, uint8_t sats,
int32_t lat_mdeg, int32_t lon_mdeg, int32_t alt_mm)
{
+31 -6
View File
@@ -315,13 +315,38 @@ extern "C" void mesh_housekeeping_ui_refresh(void)
ui_set_clock(s_rtc_clock->getCurrentTime());
ui_set_radio_params(
(uint32_t)(s_mesh->prefs.freq * 1000000.0f + 0.5f),
s_mesh->prefs.sf,
(uint16_t)(s_mesh->prefs.bw * 10.0f + 0.5f),
s_mesh->prefs.cr,
s_mesh->prefs.tx_power_dbm,
s_lora_radio->getActiveFrequencyHz(),
s_lora_radio->getActiveSpreadingFactor(),
s_lora_radio->getActiveBandwidthKHzX10(),
s_lora_radio->getActiveCodingRate(),
s_lora_radio->getConfiguredTxPower(),
s_lora_radio->getNoiseFloor());
ui_notify_radio_stats(
{
bool apc_enabled = false;
int8_t apc_reduction = 0;
int16_t apc_margin_x10 = 0;
uint8_t apc_target = s_mesh->prefs.apc_margin;
#ifdef CONFIG_ZEPHCORE_APC
apc_enabled = s_mesh->isAPCEnabled();
apc_reduction = s_mesh->getAPCReduction();
apc_margin_x10 = (int16_t)(s_mesh->getAPCMargin() * 10.0f);
apc_target = s_mesh->getAPCTargetMargin();
#endif
ui_set_radio_runtime(
s_lora_radio->getEffectiveTxPower(),
apc_enabled,
apc_reduction,
apc_margin_x10,
apc_target,
s_lora_radio->getActiveSyncWord(),
s_lora_radio->getActivePreambleLength(),
s_lora_radio->isRxDutyCycleEnabled(),
s_lora_radio->isRadioReady(),
s_lora_radio->isInRecvMode(),
s_lora_radio->isTxActive());
}
ui_set_radio_stats(
s_lora_radio->getPacketsRecv(),
s_lora_radio->getPacketsSent(),
s_lora_radio->getPacketsRecvErrors());
+15
View File
@@ -84,6 +84,21 @@ void ui_set_ble_status(bool connected, const char *name);
void ui_set_radio_params(uint32_t freq_hz, uint8_t sf, uint16_t bw_khz_x10,
uint8_t cr, int8_t tx_power, int16_t noise_floor);
/**
* Update extended live radio/APC details for display.
*/
void ui_set_radio_runtime(int8_t effective_tx_power, bool apc_enabled,
int8_t apc_reduction, int16_t apc_margin_x10,
uint8_t apc_target_margin, uint8_t sync_word,
uint16_t preamble_len, bool rx_duty_cycle,
bool radio_ready, bool in_rx, bool tx_active);
/**
* Update radio packet counters for display.
*/
void ui_set_radio_stats(uint32_t packets_rx, uint32_t packets_tx,
uint32_t packets_err);
/**
* Update GPS data for display.
*/
+77 -5
View File
@@ -678,6 +678,27 @@ public:
LOG_INF("TX power %d dBm requested (reboot to apply)", power_dbm);
}
#ifdef CONFIG_ZEPHCORE_APC
int8_t getAPCReduction() const override {
return companion_mesh.getAPCReduction();
}
float getAPCMargin() const override {
return companion_mesh.getAPCMargin();
}
bool isAPCEnabled() const override {
return companion_mesh.isAPCEnabled();
}
void setAPCEnabled(bool en) override {
companion_mesh.setAPCEnabled(en);
}
uint8_t getAPCTargetMargin() const override {
return companion_mesh.getAPCTargetMargin();
}
void setAPCTargetMargin(uint8_t margin_db) override {
companion_mesh.setAPCTargetMargin(margin_db);
}
#endif
mesh::LocalIdentity& getSelfId() override { return companion_mesh.self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override {
@@ -1107,12 +1128,39 @@ int main(void)
/* Push initial state to UI display */
ui_set_node_name(companion_mesh.prefs.node_name);
ui_set_radio_params(
(uint32_t)(companion_mesh.prefs.freq * 1000000.0f + 0.5f),
companion_mesh.prefs.sf,
(uint16_t)(companion_mesh.prefs.bw * 10.0f + 0.5f),
companion_mesh.prefs.cr,
companion_mesh.prefs.tx_power_dbm,
lora_radio.getActiveFrequencyHz(),
lora_radio.getActiveSpreadingFactor(),
lora_radio.getActiveBandwidthKHzX10(),
lora_radio.getActiveCodingRate(),
lora_radio.getConfiguredTxPower(),
lora_radio.getNoiseFloor());
#ifdef CONFIG_ZEPHCORE_APC
ui_set_radio_runtime(
lora_radio.getEffectiveTxPower(),
companion_mesh.isAPCEnabled(),
companion_mesh.getAPCReduction(),
(int16_t)(companion_mesh.getAPCMargin() * 10.0f),
companion_mesh.getAPCTargetMargin(),
lora_radio.getActiveSyncWord(),
lora_radio.getActivePreambleLength(),
lora_radio.isRxDutyCycleEnabled(),
lora_radio.isRadioReady(),
lora_radio.isInRecvMode(),
lora_radio.isTxActive());
#else
ui_set_radio_runtime(
lora_radio.getEffectiveTxPower(),
false, 0, 0, companion_mesh.prefs.apc_margin,
lora_radio.getActiveSyncWord(),
lora_radio.getActivePreambleLength(),
lora_radio.isRxDutyCycleEnabled(),
lora_radio.isRadioReady(),
lora_radio.isInRecvMode(),
lora_radio.isTxActive());
#endif
ui_set_radio_stats(lora_radio.getPacketsRecv(),
lora_radio.getPacketsSent(),
lora_radio.getPacketsRecvErrors());
ui_set_battery(zephyr_board.getBattMilliVolts(), 0);
ui_set_gps_available(gps_is_available());
ui_set_gps_enabled(companion_mesh.prefs.gps_enabled != 0);
@@ -1163,6 +1211,30 @@ int main(void)
/* Apply RX boost and duty cycle from prefs */
lora_radio.setRxBoost(companion_mesh.prefs.rx_boost != 0);
lora_radio.enableRxDutyCycle(companion_mesh.prefs.rx_duty_cycle != 0);
#ifdef CONFIG_ZEPHCORE_APC
ui_set_radio_runtime(
lora_radio.getEffectiveTxPower(),
companion_mesh.isAPCEnabled(),
companion_mesh.getAPCReduction(),
(int16_t)(companion_mesh.getAPCMargin() * 10.0f),
companion_mesh.getAPCTargetMargin(),
lora_radio.getActiveSyncWord(),
lora_radio.getActivePreambleLength(),
lora_radio.isRxDutyCycleEnabled(),
lora_radio.isRadioReady(),
lora_radio.isInRecvMode(),
lora_radio.isTxActive());
#else
ui_set_radio_runtime(
lora_radio.getEffectiveTxPower(),
false, 0, 0, companion_mesh.prefs.apc_margin,
lora_radio.getActiveSyncWord(),
lora_radio.getActivePreambleLength(),
lora_radio.isRxDutyCycleEnabled(),
lora_radio.isRadioReady(),
lora_radio.isInRecvMode(),
lora_radio.isTxActive());
#endif
/* Restore runtime ADC multiplier override (0 = keep DT default) */
zephyr_board.setAdcMultiplier(companion_mesh.prefs.adc_multiplier);