65.9K
CodeProject 正在变化。 阅读更多。
Home

EspMon 重启

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (6投票s)

2023 年 6 月 12 日

MIT

12分钟阅读

viewsIcon

16090

downloadIcon

241

使用 ESP32 监控您的 PC 的 CPU 和 GPU。

 

ESPMon UIX

引言

我曾制作过一个 此项目的先前版本,使用的是 LVGL 和 T-Display。我认为值得重新审视,因为我改进了很多,并且将它改为使用我的 UIXGFX 库,而不是 LVGL。为了能在各种不同的显示器上工作,该项目拥有可扩展的 UI。

必备组件

您需要以下一种或多种 ESP32 设备

  • Lilygo T-Display S3*
  • Lilygo TTGO T-Display T1 (Github 上有 ESP-IDF 支持***)
  • M5 Stack Fire
  • M5 Stack Core2
  • M5 Stack S3 Atom*
  • Makerfabs ESP Display S3 Parallel 带触摸屏
  • Makerfabs ESP Display S3 4 英寸带触摸屏
  • Makerfabs ESP Display S3 4.3 英寸带触摸屏
  • Espressif ESP_WROVER_KIT 4.1
  • Lilygo T-QT (和 Pro)*
  • HelTec WiFi Kit V2** (Github 上有 ESP-IDF 支持***)
  • Wio Terminal (仅限 Github***)
  • Lilygo T5 4.7" 电子纸小工具 (仅限 Github***)

* 在使用这些设备与 PC 应用程序通信时,我遇到了一些挂起的问题,并将 bug 追溯到了 2.0.7 版本之前的 Arduino HAL。无论如何,问题比我的代码更底层,但在 2.0.7 框架版本中已修复。请确保将您的 Platform IO Espressif 平台更新到最新版本。

** 此屏幕非常小且为单色,这会限制 UI 的可扩展性,并影响可读性。有可能改进小型屏幕的显示效果,但我认为不值得使代码复杂化。该设备更多的是为了好玩。

*** Github 包含更高级的代码,包括 ESP-IDF 支持,以及对 SAMD51 Wio Terminal 的支持。我不想在这里包含它,因为它更复杂,并且会使这里的核心代码解释混乱。

通过对 ESP32 LCD 的 lcd_config.hlcd_init.hplatformio.ini 进行一些修改,以及对其他平台/显示器的 display.hpp/cpp 进行修改,可以添加更多设备。

您需要安装 PlatformIO。

您需要一台 Windows PC。

您需要 Visual Studio。

如何使用这个项目

将设备插入 PC。如果设备有多个 USB 端口,请将其插入连接到串行 UART USB 桥接器的端口,而不是原生 USB 端口(仅限 ESP32-S3)。

接下来启动 PC 应用程序 - 系统会要求您批准以提升的权限运行该应用程序。

从列表框中选择您要监视的 COM 端口,然后勾选“Started”。您显示器上的显示图标应从断开连接的图标变为监视器屏幕,并且监视将开始。

理解这个项目

C# 应用程序使用 OpenHardwareMonitor(已针对 Raptor Lake 处理器打补丁的版本包含在此下载包中。它每秒查询 OHWM 十次以获取使用率、温度和 tjmax 值(仅限 Intel)。

一旦获取到这些数据,它会存储起来,并在选中“Started”的情况下将其发送到每个选定的串行端口。

通过串行接收后,数据存储在用于使用率和温度历史图表的循环缓冲区中。然后,UIX 会在 UI 中发生变化的任何部分(即条形图、图表和温度读数)被无效化后进行更新。为了使图表工作,如果循环缓冲区已满,则在添加新数据之前会移除最左边/最旧的数据。

温度读数值得额外解释,因为它是一个渐变。它使用 HSV 颜色模型,随着温度升高,在绿色、黄色和红色之间过渡。这适用于图表和条形图。

UI 本身围绕 CPU 和 GPU 标签进行缩放。对于上半部分(CPU),首先创建标签,然后是围绕它的组件。对于下半部分,所有位置都复制并偏移屏幕高度的一半。字体被缩放到屏幕高度的十分之一。

编写这堆乱七八糟的代码

我们开始吧,好吗?首先,我们将介绍固件。

固件

main.cpp

Main 负责设置设备、监听串行数据以及根据需要更新 UI。

第一部分是样板代码,只包含必要的文件。这里唯一奇怪的地方是 #define LCD_IMPLEMENTATION。它的作用是告诉 lcd_init.h 将其代码导入此 cpp 文件。这必须在项目中的恰好一个 cpp 文件中完成。

#include <Arduino.h>
// required to import the actual definitions
// for lcd_init.h
#define LCD_IMPLEMENTATION
#include <lcd_init.h>
#include <uix.hpp>
using namespace gfx;
using namespace uix;
#include <ui.hpp>
#include <interface.hpp>
#ifdef M5STACK_CORE2
#include <m5core2_power.hpp>
#endif

接下来的部分是全局变量。我们这里只有几个,它们提供了存储温度标签中动态内容的内存以及一个计时器变量,用于检测设备何时不再接收串行数据。计时器变量只是一个 millis() 时间戳。对于 M5 Stack Core2,我们声明了一个电源库类的实例。

// label string data
static char cpu_sz[32];
static char gpu_sz[32];

// signal timer for disconnection detection
static uint32_t timeout_ts = 0;

#ifdef M5STACK_CORE2
m5core2_power power;
#endif

接下来是两个回调函数。

第一个是由 lcd_init.h 基础设施(在使用 RGB 面板接口时)调用的。它通知 UIX DMA 传输已完成。

第二个是由 UIX 调用,用于发送部分屏幕数据的位图到显示器。它调用 lcd_init.h 基础设施,以便异步地将位图发送到 LCD 面板。

// only needed if not RGB interface screen
#ifndef LCD_PIN_NUM_VSYNC
static bool lcd_flush_ready(esp_lcd_panel_io_handle_t panel_io, 
                            esp_lcd_panel_io_event_data_t* edata,
                            void* user_ctx) {
    main_screen.set_flush_complete();
    return true;
}
#endif

static void uix_flush(const rect16& bounds, 
                    const void* bmp, 
                    void* state) {
    lcd_panel_draw_bitmap(bounds.x1, 
                        bounds.y1, 
                        bounds.x2, 
                        bounds.y2,
                        (void*) bmp);
    // if RGB, no DMA, so we are done once the above completes
#ifdef LCD_PIN_NUM_VSYNC
    main_screen.set_flush_complete();
#endif
}

现在我们进入 setup(),它遵循 Arduino 的传统来初始化设备。我们在这里完成。一些设备具有用于电池供电的电源使能引脚。我们根据需要进行设置。

void setup() {
    Serial.begin(115200);
    // enable the power pins, as necessary
#ifdef T_DISPLAY_S3
    pinMode(15, OUTPUT); 
    digitalWrite(15, HIGH);
#elif defined(S3_T_QT)
    pinMode(4, OUTPUT); 
    digitalWrite(4, HIGH);
#endif
#ifdef M5STACK_CORE2
    power.initialize();
#endif

    // RGB interface LCD init is slightly different
#ifdef LCD_PIN_NUM_VSYNC
    lcd_panel_init();
#else
    lcd_panel_init(lcd_buffer_size,lcd_flush_ready);
#endif
    // initialize the main screen (ui.cpp)
    main_screen_init(uix_flush);

}

进入 loop(),我们将分部分进行介绍。

第一部分使我们能够检测到一秒钟没有串行数据的情况,在这种情况下,它会显示断开连接的“屏幕”。它不是真正的屏幕,而是一个覆盖 CPU/GPU 显示的标签,然后是一个带有断开连接图标的 SVG 图像。我们本可以为此创建一个单独的屏幕 - 它甚至会更有效 - 但它会使代码复杂化,而且实际上并不需要。

// timeout for disconnection detection (1 second)
if(timeout_ts!=0 && millis()>timeout_ts+1000) {
    timeout_ts = 0;
    disconnected_label.visible(true);
    disconnected_svg.visible(true);
}

现在我们更新 main_screen,这让 UIX 有机会在必要时更新显示。

// update the UI
main_screen.update();

接下来,我们检查传入的串行数据。如果找到数据,我们会重置断开连接的超时,并隐藏相关控件。

// listen for incoming serial
int i = Serial.read();
float tmp;
if(i>-1) { // if data received...
    // reset the disconnect timeout
    timeout_ts = millis(); 
    disconnected_label.visible(false);
    disconnected_svg.visible(false);
...

现在我们检查一个字节命令标识符,它应该是 1,后跟一个 read_status 结构,其中包含多个 32 位整数,如 interface.hpp 中定义的。我们按命令 ID 进行切换的原因是,如果将来需要,我们可以添加更多消息。它也是我实现二进制串行通信的标准方式。

switch(i) {
    case read_status_t::command: {
        read_status_t data;
        if(sizeof(data)==Serial.readBytes((uint8_t*)&data,sizeof(data))) {
...

如果上面的 if 语句评估为 true,我们将处理这些值并更新 UI 数据,根据需要强制重绘。

// update the CPU graph buffer (usage)
if (cpu_buffers[0].full()) {
    cpu_buffers[0].get(&tmp);
}
cpu_buffers[0].put(data.cpu_usage/100.0f);
// update the bar and label values (usage)
cpu_values[0]=data.cpu_usage/100.0f;
// update the CPU graph buffer (temperature)
if (cpu_buffers[1].full()) {
    cpu_buffers[1].get(&tmp);
}
cpu_buffers[1].put(data.cpu_temp/(float)data.cpu_temp_max);
if(data.cpu_temp>cpu_max_temp) {
    cpu_max_temp = data.cpu_temp;
}
// update the bar and label values (temperature)
cpu_values[1]=data.cpu_temp/(float)data.cpu_temp_max;
// force a redraw of the CPU bar and graph
cpu_graph.invalidate();
cpu_bar.invalidate();
// update CPU the label (temperature)
sprintf(cpu_sz,"%dC",data.cpu_temp);
cpu_temp_label.text(cpu_sz);
// update the GPU graph buffer (usage)
if (gpu_buffers[0].full()) {
    gpu_buffers[0].get(&tmp);
}
gpu_buffers[0].put(data.gpu_usage/100.0f);
// update the bar and label values (usage)
gpu_values[0] = data.gpu_usage/100.0f;
// update the GPU graph buffer (temperature)
if (gpu_buffers[1].full()) {
    gpu_buffers[1].get(&tmp);
}
gpu_buffers[1].put(data.gpu_temp/(float)data.gpu_temp_max);
if(data.gpu_temp>gpu_max_temp) {
    gpu_max_temp = data.gpu_temp;
}
// update the bar and label values (temperature)
gpu_values[1] = data.gpu_temp/(float)data.gpu_temp_max;
// force a redraw of the GPU bar and graph
gpu_graph.invalidate();
gpu_bar.invalidate();
// update GPU the label (temperature)
sprintf(gpu_sz,"%dC",data.gpu_temp);
gpu_temp_label.text(gpu_sz);

如果数据不是我们期望的,那么我们就一直读取,直到没有其他数据可用。

// eat bad data
while(-1!=Serial.read());

ui.hpp

UI 头文件仅声明与用户界面相关的共享类型声明和全局变量。它相当直接。screen_ex<> 有点复杂,但大多数时候您可以使用更简单的 screen<>

#pragma once
#include <lcd_config.h>
#include <uix.hpp>
#include <circular_buffer.hpp>
// declare the types for our controls and other things
using screen_t = uix::screen_ex<LCD_WIDTH,LCD_HEIGHT,
                            LCD_FRAME_ADAPTER,LCD_X_ALIGN,LCD_Y_ALIGN>;

using label_t = uix::label<typename screen_t::control_surface_type>;
using svg_box_t = uix::svg_box<typename screen_t::control_surface_type>;
using canvas_t = uix::canvas<typename screen_t::control_surface_type>;
// X11 colors (used for screen)
using color_t = gfx::color<typename screen_t::pixel_type>;
// RGBA8888 X11 colors (used for controls)
using color32_t = gfx::color<gfx::rgba_pixel<32>>;
// circular buffer for graphs
using buffer_t = circular_buffer<float,100>;

// the buffers hold the graph data for the CPU
extern buffer_t cpu_buffers[];
// the array holds the bar/label data for the CPU
extern float cpu_values[];
// the max temperature received for the CPU
extern int cpu_max_temp;
// the colors for the CPU bar and graph
extern gfx::rgba_pixel<32> cpu_colors[];
// the buffers hold the graph data for the GPU
extern buffer_t gpu_buffers[];
// the array holds the bar/label data for the GPU
extern float gpu_values[];
// the max temperature received for the GPU
extern int gpu_max_temp;
// the colors for the GPU bar and graph
extern gfx::rgba_pixel<32> gpu_colors[];

// for most screens, we declare two 32kB buffers which
// we swap out for DMA. For RGB screens, DMA is not
// used so we put 64kB in one buffer
#ifndef LCD_PIN_NUM_VSYNC
constexpr static const int lcd_buffer_size = 32 * 1024;
#else
constexpr static const int lcd_buffer_size = 64 * 1024;
#endif

// the screen that holds the controls
extern screen_t main_screen;

// the controls for the CPU
extern label_t cpu_label;
extern label_t cpu_temp_label;
extern canvas_t cpu_bar;
extern canvas_t cpu_graph;

// the controls for the GPU
extern label_t gpu_label;
extern label_t gpu_temp_label;
extern canvas_t gpu_bar;
extern canvas_t gpu_graph;

// the controls for the disconnected "screen"
extern label_t disconnected_label;
extern svg_box_t disconnected_svg;

extern void main_screen_init(screen_t::on_flush_callback_type flush_callback, 
                            void* flush_callback_state = nullptr);

ui.cpp

UI 实现包含我们应用程序的许多核心内容,但尽管如此,由于 UIX,它被精简且相对直接。我们不会按顺序介绍它,因为如果我们将文件按乱序探索,它会更有意义。

我们将从全局变量定义开始,这些定义填充了 UI 头文件中的声明,并引入了一些仅在此实现文件中私有的变量。这一切都很直接且已注释。

// define the declarations from the header
buffer_t cpu_buffers[2];
rgba_pixel<32> cpu_colors[] = {color32_t::blue, rgba_pixel<32>()};
float cpu_values[] = {0.0f, 0.0f};
int cpu_max_temp = 1;
buffer_t gpu_buffers[2];
rgba_pixel<32> gpu_colors[] = {color32_t::blue, rgba_pixel<32>()};
float gpu_values[] = {0.0f, 0.0f};
int gpu_max_temp = 1;

// define our transfer buffer(s) and initialize
// the main screen with it/them.
// for RGB interface screens we only use one
// because there is no DMA
static uint8_t lcd_buffer1[lcd_buffer_size];
#ifndef LCD_PIN_NUM_VSYNC
static uint8_t lcd_buffer2[lcd_buffer_size];
screen_t main_screen(lcd_buffer_size, lcd_buffer1, lcd_buffer2);
#else
screen_t main_screen(lcd_buffer_size, lcd_buffer1, nullptr);
#endif

// define our CPU controls and state
label_t cpu_label(main_screen);
label_t cpu_temp_label(main_screen);
canvas_t cpu_bar(main_screen);
canvas_t cpu_graph(main_screen);
static bar_info_t cpu_bar_state;
static graph_info_t cpu_graph_state;

// define our GPU controls and state
label_t gpu_label(main_screen);
canvas_t gpu_bar(main_screen);
label_t gpu_temp_label(main_screen);
canvas_t gpu_graph(main_screen);
static bar_info_t gpu_bar_state;
static graph_info_t gpu_graph_state;

// define our disconnected controls
svg_box_t disconnected_svg(main_screen);
label_t disconnected_label(main_screen);

接下来是 main_screen_init(),它布局并设置所有控件。布局会根据屏幕大小进行缩放。

// declare a transparent pixel/color
rgba_pixel<32> transparent(0, 0, 0, 0);
// screen is black
main_screen.background_color(color_t::black);
// set the flush callback
main_screen.on_flush_callback(flush_callback, flush_callback_state);

// declare the first label. Everything else is based on this.
// to do so, we measure the size of the text (@ 1/10th of 
// height of the screen) and bound the label based on that
cpu_label.text("CPU");
cpu_label.text_line_height(main_screen.dimensions().height / 10);
cpu_label.bounds(text_font.measure_text(ssize16::max(), 
                            spoint16::zero(), 
                            cpu_label.text(), 
                            text_font.scale(cpu_label.text_line_height()))
                                .bounds().offset(5, 5).inflate(8, 4));
// set the design properties
cpu_label.text_color(color32_t::white);
cpu_label.background_color(transparent);
cpu_label.border_color(transparent);
cpu_label.text_justify(uix_justify::bottom_right);
cpu_label.text_open_font(&text_font);
// register the control with the screen
main_screen.register_control(cpu_label);

// the temp label is right below the first label
cpu_temp_label.bounds(cpu_label.bounds()
                        .offset(0, cpu_label.text_line_height() + 1));
cpu_temp_label.text_color(color32_t::white);
cpu_temp_label.background_color(transparent);
cpu_temp_label.border_color(transparent);
cpu_temp_label.text("0C");
cpu_temp_label.text_justify(uix_justify::bottom_right);
cpu_temp_label.text_open_font(&text_font);
cpu_temp_label.text_line_height(cpu_label.text_line_height());
main_screen.register_control(cpu_temp_label);

// the bars are to the right of the label
cpu_bar.bounds({int16_t(cpu_label.bounds().x2 + 5), 
                cpu_label.bounds().y1, 
                int16_t(main_screen.dimensions().width - 5), 
                cpu_label.bounds().y2});
cpu_bar_state.size = 2;
cpu_bar_state.colors = cpu_colors;
cpu_bar_state.values = cpu_values;
cpu_bar.on_paint(draw_bar, &cpu_bar_state);
main_screen.register_control(cpu_bar);

// the graph is below the above items.
cpu_graph.bounds({cpu_bar.bounds().x1, 
                    int16_t(cpu_label.bounds().y2 + 5), 
                    cpu_bar.bounds().x2, 
                    int16_t(main_screen.dimensions().height / 
                                2 - 5)});
cpu_graph_state.size = 2;
cpu_graph_state.colors = cpu_colors;
cpu_graph_state.buffers = cpu_buffers;
cpu_graph.on_paint(draw_graph, &cpu_graph_state);
main_screen.register_control(cpu_graph);

// the GPU label is offset from the CPU
// label by half the height of the screen
gpu_label.bounds(cpu_label.bounds().offset(0, main_screen.dimensions().height / 2));
gpu_label.text_color(color32_t::white);
gpu_label.border_color(transparent);
gpu_label.background_color(transparent);
gpu_label.text("GPU");
gpu_label.text_justify(uix_justify::bottom_right);
gpu_label.text_open_font(&text_font);
gpu_label.text_line_height(cpu_label.text_line_height());
main_screen.register_control(gpu_label);

// lay out the rest of the controls the 
// same as was done with the CPU
gpu_temp_label.bounds(gpu_label.bounds().offset(0, gpu_label.text_line_height() + 1));
gpu_temp_label.text_color(color32_t::white);
gpu_temp_label.background_color(transparent);
gpu_temp_label.border_color(transparent);
gpu_temp_label.text("0C");
gpu_temp_label.text_justify(uix_justify::bottom_right);
gpu_temp_label.text_open_font(&text_font);
gpu_temp_label.text_line_height(cpu_label.text_line_height());
main_screen.register_control(gpu_temp_label);

gpu_bar.bounds({int16_t(gpu_label.bounds().x2 + 5), 
                gpu_label.bounds().y1, 
                int16_t(main_screen.dimensions().width - 5), 
                gpu_label.bounds().y2});
gpu_bar_state.size = 2;
gpu_bar_state.colors = gpu_colors;
gpu_bar_state.values = gpu_values;
gpu_bar.on_paint(draw_bar, &gpu_bar_state);
main_screen.register_control(gpu_bar);

gpu_graph.bounds(cpu_graph.bounds()
                    .offset(0, main_screen.dimensions().height / 2));
gpu_graph_state.size = 2;
gpu_graph_state.colors = gpu_colors;
gpu_graph_state.buffers = gpu_buffers;
gpu_graph.on_paint(draw_graph, &gpu_graph_state);
main_screen.register_control(gpu_graph);

disconnected_label.bounds(main_screen.bounds());
disconnected_label.background_color(color32_t::white);
disconnected_label.border_color(color32_t::white);
main_screen.register_control(disconnected_label);
// here we center and scale the SVG control based on
// the size of the screen, clamped to a max of 128x128
float sscale;
if(main_screen.dimensions().width<128 || main_screen.dimensions().height<128) {
    sscale = disconnected_icon.scale(main_screen.dimensions());
} else {
    sscale = disconnected_icon.scale(size16(128,128));
}
disconnected_svg.bounds(srect16(0,
                                0,
                                disconnected_icon.dimensions().width*sscale-1,
                                disconnected_icon.dimensions().height*sscale-1)
                                    .center(main_screen.bounds()));
disconnected_svg.doc(&disconnected_icon);
main_screen.register_control(disconnected_svg);

接下来,我们将介绍用于我们的画布控件的 on_paint() 回调的绘制状态。这些基本上将回调与我们用于缓冲区和值的全局数据“链接”起来。

// used for the draw routines
// the state data for the bars
typedef struct bar_info {
    size_t size;
    float* values;
    rgba_pixel<32>* colors;
} bar_info_t;
// the state data for the graphs
typedef struct graph_info {
    size_t size;
    buffer_t* buffers;
    rgba_pixel<32>* colors;
} graph_info_t;

接下来是绘制条形的例程。由于渐变,这有点复杂,但否则相对直接。基本上,我们将条形空间垂直划分,根据控件的总大小除以条形数量。然后,我们继续以指定的颜色绘制背景,但略带透明,以使其相对于填充部分变暗,填充部分是我们稍后绘制的。使用渐变,我们通过使用 HSV 颜色模型在绿色、黄色和红色之间过渡来玩了一个小技巧,但即使有这个捷径,仍然需要一些努力。

// reconstitute our state info
const bar_info_t& inf = *(bar_info_t*)state;
// get the height of each bar
int h = destination.dimensions().height / inf.size;
int y = 0;
for (size_t i = 0; i < inf.size; ++i) {
    // the current value to graph
    float v = inf.values[i];
    rgba_pixel<32> col = inf.colors[i];
    rgba_pixel<32> bcol = col;
    // if the color is the default (black), then we create a gradient
    // using the HSV color model
    if (col == rgba_pixel<32>()) {
        // two reference points for the ends of the graph
        hsva_pixel<32> px = color<hsva_pixel<32>>::red;
        hsva_pixel<32> px2 = color<hsva_pixel<32>>::green;
        auto h1 = px.channel<channel_name::H>();
        auto h2 = px2.channel<channel_name::H>();
        // adjust so we don't overshoot
        h2 -= 32;
        // the actual range we're drawing
        auto range = abs(h2 - h1) + 1;
        // the width of each gradient segment
        int w = (int)ceilf(destination.dimensions().width / 
                            (float)range) + 1;
        // the step of each segment - default 1
        int s = 1;
        // if the gradient is larger than the control
        if (destination.dimensions().width < range) {
            // change the segment to width 1
            w = 1;
            // and make its step larger
            s = range / (float)destination.dimensions().width;
        }
        int x = 0;
        // c is the current color offset
        // it increases by s (step)
        int c = 0;
        // for each color in the range
        for (auto j = 0; j < range; ++j) {
            // adjust the H value (inverted and offset)
            px.channel<channel_name::H>(range - c - 1 + h1);
            // create the rect for our segment
            rect16 r(x, y, x + w, y + h);
            // if we're drawing the filled part
            // it's fully opaque
            // otherwise it's semi-transparent
            if (x >= (v * destination.dimensions().width)) {
                px.channel<channel_name::A>(95);
            } else {
                px.channel<channel_name::A>(255);
            }
            // black out the area underneath so alpha blending
            // works correctly
            draw::filled_rectangle(destination, 
                                r, 
                                main_screen.background_color(), 
                                &clip);
            // draw the segment
            draw::filled_rectangle(destination, 
                                r, 
                                px, 
                                &clip);
            // increment
            x += w;
            c += s;
        }
    } else {
        // draw the solid color bars
        // first draw the background
        bcol.channel<channel_name::A>(95);
        draw::filled_rectangle(destination, 
                            srect16((destination.dimensions().width * v), 
                                    y, 
                                    destination.dimensions().width - 1, 
                                    y + h), 
                            bcol, 
                            &clip);
        // now the filled part
        draw::filled_rectangle(destination, 
                            srect16(0, 
                                    y, 
                                    (destination.dimensions().width * v) - 1, 
                                    y + h),
                            col, 
                            &clip);
    }
    // increment to the next bar
    y += h;
}

现在来看图表。图表在某些方面实际上更容易。代码肯定更短。我们所做的就是简单地遍历图表缓冲区,并使用指定颜色(或渐变)的抗锯齿线条,从前一点绘制到当前点。

// reconstitute the state
const graph_info_t& inf = *(graph_info_t*)state;
// store the dimensions
const uint16_t width = destination.dimensions().width;
const uint16_t height = destination.dimensions().height;
spoint16 pt;
// for each graph
for (size_t i = 0; i < inf.size; ++i) {
    // easy access to the current buffer
    buffer_t& buf = inf.buffers[i];
    // the current color
    rgba_pixel<32> col = inf.colors[i];
    // is the graph a gradient?
    bool grad = col == rgba_pixel<32>();
    // the point value
    float v = NAN;
    // if we have data
    if (!buf.empty()) {
        // get and store the first value
        // (translating it to the graph)
        v = *buf.peek(0);
        pt.x = 0;
        pt.y = height - (v * height) - 1;
        if (pt.y < 0) pt.y = 0;
    }
    // for each subsequent value
    for (size_t i = 1; i < buf.size(); ++i) {
        // retrieve the value
        v = *buf.peek(i);
        // if it's a gradient
        if (grad) {
            // get our anchors for the ends
            hsva_pixel<32> px = color<hsva_pixel<32>>::red;
            hsva_pixel<32> px2 = color<hsva_pixel<32>>::green;
            // get our H values
            auto h1 = px.channel<channel_name::H>();
            auto h2 = px2.channel<channel_name::H>();
            // offset the second one to avoid overshoot
            h2 -= 32;
            // get the H range
            auto range = abs(h2 - h1) + 1;
            // set the H value based on v (inverted and offet)
            px.channel<channel_name::H>(h1 + (range - (v * range)));
            // convert to RGBA8888
            convert(px, &col);
        }
        // compute the current data point
        spoint16 pt2;
        pt2.x = (i / 100.0f) * width;
        pt2.y = height - (v * height) - 1;
        if (pt2.y < 0) pt2.y = 0;
        // draw an anti-aliased line
        // from the old point to the 
        // new point.
        draw::line_aa(destination, 
                        srect16(pt, pt2), 
                        col, 
                        col, 
                        true, 
                        &clip);
        // store the current point as 
        // the next old point
        pt = pt2;
    }
}

interface.hpp

此文件声明我们在串行缓冲区中发送的 struct。C# PC 伴侣应用程序中有一个匹配的文件。它们在二进制级别上必须匹配,PC 才能与 ESP32(s) 通信。

#pragma once
#include <stdint.h>
// the packet to receive
typedef struct read_status {
    // the command id
    constexpr static const int command = 1;
    // cpu usage from 0-100
    int cpu_usage;
    // cpu temp (C)
    int cpu_temp;
    // cpu tjmax
    int cpu_temp_max;
    // gpu usage from 0-100
    int gpu_usage;
    // gpu temp (C)
    int gpu_temp;
    // gpu tjmax
    int gpu_temp_max;
} read_status_t;

circular_buffer.hpp

此文件实现了一个简单的循环缓冲区模板。我们在此不讨论它。您可以在 PlatformIOcodewitch-honey-crisis/htcw_data 中找到类似的实现。

lcd_config.h

此文件包含各种 LCD 的配置信息。您可以尝试添加更多配置,但可能需要创建关联的驱动程序。这主要由 lcd_init.h 使用。

lcd_init.h

此文件根据 lcd_config.h 中的数据初始化和连接 LCD。它使用 ESP LCD 面板 API 与显示器通信 - 在有 DMA 可用时异步通信。在此文章中对其进行介绍已超出范围,但您可以在自己的项目中将其用作。部分内容源自 LovyanGFX。

OpenSans_Regular.hpp

此文件包含一个数组,该数组是 True Type 字体文件,逐字记录。固件使用它来渲染文本。它是使用 此工具生成的。请注意,在包含此文件之前,您必须在恰好一个 cpp 文件中 #define FONTNAME_IMPLEMENTATION,其中 FONTNAME 是您在工具 UI 中指定的字体名称,但要大写。该字体是从 fontsquirrel.com 下载的。

disconnected_icon.hpp

此文件包含断开连接图标作为 SVG 文件,逐字记录,但以二进制形式呈现,即使它是 XML。固件使用它来渲染断开连接的屏幕。该头文件是使用生成字体时使用的相同工具生成的。我不记得是从哪里下载的 SVG,我是通过 Google 搜索找到的。与字体文件一样,在包含此文件之前,您必须在恰好一个 cpp 文件中 #define SVGNAME_IMPLEMENTATION,其中 SVGNAME 反映了在工具中指定的名称,但要大写。

ssd1306_surface_adapter.hpp

SSD1306 显示控制器有点奇怪。它是单色的,因此它将 8 个像素打包到一个字节中,但每个字节中的像素是**垂直**排列而不是水平排列。字节仍然从左到右排列,但其中的像素从上到下排列,因此 (0,3) 存储在第一个字节中。

lcd_init.h 在将数据发送到显示器之前不对数据进行转换。这样做提供了一个 UIX 控件表面的替代后端 - 通常使用简单的位图。在这里不起作用,因为内存的排列方式不适合显示器。此适配器是一个“包装器”,它使所有绘图操作创建与显示器帧缓冲区兼容的内存占用。它的工作方式很傻,几乎到了令人尴尬的地步,但超出了本文的范围。

PC 伴侣应用程序

应用程序逻辑的大部分都在主窗体代码中,所以我们先介绍它。

Main.cs

第一个值得注意的代码是一个嵌套类,用于遍历 OpenHardwareMonitorLib 的发布数据,该数据以层次结构报告。这有效地允许我们遍历层次结构并更新我们的信息。

// traverses OHWM data
public class UpdateVisitor : IVisitor
{
    public void VisitComputer(IComputer computer)
    {
        computer.Traverse(this);
    }
    public void VisitHardware(IHardware hardware)
    {
        hardware.Update();
        foreach (IHardware subHardware in hardware.SubHardware)
            subHardware.Accept(this);
    }
    public void VisitSensor(ISensor sensor) { }
    public void VisitParameter(IParameter parameter) { }
}

下一段代码是我们存储在 PortBox 复选框列表中的一个结构。它允许我们为每个列表项保留一个关联的 COM 端口。

// list item that holds a com port
struct PortData
{
    public SerialPort Port;
    public PortData(SerialPort port)
    {
        Port = port;
    }
    public override string ToString()
    {
        if (Port != null)
        {
            return Port.PortName;
        }
        return "<null>";
    }
    public override bool Equals(object obj)
    {
        if (!(obj is PortData)) return false;
        PortData other = (PortData)obj;
        return object.Equals(Port, other.Port);
    }
    public override int GetHashCode()
    {
        if (Port == null) return 0;

        return Port.GetHashCode();
    }
}

下一段代码声明了我们用于保存系统信息数据的成员变量。

// local members for system info
float cpuUsage;
float gpuUsage;
float cpuTemp;
float cpuTjMax;
float gpuTemp;
float gpuTjMax;
private readonly Computer _computer = new Computer
{
    CPUEnabled = true,
    GPUEnabled = true
};

下一段代码用 COM 端口填充我们的复选框列表。它会记住先前已勾选的端口并保持勾选状态。

// Populates the PortBox control with COM ports
void RefreshPortList()
{
    // get the active ports
    var ports = new List<SerialPort>();
    foreach (var item in PortBox.CheckedItems)
    {
        ports.Add(((PortData)item).Port);
    }
    // reset the portbox
    PortBox.Items.Clear();
    var names = SerialPort.GetPortNames();
    foreach (var name in names)
    {
        // check to see if the port is
        // one of the checked ports
        SerialPort found = null;
        foreach (var ep in ports)
        {
            if (ep.PortName == name)
            {
                found = ep;
                break;
            }
        }
        var chk = false;
        if (found == null)
        {
            // create a new port
            found = new SerialPort(name, 115200);
        }
        else
        {
            chk = true;
        }
        PortBox.Items.Add(new PortData(found));
        if (chk)
        {
            // if it's one of our previously
            // checked ports, check it
            PortBox.SetItemChecked(
                PortBox.Items.IndexOf(new PortData(found)), true);
        }
    }
}

计时器的 tick 事件是我们处理收集系统信息并将其发送到 COM 端口的地方。

private void UpdateTimer_Tick(object sender, EventArgs e)
{
    // only process if we're started
    if (StartedCheckBox.Checked)
    {
        // gather the system info
        CollectSystemInfo();
        // put it in the struct for sending
        ReadStatus data;
        data.CpuTemp = (byte)cpuTemp;
        data.CpuUsage = (byte)cpuUsage;
        data.GpuTemp = (byte)gpuTemp;
        data.GpuUsage = (byte)gpuUsage;
        data.CpuTempMax = (byte)cpuTjMax;
        data.GpuTempMax = (byte)gpuTjMax;
        // go through all the ports
        int i = 0;
        foreach (PortData pdata in PortBox.Items)
        {
            var port = pdata.Port;
            // if it's checked
            if (PortBox.GetItemChecked(i))
            {
                try
                {
                    // open if necessary
                    if (!port.IsOpen)
                    {
                        port.Open();
                    }
                    // if there's enough write buffer left
                    if (port.WriteBufferSize - port.BytesToWrite > 
                        1 + System.Runtime.InteropServices.Marshal.SizeOf(data))
                    {
                        // write the command id
                        var ba = new byte[] { 1 };
                        port.Write(ba, 0, ba.Length);
                        // write the data
                        port.WriteStruct(data);
                    }
                    port.BaseStream.Flush();
                }
                catch { }
            }
            else
            {
                // make sure unchecked ports are closed
                if (port.IsOpen)
                {
                    try { port.Close(); } catch { }
                }
            }
            ++i;
        }
    }
}

下一个例程负责通过遍历所有报告的项目来实际从 OpenHardwareMonitorLib 收集相关系统信息,并将相关信息存储在前面声明的窗体的成员变量中。

void CollectSystemInfo()
{
    // use OpenHardwareMonitorLib to collect the system info
    var updateVisitor = new UpdateVisitor();
    _computer.Accept(updateVisitor);
    cpuTjMax = (int)CpuMaxUpDown.Value;
    gpuTjMax = (int)GpuMaxUpDown.Value;
    for (int i = 0; i < _computer.Hardware.Length; i++)
    {
        if (_computer.Hardware[i].HardwareType == HardwareType.CPU)
        {
            for (int j = 0; j < _computer.Hardware[i].Sensors.Length; j++)
            {
                var sensor = _computer.Hardware[i].Sensors[j];
                if (sensor.SensorType == SensorType.Temperature && 
                    sensor.Name.Contains("CPU Package"))
                {
                    for (int k = 0; k < sensor.Parameters.Length; ++k)
                    {
                        var p = sensor.Parameters[i];
                        if (p.Name.ToLowerInvariant().Contains("tjmax"))
                        {
                            cpuTjMax = (float)p.Value;
                        }
                    }
                    cpuTemp = sensor.Value.GetValueOrDefault();
                }
                else if (sensor.SensorType == SensorType.Load && 
                    sensor.Name.Contains("CPU Total"))
                {
                    // store
                    cpuUsage = sensor.Value.GetValueOrDefault();
                }
            }
        }
        if (_computer.Hardware[i].HardwareType == HardwareType.GpuAti || 
            _computer.Hardware[i].HardwareType == HardwareType.GpuNvidia)
        {
            for (int j = 0; j < _computer.Hardware[i].Sensors.Length; j++)
            {
                var sensor = _computer.Hardware[i].Sensors[j];
                if (sensor.SensorType == SensorType.Temperature && 
                    sensor.Name.Contains("GPU Core"))
                {
                    // store
                    gpuTemp = sensor.Value.GetValueOrDefault();
                }
                else if (sensor.SensorType == SensorType.Load && 
                    sensor.Name.Contains("GPU Core"))
                {
                    // store
                    gpuUsage = sensor.Value.GetValueOrDefault();
                }
            }
        }
    }
}

窗体的其余部分是简单的事件处理。

Interface.cs

此文件对应于固件中的 interface.hpp,并提供了将二进制数据通过串行 UART 发送所需的结构。如前所述,它们必须与另一个文件匹配才能进行通信。我们使用 .NET 封送处理将结构转换为字节数组。

using System.Runtime.InteropServices;
namespace EspMon
{
    // the data to send over serial
    // pack for 32-bit systems
    [StructLayout(LayoutKind.Sequential,Pack = 4)]
    internal struct ReadStatus
    {
        // command = 1
        public int CpuUsage;
        public int CpuTemp;
        public int CpuTempMax;
        public int GpuUsage;
        public int GpuTemp;
        public int GpuTempMax;
    }
}

SerialExtensions.cs

此文件为 SerialPort 类提供了附加功能。主要允许您以二进制形式通过串行端口封送结构。我们本来可以,也许应该使用二进制序列化,因为它就是为此设计的,但虽然这有点 hacky,但也是修改和创建结构的最简单方法。在此文中探讨其工作原理已超出范围,但您可以在自己的项目中轻松使用它。

app.manifest

此文件包含使可执行文件需要提升的权限的必要信息,这对于 OpenHardwareMonitorLib 的正常运行是必不可少的。

OpenHardwareMonitorLib

如前所述,这不是我的项目,但我将其包含在内,因为我已对其进行修补,使其能够与 Raptor Lake 桌面和移动处理器一起使用。

历史

  • 2023 年 6 月 6 日 - 初次提交
  • 2023 年 6 月 14 日 - 更新以支持更多设备,进行一些清理
  • 2023 年 6 月 14 日 - 更新以支持新的 UIX。对 HelTec WiFi Kit v2 的实验性支持
  • 2023 年 7 月 7 日 - 更新以支持 Wio Terminal 和 ESP-IDF(仅限 Github)
© . All rights reserved.