diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0fee6d0..9d48761 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,4 +48,23 @@ jobs: name: nrf52811_xxaa path: | _build/nrf52811_xxaa.hex - SDK/17.1.0_ddde560/components/softdevice/s112/hex/s112_nrf52_7.2.0_softdevice.hex \ No newline at end of file + 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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 508e75f..687667d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ *.obj *.o *.sbr +*.exe # Build files # define exception below if needed diff --git a/EPD/EPD_service.c b/EPD/EPD_service.c index efdd5d1..0e64aa4 100644 --- a/EPD/EPD_service.c +++ b/EPD/EPD_service.c @@ -43,7 +43,16 @@ static void epd_gui_update(void * p_event_data, uint16_t event_size) EPD_GPIO_Init(); 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(); } diff --git a/EPD/UC8176.c b/EPD/UC8176.c index d8d24c7..18f2c7e 100644 --- a/EPD/UC8176.c +++ b/EPD/UC8176.c @@ -77,6 +77,7 @@ static void UC8176_PowerOff(void) int8_t UC8176_Read_Temp(void) { EPD_WriteCommand(CMD_TSC); + UC8176_WaitBusy(100); return (int8_t) EPD_ReadByte(); } diff --git a/GUI/GUI.c b/GUI/GUI.c index 38d0629..b7db204 100644 --- a/GUI/GUI.c +++ b/GUI/GUI.c @@ -1,12 +1,8 @@ -#include "Adafruit_GFX.h" #include "fonts.h" #include "Lunar.h" #include "GUI.h" -#include "nrf_log.h" #include -#define PAGE_HEIGHT ((__HEAP_SIZE / 50) - 4) - #define GFX_printf_styled(gfx, fg, bg, font, ...) \ GFX_setTextColor(gfx, fg, bg); \ 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); } -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)(vol * 100 / 4.2); + uint8_t level = (uint8_t)(voltage * 100 / 4.2); GFX_setCursor(gfx, x - 26, y + 9); 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_drawRect(gfx, x, y, 20, 10, 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); } -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); 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], Lunar_DateString[Lunar->Date]); - DrawBattery(gfx, 330, 25); - DrawTemperature(gfx, 330, 58, temp); + DrawBattery(gfx, 330, 25, data->voltage); + DrawTemperature(gfx, 330, 58, data->temperature); GFX_drawFastHLine(gfx, 30, 68, 330, GFX_BLACK); 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}; 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); Adafruit_GFX gfx; - if (epd->bwr) - GFX_begin_3c(&gfx, epd->width, epd->height, PAGE_HEIGHT); + if (data->bwr) + GFX_begin_3c(&gfx, data->width, data->height, PAGE_HEIGHT); else - GFX_begin(&gfx, epd->width, epd->height, PAGE_HEIGHT); + GFX_begin(&gfx, data->width, data->height, PAGE_HEIGHT); GFX_firstPage(&gfx); do { - NRF_LOG_DEBUG("page %d\n", gfx.current_page); GFX_fillScreen(&gfx, GFX_WHITE); switch (mode) { @@ -222,16 +216,12 @@ void DrawGUI(epd_model_t *epd, uint32_t timestamp, display_mode_t mode) DrawCalendar(&gfx, &tm, &Lunar); break; case MODE_CLOCK: - DrawClock(&gfx, &tm, &Lunar, epd->drv->read_temp()); + DrawClock(&gfx, &tm, &Lunar, data); break; default: break; } - } while(GFX_nextPage(&gfx, epd->drv->write_image)); + } while(GFX_nextPage(&gfx, draw)); GFX_end(&gfx); - - NRF_LOG_DEBUG("display start\n"); - epd->drv->refresh(); - NRF_LOG_DEBUG("display end\n"); } diff --git a/GUI/GUI.h b/GUI/GUI.h index 4d16fca..8a51a21 100644 --- a/GUI/GUI.h +++ b/GUI/GUI.h @@ -1,8 +1,11 @@ #ifndef __GUI_H #define __GUI_H -#include -#include "EPD_driver.h" +#include "Adafruit_GFX.h" + +#ifndef PAGE_HEIGHT +#define PAGE_HEIGHT ((__HEAP_SIZE / 50) - 4) +#endif typedef enum { MODE_NONE = 0, @@ -10,6 +13,15 @@ typedef enum { MODE_CLOCK = 2, } 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 diff --git a/Makefile.win32 b/Makefile.win32 new file mode 100644 index 0000000..4303547 --- /dev/null +++ b/Makefile.win32 @@ -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) \ No newline at end of file diff --git a/README.md b/README.md index 9c68f40..0ef1b37 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,27 @@ 2. 切换到 `flash_softdevice`,下载蓝牙协议栈,**不要编译直接下载**(只需刷一次) 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 +``` + ## 附录 上位机支持的指令列表(指令和参数全部要使用十六进制): diff --git a/docs/images/4.jpg b/docs/images/4.jpg new file mode 100644 index 0000000..17a099e Binary files /dev/null and b/docs/images/4.jpg differ diff --git a/emulator.c b/emulator.c new file mode 100644 index 0000000..b26a33b --- /dev/null +++ b/emulator.c @@ -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 +#include +#include +#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; +} \ No newline at end of file