EspMon 重启






4.62/5 (6投票s)
使用 ESP32 监控您的 PC 的 CPU 和 GPU。
引言
我曾制作过一个 此项目的先前版本,使用的是 LVGL 和 T-Display。我认为值得重新审视,因为我改进了很多,并且将它改为使用我的 UIX 和 GFX 库,而不是 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.h、lcd_init.h 和 platformio.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
此文件实现了一个简单的循环缓冲区模板。我们在此不讨论它。您可以在 PlatformIO
库 codewitch-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)