add GUI emulator

This commit is contained in:
Shuanglei Tao
2025-04-27 14:02:13 +08:00
parent 6d1bbcf3e3
commit 1b9d5b3334
10 changed files with 361 additions and 28 deletions

View File

@@ -49,3 +49,22 @@ jobs:
path: | path: |
_build/nrf52811_xxaa.hex _build/nrf52811_xxaa.hex
SDK/17.1.0_ddde560/components/softdevice/s112/hex/s112_nrf52_7.2.0_softdevice.hex SDK/17.1.0_ddde560/components/softdevice/s112/hex/s112_nrf52_7.2.0_softdevice.hex
win32:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install MSYS2
uses: msys2/setup-msys2@v2
with:
update: true
install: >-
make
mingw-w64-x86_64-gcc
- name: Build
run: make -f Makefile.win32
- uses: actions/upload-artifact@v4
with:
name: emulator
path: emulator.exe

1
.gitignore vendored
View File

@@ -33,6 +33,7 @@
*.obj *.obj
*.o *.o
*.sbr *.sbr
*.exe
# Build files # Build files
# define exception below if needed # define exception below if needed

View File

@@ -43,7 +43,16 @@ static void epd_gui_update(void * p_event_data, uint16_t event_size)
EPD_GPIO_Init(); EPD_GPIO_Init();
epd_model_t *epd = epd_init((epd_model_id_t)p_epd->config.model_id); epd_model_t *epd = epd_init((epd_model_id_t)p_epd->config.model_id);
DrawGUI(epd, event->timestamp, p_epd->display_mode); gui_data_t data = {
.bwr = epd->bwr,
.width = epd->width,
.height = epd->height,
.timestamp = event->timestamp,
.temperature = epd->drv->read_temp(),
.voltage = EPD_ReadVoltage(),
};
DrawGUI(&data, epd->drv->write_image, p_epd->display_mode);
epd->drv->refresh();
EPD_GPIO_Uninit(); EPD_GPIO_Uninit();
} }

View File

@@ -77,6 +77,7 @@ static void UC8176_PowerOff(void)
int8_t UC8176_Read_Temp(void) int8_t UC8176_Read_Temp(void)
{ {
EPD_WriteCommand(CMD_TSC); EPD_WriteCommand(CMD_TSC);
UC8176_WaitBusy(100);
return (int8_t) EPD_ReadByte(); return (int8_t) EPD_ReadByte();
} }

View File

@@ -1,12 +1,8 @@
#include "Adafruit_GFX.h"
#include "fonts.h" #include "fonts.h"
#include "Lunar.h" #include "Lunar.h"
#include "GUI.h" #include "GUI.h"
#include "nrf_log.h"
#include <stdio.h> #include <stdio.h>
#define PAGE_HEIGHT ((__HEAP_SIZE / 50) - 4)
#define GFX_printf_styled(gfx, fg, bg, font, ...) \ #define GFX_printf_styled(gfx, fg, bg, font, ...) \
GFX_setTextColor(gfx, fg, bg); \ GFX_setTextColor(gfx, fg, bg); \
GFX_setFont(gfx, font); \ GFX_setFont(gfx, font); \
@@ -142,13 +138,12 @@ static void DrawTime(Adafruit_GFX *gfx, tm_t *tm, int16_t x, int16_t y, uint16_t
Draw7Number(gfx, tm->tm_min, x, y, cS, GFX_BLACK, GFX_WHITE, nD); Draw7Number(gfx, tm->tm_min, x, y, cS, GFX_BLACK, GFX_WHITE, nD);
} }
static void DrawBattery(Adafruit_GFX *gfx, int16_t x, int16_t y) static void DrawBattery(Adafruit_GFX *gfx, int16_t x, int16_t y, float voltage)
{ {
float vol = EPD_ReadVoltage(); uint8_t level = (uint8_t)(voltage * 100 / 4.2);
uint8_t level = (uint8_t)(vol * 100 / 4.2);
GFX_setCursor(gfx, x - 26, y + 9); GFX_setCursor(gfx, x - 26, y + 9);
GFX_setFont(gfx, u8g2_font_wqy9_t_lunar); GFX_setFont(gfx, u8g2_font_wqy9_t_lunar);
GFX_printf(gfx, "%.1fV", vol); GFX_printf(gfx, "%.1fV", voltage);
GFX_fillRect(gfx, x, y, 20, 10, GFX_WHITE); GFX_fillRect(gfx, x, y, 20, 10, GFX_WHITE);
GFX_drawRect(gfx, x, y, 20, 10, GFX_BLACK); GFX_drawRect(gfx, x, y, 20, 10, GFX_BLACK);
GFX_fillRect(gfx, x + 20, y + 4, 2, 2, GFX_BLACK); GFX_fillRect(gfx, x + 20, y + 4, 2, 2, GFX_BLACK);
@@ -162,7 +157,7 @@ static void DrawTemperature(Adafruit_GFX *gfx, int16_t x, int16_t y, int8_t temp
GFX_printf(gfx, "%d℃", temp); GFX_printf(gfx, "%d℃", temp);
} }
static void DrawClock(Adafruit_GFX *gfx, tm_t *tm, struct Lunar_Date *Lunar, int8_t temp) static void DrawClock(Adafruit_GFX *gfx, tm_t *tm, struct Lunar_Date *Lunar, gui_data_t *data)
{ {
DrawDate(gfx, 40, 36, tm); DrawDate(gfx, 40, 36, tm);
GFX_setCursor(gfx, 40, 58); GFX_setCursor(gfx, 40, 58);
@@ -172,8 +167,8 @@ static void DrawClock(Adafruit_GFX *gfx, tm_t *tm, struct Lunar_Date *Lunar, int
GFX_printf(gfx, "%s%s%s", Lunar_MonthLeapString[Lunar->IsLeap], Lunar_MonthString[Lunar->Month], GFX_printf(gfx, "%s%s%s", Lunar_MonthLeapString[Lunar->IsLeap], Lunar_MonthString[Lunar->Month],
Lunar_DateString[Lunar->Date]); Lunar_DateString[Lunar->Date]);
DrawBattery(gfx, 330, 25); DrawBattery(gfx, 330, 25, data->voltage);
DrawTemperature(gfx, 330, 58, temp); DrawTemperature(gfx, 330, 58, data->temperature);
GFX_drawFastHLine(gfx, 30, 68, 330, GFX_BLACK); GFX_drawFastHLine(gfx, 30, 68, 330, GFX_BLACK);
DrawTime(gfx, tm, 70, 98, 5, 2); DrawTime(gfx, tm, 70, 98, 5, 2);
@@ -197,24 +192,23 @@ static void DrawClock(Adafruit_GFX *gfx, tm_t *tm, struct Lunar_Date *Lunar, int
} }
} }
void DrawGUI(epd_model_t *epd, uint32_t timestamp, display_mode_t mode) void DrawGUI(gui_data_t *data, buffer_callback draw, display_mode_t mode)
{ {
tm_t tm = {0}; tm_t tm = {0};
struct Lunar_Date Lunar; struct Lunar_Date Lunar;
transformTime(timestamp, &tm); transformTime(data->timestamp, &tm);
LUNAR_SolarToLunar(&Lunar, tm.tm_year + YEAR0, tm.tm_mon + 1, tm.tm_mday); LUNAR_SolarToLunar(&Lunar, tm.tm_year + YEAR0, tm.tm_mon + 1, tm.tm_mday);
Adafruit_GFX gfx; Adafruit_GFX gfx;
if (epd->bwr) if (data->bwr)
GFX_begin_3c(&gfx, epd->width, epd->height, PAGE_HEIGHT); GFX_begin_3c(&gfx, data->width, data->height, PAGE_HEIGHT);
else else
GFX_begin(&gfx, epd->width, epd->height, PAGE_HEIGHT); GFX_begin(&gfx, data->width, data->height, PAGE_HEIGHT);
GFX_firstPage(&gfx); GFX_firstPage(&gfx);
do { do {
NRF_LOG_DEBUG("page %d\n", gfx.current_page);
GFX_fillScreen(&gfx, GFX_WHITE); GFX_fillScreen(&gfx, GFX_WHITE);
switch (mode) { switch (mode) {
@@ -222,16 +216,12 @@ void DrawGUI(epd_model_t *epd, uint32_t timestamp, display_mode_t mode)
DrawCalendar(&gfx, &tm, &Lunar); DrawCalendar(&gfx, &tm, &Lunar);
break; break;
case MODE_CLOCK: case MODE_CLOCK:
DrawClock(&gfx, &tm, &Lunar, epd->drv->read_temp()); DrawClock(&gfx, &tm, &Lunar, data);
break; break;
default: default:
break; break;
} }
} while(GFX_nextPage(&gfx, epd->drv->write_image)); } while(GFX_nextPage(&gfx, draw));
GFX_end(&gfx); GFX_end(&gfx);
NRF_LOG_DEBUG("display start\n");
epd->drv->refresh();
NRF_LOG_DEBUG("display end\n");
} }

View File

@@ -1,8 +1,11 @@
#ifndef __GUI_H #ifndef __GUI_H
#define __GUI_H #define __GUI_H
#include <stdint.h> #include "Adafruit_GFX.h"
#include "EPD_driver.h"
#ifndef PAGE_HEIGHT
#define PAGE_HEIGHT ((__HEAP_SIZE / 50) - 4)
#endif
typedef enum { typedef enum {
MODE_NONE = 0, MODE_NONE = 0,
@@ -10,6 +13,15 @@ typedef enum {
MODE_CLOCK = 2, MODE_CLOCK = 2,
} display_mode_t; } display_mode_t;
void DrawGUI(epd_model_t *epd, uint32_t timestamp, display_mode_t mode); typedef struct {
bool bwr;
uint16_t width;
uint16_t height;
uint32_t timestamp;
int8_t temperature;
float voltage;
} gui_data_t;
void DrawGUI(gui_data_t *data, buffer_callback draw, display_mode_t mode);
#endif #endif

18
Makefile.win32 Normal file
View File

@@ -0,0 +1,18 @@
CC = gcc
CFLAGS = -Wall -O2 -IGUI -DPAGE_HEIGHT=600
LDFLAGS = -lgdi32 -mwindows
SRCS = GUI/Adafruit_GFX.c GUI/u8g2_font.c GUI/fonts.c GUI/GUI.c GUI/Lunar.c emulator.c
OBJS = $(SRCS:.c=.o)
TARGET = emulator.exe
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)

View File

@@ -72,6 +72,27 @@
2. 切换到 `flash_softdevice`,下载蓝牙协议栈,**不要编译直接下载**(只需刷一次) 2. 切换到 `flash_softdevice`,下载蓝牙协议栈,**不要编译直接下载**(只需刷一次)
3. 切换到 `nRF51822_xxAA`,先编译再下载 3. 切换到 `nRF51822_xxAA`,先编译再下载
### 模拟器
本项目提供了一个可在 Windows 下运行界面代码的模拟器,修改了界面代码后无需下载到单片机即可查看效果。
仿真效果图:
![](docs/images/4.jpg)
> **提示:** 按 `空格` 可切换日历时钟界面,按 `R` 可切换黑白、三色
**编译方法:**
下载并安装 [MSYS2](https://www.msys2.org) 后,打开 `MSYS2 MINGW64` 命令窗口执行:
```bash
pacman -Syu
pacman -S make mingw-w64-x86_64-gcc
cd <本项目目录>
make -f Makefile.win32
```
## 附录 ## 附录
上位机支持的指令列表(指令和参数全部要使用十六进制): 上位机支持的指令列表(指令和参数全部要使用十六进制):

BIN
docs/images/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

262
emulator.c Normal file
View File

@@ -0,0 +1,262 @@
// GUI emulator for Windows
// This code is a simple Windows GUI application that emulates the display of an e-paper device.
#include <windows.h>
#include <stdint.h>
#include <time.h>
#include "GUI.h"
#define BITMAP_WIDTH 400
#define BITMAP_HEIGHT 300
#define WINDOW_WIDTH 400
#define WINDOW_HEIGHT 340
#define WINDOW_TITLE TEXT("Emurator")
// Global variables
HINSTANCE g_hInstance;
HWND g_hwnd;
display_mode_t g_display_mode = MODE_CALENDAR; // Default to calendar mode
BOOL g_bwr_mode = TRUE; // Default to BWR mode
// Convert bitmap data from e-paper format to Windows DIB format
static uint8_t *convertBitmap(uint8_t *bitmap, uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
int bytesPerRow = ((w + 31) / 32) * 4; // Round up to nearest 4 bytes
int totalSize = bytesPerRow * h;
// Allocate memory for converted bitmap
uint8_t *convertedBitmap = (uint8_t*)malloc(totalSize);
if (convertedBitmap == NULL) return NULL;
memset(convertedBitmap, 0, totalSize);
int ePaperBytesPerRow = (w + 7) / 8; // E-paper buffer stride
for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) {
// Calculate byte and bit position in e-paper buffer
int bytePos = row * ePaperBytesPerRow + col / 8;
int bitPos = 7 - (col % 8); // MSB first (typical e-paper format)
// Check if the bit is set in the e-paper buffer
int isSet = (bitmap[bytePos] >> bitPos) & 0x01;
// Calculate byte and bit position in Windows DIB
int dibBytePos = row * bytesPerRow + col / 8;
int dibBitPos = 7 - (col % 8); // MSB first for DIB too
// Set the bit in the Windows DIB if it's set in the e-paper buffer
if (isSet) {
convertedBitmap[dibBytePos] |= (1 << dibBitPos);
}
}
}
return convertedBitmap;
}
// Implementation of the buffer_callback function
void DrawBitmap(uint8_t *black, uint8_t *color, uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
HDC hdc;
RECT clientRect;
int scale = 1;
// Get the device context for immediate drawing
hdc = GetDC(g_hwnd);
if (!hdc) return;
// Get client area for positioning
GetClientRect(g_hwnd, &clientRect);
// Calculate position to center the entire bitmap in the window
int drawX = (clientRect.right - BITMAP_WIDTH * scale) / 2;
int drawY = (clientRect.bottom - BITMAP_HEIGHT * scale) / 2;
// Create DIB for visible pixels
BITMAPINFO bmi;
ZeroMemory(&bmi, sizeof(BITMAPINFO));
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = w;
bmi.bmiHeader.biHeight = -h; // Negative for top-down bitmap
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 1;
bmi.bmiHeader.biCompression = BI_RGB;
uint8_t *convertedBitmap = convertBitmap(black, x, y, w, h);
if (convertedBitmap == NULL) {
ReleaseDC(g_hwnd, hdc);
return;
}
// Set colors for black and white display
bmi.bmiColors[0].rgbBlue = 0;
bmi.bmiColors[0].rgbGreen = 0;
bmi.bmiColors[0].rgbRed = 0;
bmi.bmiColors[0].rgbReserved = 0;
bmi.bmiColors[1].rgbBlue = 255;
bmi.bmiColors[1].rgbGreen = 255;
bmi.bmiColors[1].rgbRed = 255;
bmi.bmiColors[1].rgbReserved = 0;
// Draw the black layer
StretchDIBits(hdc,
drawX + x * scale, drawY + y * scale, // Destination position
w * scale, h * scale, // Destination size
0, 0, // Source position
w, h, // Source size
convertedBitmap, // Converted bitmap bits
&bmi, // Bitmap info
DIB_RGB_COLORS, // Usage
SRCCOPY); // Raster operation code
free(convertedBitmap);
// Handle color layer if present (red in BWR displays)
if (color) {
// Allocate memory for converted color bitmap
uint8_t *convertedColor = convertBitmap(color, x, y, w, h);
if (convertedColor) {
// Set colors for red overlay
bmi.bmiColors[0].rgbBlue = 255;
bmi.bmiColors[0].rgbGreen = 255;
bmi.bmiColors[0].rgbRed = 0;
bmi.bmiColors[0].rgbReserved = 0;
bmi.bmiColors[1].rgbBlue = 0;
bmi.bmiColors[1].rgbGreen = 0;
bmi.bmiColors[1].rgbRed = 0;
bmi.bmiColors[1].rgbReserved = 0;
// Draw red overlay
StretchDIBits(hdc,
drawX + x * scale, drawY + y * scale, // Destination position
w * scale, h * scale, // Destination size
0, 0, // Source position
w, h, // Source size
convertedColor, // Converted bitmap bits
&bmi, // Bitmap info
DIB_RGB_COLORS, // Usage
SRCINVERT); // Use XOR operation to blend
free(convertedColor);
}
}
// Release the device context
ReleaseDC(g_hwnd, hdc);
}
// Window procedure
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_CREATE:
// Set a timer to update the display periodically (every second)
SetTimer(hwnd, 1, 1000, NULL);
return 0;
case WM_TIMER:
// Force a redraw of the window without erasing the background
InvalidateRect(hwnd, NULL, FALSE);
return 0;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// Get client rect for calculations
RECT clientRect;
GetClientRect(hwnd, &clientRect);
// Clear the entire client area with a solid color
HBRUSH bgBrush = CreateSolidBrush(RGB(240, 240, 240));
FillRect(hdc, &clientRect, bgBrush);
DeleteObject(bgBrush);
// Get current timestamp
gui_data_t data = {
.bwr = g_bwr_mode,
.width = BITMAP_WIDTH,
.height = BITMAP_HEIGHT,
.timestamp = time(NULL) + 8*3600,
.temperature = 25,
.voltage = 3.2f,
};
// Call DrawGUI to render the interface, passing the BWR mode
DrawGUI(&data, DrawBitmap, g_display_mode);
EndPaint(hwnd, &ps);
return 0;
}
case WM_KEYDOWN:
// Toggle display mode with spacebar
if (wParam == VK_SPACE) {
if (g_display_mode == MODE_CLOCK)
g_display_mode = MODE_CALENDAR;
else
g_display_mode = MODE_CLOCK;
InvalidateRect(hwnd, NULL, TRUE);
}
// Toggle BWR mode with R key
else if (wParam == 'R') {
g_bwr_mode = !g_bwr_mode;
InvalidateRect(hwnd, NULL, TRUE);
}
return 0;
case WM_DESTROY:
KillTimer(hwnd, 1);
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd, message, wParam, lParam);
}
}
// Main entry point
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
g_hInstance = hInstance;
// Register window class
WNDCLASSA wc = {0}; // Using WNDCLASSA for ANSI version
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wc.lpszClassName = "BitmapDemo"; // No L prefix - using ANSI strings
if (!RegisterClassA(&wc)) {
MessageBoxA(NULL, "Window Registration Failed!", "Error", MB_ICONEXCLAMATION | MB_OK);
return 0;
}
// Create the window - explicit use of CreateWindowA for ANSI version
g_hwnd = CreateWindowA(
"BitmapDemo",
"Emurator (Press Space/R Key)", // Using simple title
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
WINDOW_WIDTH, WINDOW_HEIGHT,
NULL, NULL, hInstance, NULL
);
if (!g_hwnd) {
MessageBoxA(NULL, "Window Creation Failed!", "Error", MB_ICONEXCLAMATION | MB_OK);
return 0;
}
// Show window
ShowWindow(g_hwnd, nCmdShow);
UpdateWindow(g_hwnd);
// Main message loop
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}