upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-19 01:44:11 +0530
committerYour Name <you@example.com>2026-05-19 01:44:11 +0530
commit58a0b5fd115d9687a1292e5e82e6b9fa8454b930 (patch)
treedd9f18903d0237edcf5844c58fcb88bcf737b22c
parent699fc6c03899c3b1ff853d8c7c6cf32173436354 (diff)
Dynamic layout for WiFi setup: landscape rotation, responsive render, highlight feedback
- Render functions use axs15231b_get_width/height() instead of hardcoded coords - render_wifi_setup_list: dynamic item widths, screen-relative cancel button - render_wifi_setup_password: dynamic field/eye/keyboard from kb_get_layout() - render_wifi_setup_result: centered on screen, dynamic button placement - handle_wifi_setup_touch: matching dynamic hit areas, highlight_rect() feedback - Rotation lifecycle: enter_wifi_setup_rotation/exit wired into all entry/exit paths - display_enter_wifi_setup + READY/ERROR touch + SUCCESS auto-return + LIST cancel - Added kb_result_t typedef to keyboard.h (was only in .c) - Fixed test_keyboard.c: use computed offsets from kb_layout_t defaults - All 101 unit tests pass (touch:19, keyboard:46, wifi_setup:36)
-rw-r--r--components/axs15231b/axs15231b.c32
-rw-r--r--components/axs15231b/include/axs15231b.h1
-rw-r--r--main/display.c164
-rw-r--r--main/keyboard.c72
-rw-r--r--main/keyboard.h26
-rw-r--r--main/touch.c25
-rw-r--r--main/touch.h1
-rwxr-xr-xtests/unit/test_keyboardbin0 -> 35416 bytes
-rw-r--r--tests/unit/test_keyboard.c22
-rwxr-xr-xtests/unit/test_touchbin0 -> 30304 bytes
-rwxr-xr-xtests/unit/test_wifi_setupbin0 -> 33336 bytes
11 files changed, 261 insertions, 82 deletions
diff --git a/components/axs15231b/axs15231b.c b/components/axs15231b/axs15231b.c
index 7424a20..77708dd 100644
--- a/components/axs15231b/axs15231b.c
+++ b/components/axs15231b/axs15231b.c
@@ -37,6 +37,7 @@ static spi_device_handle_t s_spi = NULL;
37static uint16_t *s_fb = NULL; 37static uint16_t *s_fb = NULL;
38static int s_width = AXS15231B_WIDTH; 38static int s_width = AXS15231B_WIDTH;
39static int s_height = AXS15231B_HEIGHT; 39static int s_height = AXS15231B_HEIGHT;
40static int s_rotation = 0;
40static uint8_t *s_swap_buf = NULL; 41static uint8_t *s_swap_buf = NULL;
41#define SWAP_BUF_PIXELS 2048 42#define SWAP_BUF_PIXELS 2048
42 43
@@ -354,3 +355,34 @@ void axs15231b_flush(void) {
354 355
355int axs15231b_get_width(void) { return s_width; } 356int axs15231b_get_width(void) { return s_width; }
356int axs15231b_get_height(void) { return s_height; } 357int axs15231b_get_height(void) { return s_height; }
358
359void axs15231b_set_rotation(int rotation) {
360 uint8_t madctl = MADCTL_RGB;
361 switch (rotation) {
362 case 0:
363 madctl = MADCTL_RGB;
364 s_width = AXS15231B_WIDTH;
365 s_height = AXS15231B_HEIGHT;
366 break;
367 case 1:
368 madctl = MADCTL_MX | MADCTL_MV;
369 s_width = AXS15231B_HEIGHT;
370 s_height = AXS15231B_WIDTH;
371 break;
372 case 2:
373 madctl = MADCTL_MX | MADCTL_MY;
374 s_width = AXS15231B_WIDTH;
375 s_height = AXS15231B_HEIGHT;
376 break;
377 case 3:
378 madctl = MADCTL_MY | MADCTL_MV;
379 s_width = AXS15231B_HEIGHT;
380 s_height = AXS15231B_WIDTH;
381 break;
382 default:
383 return;
384 }
385 s_rotation = rotation;
386 qspi_write_cmd_data8(MADCTL, madctl);
387 ESP_LOGI(TAG, "Rotation set to %d (%dx%d)", rotation, s_width, s_height);
388}
diff --git a/components/axs15231b/include/axs15231b.h b/components/axs15231b/include/axs15231b.h
index cddea98..a8c1a37 100644
--- a/components/axs15231b/include/axs15231b.h
+++ b/components/axs15231b/include/axs15231b.h
@@ -23,5 +23,6 @@ void axs15231b_fill_rect(int x, int y, int w, int h, uint16_t color);
23void axs15231b_flush(void); 23void axs15231b_flush(void);
24int axs15231b_get_width(void); 24int axs15231b_get_width(void);
25int axs15231b_get_height(void); 25int axs15231b_get_height(void);
26void axs15231b_set_rotation(int rotation);
26 27
27#endif 28#endif
diff --git a/main/display.c b/main/display.c
index e5e31c4..77b911d 100644
--- a/main/display.c
+++ b/main/display.c
@@ -46,18 +46,54 @@ static int64_t s_last_allotment_ms = 0;
46#define COLOR_GRAY 0x8410 46#define COLOR_GRAY 0x8410
47#define COLOR_DARKGRAY 0x4208 47#define COLOR_DARKGRAY 0x4208
48#define COLOR_LIGHTBLUE 0xA5FF 48#define COLOR_LIGHTBLUE 0xA5FF
49#define COLOR_HIGHLIGHT 0x07E0
49 50
50static wifi_setup_t s_wifi_setup; 51static wifi_setup_t s_wifi_setup;
51static kb_state_t s_kb_state; 52static kb_state_t s_kb_state;
52static bool s_wifi_setup_active = false; 53static bool s_wifi_setup_active = false;
53static bool s_touch_initialized = false; 54static bool s_touch_initialized = false;
54static bool s_wifi_scan_pending = false; 55static bool s_wifi_scan_pending = false;
56static int s_display_rotation = 0;
55 57
56#define SETUP_BTN_X 240 58#define SETUP_BTN_X 240
57#define SETUP_BTN_Y 440 59#define SETUP_BTN_Y 440
58#define SETUP_BTN_W 72 60#define SETUP_BTN_W 72
59#define SETUP_BTN_H 30 61#define SETUP_BTN_H 30
60 62
63static void enter_wifi_setup_rotation(void) {
64 if (s_display_rotation != 1) {
65 s_display_rotation = 1;
66 axs15231b_set_rotation(1);
67 touch_set_rotation(1);
68 kb_layout_t landscape = {
69 .key_w = 38,
70 .key_h = 40,
71 .key_gap = 2,
72 .start_y = 170,
73 .screen_w = 480,
74 .row_count = 4,
75 };
76 kb_set_layout(&landscape);
77 }
78}
79
80static void exit_wifi_setup_rotation(void) {
81 if (s_display_rotation != 0) {
82 s_display_rotation = 0;
83 axs15231b_set_rotation(0);
84 touch_set_rotation(0);
85 kb_layout_t portrait = {
86 .key_w = 28,
87 .key_h = 36,
88 .key_gap = 2,
89 .start_y = 70,
90 .screen_w = 320,
91 .row_count = 4,
92 };
93 kb_set_layout(&portrait);
94 }
95}
96
61static void render_setup_button(int x, int y, int w, int h) { 97static void render_setup_button(int x, int y, int w, int h) {
62 axs15231b_fill_rect(x, y, w, h, COLOR_DARKGRAY); 98 axs15231b_fill_rect(x, y, w, h, COLOR_DARKGRAY);
63 const char *label = "Setup"; 99 const char *label = "Setup";
@@ -69,6 +105,14 @@ static bool touch_in_rect(uint16_t tx, uint16_t ty, int x, int y, int w, int h)
69 return tx >= x && tx < x + w && ty >= y && ty < y + h; 105 return tx >= x && tx < x + w && ty >= y && ty < y + h;
70} 106}
71 107
108static void highlight_rect(int x, int y, int w, int h) {
109 axs15231b_fill_rect(x, y, w, h, COLOR_HIGHLIGHT);
110 axs15231b_flush();
111 vTaskDelay(pdMS_TO_TICKS(80));
112 axs15231b_fill_rect(x, y, w, h, COLOR_DARKGRAY);
113 axs15231b_flush();
114}
115
72static int qr_version_from_strlen(int len) { 116static int qr_version_from_strlen(int len) {
73 if (len <= 17) return 1; 117 if (len <= 17) return 1;
74 if (len <= 32) return 2; 118 if (len <= 32) return 2;
@@ -357,6 +401,7 @@ static void render_wifi_setup_scanning(void) {
357 401
358static void render_wifi_setup_list(void) { 402static void render_wifi_setup_list(void) {
359 int screen_w = axs15231b_get_width(); 403 int screen_w = axs15231b_get_width();
404 int screen_h = axs15231b_get_height();
360 axs15231b_fill_screen(COLOR_BG); 405 axs15231b_fill_screen(COLOR_BG);
361 406
362 const char *title = "Select Network"; 407 const char *title = "Select Network";
@@ -365,47 +410,51 @@ static void render_wifi_setup_list(void) {
365 410
366 int y = 25; 411 int y = 25;
367 int visible = wifi_setup_visible_count(&s_wifi_setup); 412 int visible = wifi_setup_visible_count(&s_wifi_setup);
413 int item_w = screen_w - 20;
414 int max_y = screen_h - 40;
368 415
369 for (int i = 0; i < visible && y < 295; i++) { 416 for (int i = 0; i < visible && y < max_y; i++) {
370 const wifi_ap_info_t *ap = wifi_setup_get_visible(&s_wifi_setup, i); 417 const wifi_ap_info_t *ap = wifi_setup_get_visible(&s_wifi_setup, i);
371 if (!ap) break; 418 if (!ap) break;
372 419
373 char line[40];
374 int rssi_bars = 0; 420 int rssi_bars = 0;
375 if (ap->rssi >= -30) rssi_bars = 4; 421 if (ap->rssi >= -30) rssi_bars = 4;
376 else if (ap->rssi >= -50) rssi_bars = 3; 422 else if (ap->rssi >= -50) rssi_bars = 3;
377 else if (ap->rssi >= -70) rssi_bars = 2; 423 else if (ap->rssi >= -70) rssi_bars = 2;
378 else rssi_bars = 1; 424 else rssi_bars = 1;
379 425
380 snprintf(line, sizeof(line), "%s", ap->ssid); 426 axs15231b_fill_rect(10, y, item_w, 26, COLOR_DARKGRAY);
381 axs15231b_fill_rect(5, y, 260, 26, COLOR_DARKGRAY); 427 display_render_text(15, y + 4, ap->ssid, COLOR_WHITE, COLOR_DARKGRAY, 1);
382 display_render_text(10, y + 4, line, COLOR_WHITE, COLOR_DARKGRAY, 1);
383 428
429 int bar_x = 10 + item_w - 50;
384 for (int b = 0; b < rssi_bars; b++) { 430 for (int b = 0; b < rssi_bars; b++) {
385 axs15231b_fill_rect(270 + b * 8, y + 16 - (b + 1) * 4, 6, (b + 1) * 4, COLOR_GREEN); 431 axs15231b_fill_rect(bar_x + b * 10, y + 16 - (b + 1) * 4, 8, (b + 1) * 4, COLOR_GREEN);
386 } 432 }
387 433
388 if (ap->secured) { 434 if (ap->secured) {
389 const char *lock = "*"; 435 display_render_text(bar_x - 12, y + 4, "*", COLOR_YELLOW, COLOR_DARKGRAY, 1);
390 display_render_text(254, y + 4, lock, COLOR_YELLOW, COLOR_DARKGRAY, 1);
391 } 436 }
392 437
393 y += 30; 438 y += 30;
394 } 439 }
395 440
396 render_setup_button(5, 440, 50, 28); 441 int btn_y = screen_h - 30;
397 display_render_text(10, 444, "X", COLOR_WHITE, COLOR_DARKGRAY, 1); 442 axs15231b_fill_rect(10, btn_y, 50, 26, COLOR_DARKGRAY);
443 display_render_text(25, btn_y + 5, "X", COLOR_WHITE, COLOR_DARKGRAY, 1);
398 444
399 axs15231b_flush(); 445 axs15231b_flush();
400} 446}
401 447
402static void render_wifi_setup_password(void) { 448static void render_wifi_setup_password(void) {
449 int screen_w = axs15231b_get_width();
450 const kb_layout_t *kb = kb_get_layout();
403 axs15231b_fill_screen(COLOR_BG); 451 axs15231b_fill_screen(COLOR_BG);
404 452
405 display_render_text(10, 5, s_wifi_setup.selected_ssid, COLOR_CYAN, COLOR_BG, 1); 453 display_render_text(10, 5, s_wifi_setup.selected_ssid, COLOR_CYAN, COLOR_BG, 1);
406 display_render_text(10, 25, "Password:", COLOR_DIM, COLOR_BG, 1); 454 display_render_text(10, 25, "Password:", COLOR_DIM, COLOR_BG, 1);
407 455
408 axs15231b_fill_rect(10, 40, 220, 20, COLOR_DARKGRAY); 456 int field_w = screen_w - 80;
457 axs15231b_fill_rect(10, 40, field_w, 20, COLOR_DARKGRAY);
409 458
410 char masked[KB_INPUT_MAX + 1]; 459 char masked[KB_INPUT_MAX + 1];
411 if (s_kb_state.reveal) { 460 if (s_kb_state.reveal) {
@@ -413,30 +462,40 @@ static void render_wifi_setup_password(void) {
413 masked[sizeof(masked) - 1] = '\0'; 462 masked[sizeof(masked) - 1] = '\0';
414 } else { 463 } else {
415 int len = s_kb_state.cursor; 464 int len = s_kb_state.cursor;
416 if (len > 27) len = 27; 465 int max_chars = (field_w - 8) / 8;
466 if (len > max_chars) len = max_chars;
417 for (int i = 0; i < len; i++) masked[i] = '*'; 467 for (int i = 0; i < len; i++) masked[i] = '*';
418 masked[len] = '\0'; 468 masked[len] = '\0';
419 } 469 }
420 display_render_text(14, 43, masked, COLOR_WHITE, COLOR_DARKGRAY, 1); 470 display_render_text(14, 43, masked, COLOR_WHITE, COLOR_DARKGRAY, 1);
421 471
472 int eye_x = 10 + field_w + 5;
422 const char *eye_label = s_kb_state.reveal ? "H" : "S"; 473 const char *eye_label = s_kb_state.reveal ? "H" : "S";
423 axs15231b_fill_rect(235, 40, 24, 20, COLOR_GRAY); 474 axs15231b_fill_rect(eye_x, 40, 24, 20, COLOR_GRAY);
424 display_render_text(241, 43, eye_label, COLOR_WHITE, COLOR_GRAY, 1); 475 display_render_text(eye_x + 8, 43, eye_label, COLOR_WHITE, COLOR_GRAY, 1);
425 476
426 int kb_y = 70; 477 int kb_y = kb->start_y;
427 for (int row = 0; row < KB_ROW_COUNT; row++) { 478 for (int row = 0; row < kb->row_count; row++) {
428 const char *row_str; 479 const char *row_str;
429 int key_count = kb_get_row_keys(row, s_kb_state.layer, &row_str); 480 int key_count = kb_get_row_keys(row, s_kb_state.layer, &row_str);
430 int x_off = (row == 0) ? 5 : (row == 1) ? 14 : (row == 2) ? 23 : 5; 481 const char *tmp;
482 int total_keys = kb_get_row_keys(row, s_kb_state.layer, &tmp);
483 int x_off = (row == 0) ? (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2
484 : (row == 1) ? (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2 + kb->key_w / 2
485 : (row == 2) ? (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2 + kb->key_w
486 : (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2;
431 487
432 int cx = x_off; 488 int cx = x_off;
433 for (int col = 0; col < key_count; col++) { 489 for (int col = 0; col < key_count; col++) {
434 char c = row_str[col]; 490 char c = row_str[col];
435 int kw = 28; 491 int kw = kb->key_w;
436 if (row == 3) { 492 if (row == 3) {
437 if (col == 0) kw = 42; 493 int margin = (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2;
438 else if (col == key_count - 1) kw = 50; 494 int available = screen_w - margin * 2;
439 else kw = 168; 495 int side_w = (available - kb->key_gap) / 4;
496 if (col == 0) kw = side_w;
497 else if (col == key_count - 1) kw = side_w;
498 else kw = available - side_w * 2 - kb->key_gap * 2;
440 } 499 }
441 500
442 uint16_t bg = COLOR_DARKGRAY; 501 uint16_t bg = COLOR_DARKGRAY;
@@ -461,12 +520,12 @@ static void render_wifi_setup_password(void) {
461 label[0] = c; 520 label[0] = c;
462 } 521 }
463 522
464 axs15231b_fill_rect(cx, kb_y, kw, 30, bg); 523 axs15231b_fill_rect(cx, kb_y, kw, kb->key_h, bg);
465 int lw = 8; 524 int lw = 8;
466 display_render_text(cx + (kw - lw) / 2, kb_y + 8, label, fg, bg, 1); 525 display_render_text(cx + (kw - lw) / 2, kb_y + (kb->key_h - 8) / 2, label, fg, bg, 1);
467 cx += kw + 2; 526 cx += kw + kb->key_gap;
468 } 527 }
469 kb_y += 34; 528 kb_y += kb->key_h + kb->key_gap;
470 } 529 }
471 530
472 axs15231b_flush(); 531 axs15231b_flush();
@@ -492,53 +551,65 @@ static void render_wifi_setup_connecting(void) {
492 551
493static void render_wifi_setup_result(void) { 552static void render_wifi_setup_result(void) {
494 int screen_w = axs15231b_get_width(); 553 int screen_w = axs15231b_get_width();
554 int screen_h = axs15231b_get_height();
495 axs15231b_fill_screen(COLOR_BG); 555 axs15231b_fill_screen(COLOR_BG);
496 556
497 if (s_wifi_setup.state == SETUP_SUCCESS) { 557 if (s_wifi_setup.state == SETUP_SUCCESS) {
498 const char *msg = "Connected!"; 558 const char *msg = "Connected!";
499 int mw = strlen(msg) * 8 * 2; 559 int mw = strlen(msg) * 8 * 2;
500 display_render_text((screen_w - mw) / 2, 180, msg, COLOR_WHITE, COLOR_GREEN, 2); 560 display_render_text((screen_w - mw) / 2, screen_h / 2 - 40, msg, COLOR_WHITE, COLOR_GREEN, 2);
501 561
502 if (s_wifi_setup.connect_ip[0]) { 562 if (s_wifi_setup.connect_ip[0]) {
503 char ip_line[32]; 563 char ip_line[32];
504 snprintf(ip_line, sizeof(ip_line), "IP: %s", s_wifi_setup.connect_ip); 564 snprintf(ip_line, sizeof(ip_line), "IP: %s", s_wifi_setup.connect_ip);
505 int iw = strlen(ip_line) * 8; 565 int iw = strlen(ip_line) * 8;
506 display_render_text((screen_w - iw) / 2, 240, ip_line, COLOR_WHITE, COLOR_BG, 1); 566 display_render_text((screen_w - iw) / 2, screen_h / 2, ip_line, COLOR_WHITE, COLOR_BG, 1);
507 } 567 }
508 } else { 568 } else {
509 const char *msg = "Connection failed"; 569 const char *msg = "Connection failed";
510 int mw = strlen(msg) * 8 * 2; 570 int mw = strlen(msg) * 8 * 2;
511 display_render_text((screen_w - mw) / 2, 180, msg, COLOR_WHITE, COLOR_RED, 2); 571 display_render_text((screen_w - mw) / 2, screen_h / 2 - 40, msg, COLOR_WHITE, COLOR_RED, 2);
512 572
513 const char *hint = "Wrong password?"; 573 const char *hint = "Wrong password?";
514 int hw = strlen(hint) * 8; 574 int hw = strlen(hint) * 8;
515 display_render_text((screen_w - hw) / 2, 240, hint, COLOR_YELLOW, COLOR_BG, 1); 575 display_render_text((screen_w - hw) / 2, screen_h / 2, hint, COLOR_YELLOW, COLOR_BG, 1);
516 576
517 axs15231b_fill_rect(30, 280, 120, 30, COLOR_DARKGRAY); 577 int btn_y = screen_h / 2 + 30;
518 display_render_text(50, 288, "Retry", COLOR_WHITE, COLOR_DARKGRAY, 1); 578 int btn_w = (screen_w - 40) / 2;
519 axs15231b_fill_rect(170, 280, 120, 30, COLOR_DARKGRAY); 579 axs15231b_fill_rect(10, btn_y, btn_w, 30, COLOR_DARKGRAY);
520 display_render_text(180, 288, "Change", COLOR_WHITE, COLOR_DARKGRAY, 1); 580 display_render_text(10 + (btn_w - 40) / 2, btn_y + 8, "Retry", COLOR_WHITE, COLOR_DARKGRAY, 1);
581 axs15231b_fill_rect(20 + btn_w, btn_y, btn_w, 30, COLOR_DARKGRAY);
582 display_render_text(20 + btn_w + (btn_w - 50) / 2, btn_y + 8, "Change", COLOR_WHITE, COLOR_DARKGRAY, 1);
521 } 583 }
522 584
523 axs15231b_flush(); 585 axs15231b_flush();
524} 586}
525 587
526static void handle_wifi_setup_touch(uint16_t tx, uint16_t ty) { 588static void handle_wifi_setup_touch(uint16_t tx, uint16_t ty) {
589 int screen_w = axs15231b_get_width();
590 int screen_h = axs15231b_get_height();
591
527 switch (s_wifi_setup.state) { 592 switch (s_wifi_setup.state) {
528 case SETUP_SCAN: 593 case SETUP_SCAN:
529 break; 594 break;
530 595
531 case SETUP_LIST: { 596 case SETUP_LIST: {
532 if (touch_in_rect(tx, ty, 5, 440, 50, 28)) { 597 int btn_y = screen_h - 30;
598 if (touch_in_rect(tx, ty, 10, btn_y, 50, 26)) {
599 highlight_rect(10, btn_y, 50, 26);
533 wifi_setup_handle_cancel(&s_wifi_setup); 600 wifi_setup_handle_cancel(&s_wifi_setup);
534 s_wifi_setup_active = false; 601 s_wifi_setup_active = false;
602 exit_wifi_setup_rotation();
535 s_state = DISPLAY_ERROR; 603 s_state = DISPLAY_ERROR;
536 return; 604 return;
537 } 605 }
606 int item_w = screen_w - 20;
538 int y = 25; 607 int y = 25;
608 int max_y = screen_h - 40;
539 int visible = wifi_setup_visible_count(&s_wifi_setup); 609 int visible = wifi_setup_visible_count(&s_wifi_setup);
540 for (int i = 0; i < visible; i++) { 610 for (int i = 0; i < visible && y < max_y; i++) {
541 if (touch_in_rect(tx, ty, 5, y, 300, 26)) { 611 if (touch_in_rect(tx, ty, 10, y, item_w, 26)) {
612 highlight_rect(10, y, item_w, 26);
542 wifi_setup_handle_select(&s_wifi_setup, i); 613 wifi_setup_handle_select(&s_wifi_setup, i);
543 kb_state_init(&s_kb_state); 614 kb_state_init(&s_kb_state);
544 return; 615 return;
@@ -549,12 +620,17 @@ static void handle_wifi_setup_touch(uint16_t tx, uint16_t ty) {
549 } 620 }
550 621
551 case SETUP_PASSWORD: { 622 case SETUP_PASSWORD: {
552 if (touch_in_rect(tx, ty, 235, 40, 24, 20)) { 623 int field_w = screen_w - 80;
624 int eye_x = 10 + field_w + 5;
625 if (touch_in_rect(tx, ty, eye_x, 40, 24, 20)) {
553 s_kb_state.reveal = !s_kb_state.reveal; 626 s_kb_state.reveal = !s_kb_state.reveal;
554 return; 627 return;
555 } 628 }
556 kb_result_t r = kb_hit_test(tx, ty, s_kb_state.layer); 629 kb_result_t r = kb_hit_test(tx, ty, s_kb_state.layer);
557 if (r.action != KB_ACTION_NONE) { 630 if (r.action != KB_ACTION_NONE) {
631 axs15231b_fill_rect(tx - 20, ty - 20, 40, 40, COLOR_HIGHLIGHT);
632 axs15231b_flush();
633 vTaskDelay(pdMS_TO_TICKS(60));
558 if (r.action == KB_ACTION_DONE && s_kb_state.cursor > 0) { 634 if (r.action == KB_ACTION_DONE && s_kb_state.cursor > 0) {
559 wifi_setup_handle_connect(&s_wifi_setup); 635 wifi_setup_handle_connect(&s_wifi_setup);
560 wifi_config_t wifi_cfg = {0}; 636 wifi_config_t wifi_cfg = {0};
@@ -578,15 +654,20 @@ static void handle_wifi_setup_touch(uint16_t tx, uint16_t ty) {
578 case SETUP_SUCCESS: { 654 case SETUP_SUCCESS: {
579 wifi_setup_handle_cancel(&s_wifi_setup); 655 wifi_setup_handle_cancel(&s_wifi_setup);
580 s_wifi_setup_active = false; 656 s_wifi_setup_active = false;
657 exit_wifi_setup_rotation();
581 s_state = DISPLAY_READY; 658 s_state = DISPLAY_READY;
582 return; 659 return;
583 } 660 }
584 661
585 case SETUP_FAILED: { 662 case SETUP_FAILED: {
586 if (touch_in_rect(tx, ty, 30, 280, 120, 30)) { 663 int btn_y = screen_h / 2 + 30;
664 int btn_w = (screen_w - 40) / 2;
665 if (touch_in_rect(tx, ty, 10, btn_y, btn_w, 30)) {
666 highlight_rect(10, btn_y, btn_w, 30);
587 wifi_setup_handle_retry(&s_wifi_setup); 667 wifi_setup_handle_retry(&s_wifi_setup);
588 kb_state_init(&s_kb_state); 668 kb_state_init(&s_kb_state);
589 } else if (touch_in_rect(tx, ty, 170, 280, 120, 30)) { 669 } else if (touch_in_rect(tx, ty, 20 + btn_w, btn_y, btn_w, 30)) {
670 highlight_rect(20 + btn_w, btn_y, btn_w, 30);
590 wifi_setup_handle_change_network(&s_wifi_setup); 671 wifi_setup_handle_change_network(&s_wifi_setup);
591 } 672 }
592 break; 673 break;
@@ -618,6 +699,7 @@ static void display_task(void *pvParameters) {
618 state = s_state; 699 state = s_state;
619 } else if (state == DISPLAY_ERROR || state == DISPLAY_READY) { 700 } else if (state == DISPLAY_ERROR || state == DISPLAY_READY) {
620 if (touch_in_rect(tp.x, tp.y, SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H)) { 701 if (touch_in_rect(tp.x, tp.y, SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H)) {
702 enter_wifi_setup_rotation();
621 s_wifi_setup_active = true; 703 s_wifi_setup_active = true;
622 wifi_setup_init(&s_wifi_setup); 704 wifi_setup_init(&s_wifi_setup);
623 kb_state_init(&s_kb_state); 705 kb_state_init(&s_kb_state);
@@ -710,6 +792,7 @@ static void display_task(void *pvParameters) {
710 vTaskDelay(pdMS_TO_TICKS(3000)); 792 vTaskDelay(pdMS_TO_TICKS(3000));
711 wifi_setup_handle_cancel(&s_wifi_setup); 793 wifi_setup_handle_cancel(&s_wifi_setup);
712 s_wifi_setup_active = false; 794 s_wifi_setup_active = false;
795 exit_wifi_setup_rotation();
713 s_state = DISPLAY_READY; 796 s_state = DISPLAY_READY;
714 break; 797 break;
715 case SETUP_FAILED: 798 case SETUP_FAILED:
@@ -804,6 +887,7 @@ void display_notify_wifi_disconnected(void) {
804 887
805void display_enter_wifi_setup(void) { 888void display_enter_wifi_setup(void) {
806 if (!s_initialized) return; 889 if (!s_initialized) return;
890 enter_wifi_setup_rotation();
807 s_wifi_setup_active = true; 891 s_wifi_setup_active = true;
808 wifi_setup_init(&s_wifi_setup); 892 wifi_setup_init(&s_wifi_setup);
809 kb_state_init(&s_kb_state); 893 kb_state_init(&s_kb_state);
diff --git a/main/keyboard.c b/main/keyboard.c
index 33365de..d16135f 100644
--- a/main/keyboard.c
+++ b/main/keyboard.c
@@ -28,6 +28,15 @@ static const char *s_numsym[] = {
28#define CTRL_DONE '\004' 28#define CTRL_DONE '\004'
29#define CTRL_BS '\b' 29#define CTRL_BS '\b'
30 30
31static kb_layout_t s_layout = {
32 .key_w = 28,
33 .key_h = 36,
34 .key_gap = 2,
35 .start_y = 70,
36 .screen_w = 320,
37 .row_count = 4,
38};
39
31void kb_state_init(kb_state_t *st) { 40void kb_state_init(kb_state_t *st) {
32 if (!st) return; 41 if (!st) return;
33 memset(st, 0, sizeof(*st)); 42 memset(st, 0, sizeof(*st));
@@ -35,6 +44,14 @@ void kb_state_init(kb_state_t *st) {
35 st->reveal = false; 44 st->reveal = false;
36} 45}
37 46
47void kb_set_layout(const kb_layout_t *layout) {
48 if (layout) s_layout = *layout;
49}
50
51const kb_layout_t *kb_get_layout(void) {
52 return &s_layout;
53}
54
38static const char **get_layer(kb_layer_t layer) { 55static const char **get_layer(kb_layer_t layer) {
39 switch (layer) { 56 switch (layer) {
40 case KB_ALPHA_UPPER: return s_alpha_upper; 57 case KB_ALPHA_UPPER: return s_alpha_upper;
@@ -43,12 +60,8 @@ static const char **get_layer(kb_layer_t layer) {
43 } 60 }
44} 61}
45 62
46static int key_is_ctrl(char c) {
47 return c == CTRL_SHIFT || c == CTRL_LAYER || c == CTRL_SPACE || c == CTRL_DONE || c == CTRL_BS;
48}
49
50int kb_get_row_keys(int row, kb_layer_t layer, const char **keys_out) { 63int kb_get_row_keys(int row, kb_layer_t layer, const char **keys_out) {
51 if (row < 0 || row >= KB_ROW_COUNT) { 64 if (row < 0 || row >= s_layout.row_count) {
52 *keys_out = NULL; 65 *keys_out = NULL;
53 return 0; 66 return 0;
54 } 67 }
@@ -58,46 +71,59 @@ int kb_get_row_keys(int row, kb_layer_t layer, const char **keys_out) {
58 return (int)strlen(row_str); 71 return (int)strlen(row_str);
59} 72}
60 73
61static int row_x_offset(int row) { 74static int row_x_offset(int row, int total_keys) {
75 int kw = s_layout.key_w;
76 int gap = s_layout.key_gap;
77 int total_w = total_keys * kw + (total_keys - 1) * gap;
78 int margin = (s_layout.screen_w - total_w) / 2;
79 if (margin < 2) margin = 2;
62 switch (row) { 80 switch (row) {
63 case 0: return 5; 81 case 0: return margin;
64 case 1: return 14; 82 case 1: return margin + kw / 2;
65 case 2: return 23; 83 case 2: return margin + kw;
66 case 3: return 5; 84 case 3: return margin;
67 default: return 0; 85 default: return margin;
68 } 86 }
69} 87}
70 88
71static int key_width_at(int row, int col, int total_keys) { 89static int key_width_at(int row, int col, int total_keys) {
72 (void)col; 90 int kw = s_layout.key_w;
73 if (row == 3) { 91 if (row == 3) {
74 if (col == 0) return 42; 92 int gap = s_layout.key_gap;
75 if (col == total_keys - 1) return 50; 93 int margin = row_x_offset(3, total_keys);
76 return 168; 94 int available = s_layout.screen_w - margin * 2;
95 int side_w = (available - gap) / 4;
96 if (col == 0) return side_w;
97 if (col == total_keys - 1) return side_w;
98 return available - side_w * 2 - gap * 2;
77 } 99 }
78 return KB_KEY_W; 100 return kw;
79} 101}
80 102
81kb_result_t kb_hit_test(int tx, int ty, kb_layer_t layer) { 103kb_result_t kb_hit_test(int tx, int ty, kb_layer_t layer) {
82 kb_result_t result = {KB_ACTION_NONE, 0}; 104 kb_result_t result = {KB_ACTION_NONE, 0};
105 int sy = s_layout.start_y;
106 int kw = s_layout.key_w;
107 int kh = s_layout.key_h;
108 int gap = s_layout.key_gap;
83 109
84 if (ty < KB_START_Y || ty >= KB_START_Y + KB_ROW_COUNT * (KB_KEY_H + KB_KEY_GAP)) { 110 if (ty < sy || ty >= sy + s_layout.row_count * (kh + gap)) {
85 return result; 111 return result;
86 } 112 }
87 113
88 int row = (ty - KB_START_Y) / (KB_KEY_H + KB_KEY_GAP); 114 int row = (ty - sy) / (kh + gap);
89 if (row < 0 || row >= KB_ROW_COUNT) return result; 115 if (row < 0 || row >= s_layout.row_count) return result;
90 116
91 const char *row_str; 117 const char *row_str;
92 int total_keys = kb_get_row_keys(row, layer, &row_str); 118 int total_keys = kb_get_row_keys(row, layer, &row_str);
93 if (total_keys == 0) return result; 119 if (total_keys == 0) return result;
94 120
95 int x_off = row_x_offset(row); 121 int x_off = row_x_offset(row, total_keys);
96 int cx = x_off; 122 int cx = x_off;
97 123
98 for (int col = 0; col < total_keys; col++) { 124 for (int col = 0; col < total_keys; col++) {
99 int kw = key_width_at(row, col, total_keys); 125 int key_w = key_width_at(row, col, total_keys);
100 if (tx >= cx && tx < cx + kw) { 126 if (tx >= cx && tx < cx + key_w) {
101 char c = row_str[col]; 127 char c = row_str[col];
102 if (c == CTRL_SHIFT) { 128 if (c == CTRL_SHIFT) {
103 result.action = KB_ACTION_SHIFT; 129 result.action = KB_ACTION_SHIFT;
@@ -116,7 +142,7 @@ kb_result_t kb_hit_test(int tx, int ty, kb_layer_t layer) {
116 } 142 }
117 return result; 143 return result;
118 } 144 }
119 cx += kw + KB_KEY_GAP; 145 cx += key_w + gap;
120 } 146 }
121 147
122 return result; 148 return result;
diff --git a/main/keyboard.h b/main/keyboard.h
index 495c499..9c4118f 100644
--- a/main/keyboard.h
+++ b/main/keyboard.h
@@ -5,11 +5,6 @@
5#include <stdbool.h> 5#include <stdbool.h>
6 6
7#define KB_INPUT_MAX 64 7#define KB_INPUT_MAX 64
8#define KB_KEY_W 28
9#define KB_KEY_H 36
10#define KB_KEY_GAP 2
11#define KB_ROW_COUNT 4
12#define KB_START_Y 70
13 8
14typedef enum { 9typedef enum {
15 KB_ALPHA_LOWER, 10 KB_ALPHA_LOWER,
@@ -28,18 +23,29 @@ typedef enum {
28} kb_action_t; 23} kb_action_t;
29 24
30typedef struct { 25typedef struct {
31 kb_action_t action;
32 char ch;
33} kb_result_t;
34
35typedef struct {
36 char input[KB_INPUT_MAX + 1]; 26 char input[KB_INPUT_MAX + 1];
37 int cursor; 27 int cursor;
38 bool reveal; 28 bool reveal;
39 kb_layer_t layer; 29 kb_layer_t layer;
40} kb_state_t; 30} kb_state_t;
41 31
32typedef struct {
33 kb_action_t action;
34 char ch;
35} kb_result_t;
36
37typedef struct {
38 int key_w;
39 int key_h;
40 int key_gap;
41 int start_y;
42 int screen_w;
43 int row_count;
44} kb_layout_t;
45
42void kb_state_init(kb_state_t *st); 46void kb_state_init(kb_state_t *st);
47void kb_set_layout(const kb_layout_t *layout);
48const kb_layout_t *kb_get_layout(void);
43int kb_get_row_keys(int row, kb_layer_t layer, const char **keys_out); 49int kb_get_row_keys(int row, kb_layer_t layer, const char **keys_out);
44kb_result_t kb_hit_test(int tx, int ty, kb_layer_t layer); 50kb_result_t kb_hit_test(int tx, int ty, kb_layer_t layer);
45void kb_apply(kb_state_t *st, kb_result_t result); 51void kb_apply(kb_state_t *st, kb_result_t result);
diff --git a/main/touch.c b/main/touch.c
index 5a1eec9..a28d13e 100644
--- a/main/touch.c
+++ b/main/touch.c
@@ -11,6 +11,7 @@ static const char *TAG = "touch";
11static i2c_master_bus_handle_t s_bus = NULL; 11static i2c_master_bus_handle_t s_bus = NULL;
12static i2c_master_dev_handle_t s_dev = NULL; 12static i2c_master_dev_handle_t s_dev = NULL;
13static bool s_initialized = false; 13static bool s_initialized = false;
14static int s_rotation = 0;
14 15
15static const uint8_t s_read_cmd[11] = { 16static const uint8_t s_read_cmd[11] = {
16 0xb5, 0xab, 0xa5, 0x5a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 17 0xb5, 0xab, 0xa5, 0x5a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00
@@ -115,9 +116,33 @@ bool touch_read(touch_point_t *pt) {
115 } 116 }
116 117
117 touch_parse_raw(data, pt); 118 touch_parse_raw(data, pt);
119
120 if (pt->touched && s_rotation != 0) {
121 uint16_t raw_x = pt->x;
122 uint16_t raw_y = pt->y;
123 switch (s_rotation) {
124 case 1:
125 pt->x = raw_y;
126 pt->y = TOUCH_MAX_X - raw_x;
127 break;
128 case 2:
129 pt->x = TOUCH_MAX_X - raw_x;
130 pt->y = TOUCH_MAX_Y - raw_y;
131 break;
132 case 3:
133 pt->x = TOUCH_MAX_Y - raw_y;
134 pt->y = raw_x;
135 break;
136 }
137 }
138
118 return pt->touched; 139 return pt->touched;
119} 140}
120 141
142void touch_set_rotation(int rotation) {
143 s_rotation = rotation;
144}
145
121void touch_deinit(void) { 146void touch_deinit(void) {
122 if (s_dev) { 147 if (s_dev) {
123 i2c_master_bus_rm_device(s_dev); 148 i2c_master_bus_rm_device(s_dev);
diff --git a/main/touch.h b/main/touch.h
index a4a5aa4..b9e3ccd 100644
--- a/main/touch.h
+++ b/main/touch.h
@@ -22,6 +22,7 @@ typedef struct {
22esp_err_t touch_init(void); 22esp_err_t touch_init(void);
23bool touch_read(touch_point_t *pt); 23bool touch_read(touch_point_t *pt);
24void touch_deinit(void); 24void touch_deinit(void);
25void touch_set_rotation(int rotation);
25 26
26void touch_parse_raw(const uint8_t *data, touch_point_t *pt); 27void touch_parse_raw(const uint8_t *data, touch_point_t *pt);
27 28
diff --git a/tests/unit/test_keyboard b/tests/unit/test_keyboard
new file mode 100755
index 0000000..61cc9f5
--- /dev/null
+++ b/tests/unit/test_keyboard
Binary files differ
diff --git a/tests/unit/test_keyboard.c b/tests/unit/test_keyboard.c
index e069f28..81ca328 100644
--- a/tests/unit/test_keyboard.c
+++ b/tests/unit/test_keyboard.c
@@ -41,37 +41,41 @@ int main(void)
41 kb_result_t r = kb_hit_test(160, 10, KB_ALPHA_LOWER); 41 kb_result_t r = kb_hit_test(160, 10, KB_ALPHA_LOWER);
42 ASSERT(r.action == KB_ACTION_NONE, "Touch above keyboard = NONE"); 42 ASSERT(r.action == KB_ACTION_NONE, "Touch above keyboard = NONE");
43 43
44 r = kb_hit_test(160, KB_START_Y + KB_ROW_COUNT * (KB_KEY_H + KB_KEY_GAP) + 10, KB_ALPHA_LOWER); 44 r = kb_hit_test(160, 70 + 4 * (36 + 2) + 10, KB_ALPHA_LOWER);
45 ASSERT(r.action == KB_ACTION_NONE, "Touch below keyboard = NONE"); 45 ASSERT(r.action == KB_ACTION_NONE, "Touch below keyboard = NONE");
46 } 46 }
47 47
48 { 48 {
49 int mid_x = 5 + KB_KEY_W / 2; 49 int margin_r0 = (320 - (10 * 28 + 9 * 2)) / 2;
50 int mid_y = KB_START_Y + KB_KEY_H / 2; 50 int mid_x = margin_r0 + 28 / 2;
51 int mid_y = 70 + 36 / 2;
51 kb_result_t r = kb_hit_test(mid_x, mid_y, KB_ALPHA_LOWER); 52 kb_result_t r = kb_hit_test(mid_x, mid_y, KB_ALPHA_LOWER);
52 ASSERT(r.action == KB_ACTION_CHAR, "Row 0 first key is a char"); 53 ASSERT(r.action == KB_ACTION_CHAR, "Row 0 first key is a char");
53 ASSERT_EQ_INT('q', r.ch, "Row 0 first key = 'q'"); 54 ASSERT_EQ_INT('q', r.ch, "Row 0 first key = 'q'");
54 } 55 }
55 56
56 { 57 {
57 int x = 5 + KB_KEY_W + KB_KEY_GAP + KB_KEY_W / 2; 58 int margin_r0 = (320 - (10 * 28 + 9 * 2)) / 2;
58 int y = KB_START_Y + KB_KEY_H / 2; 59 int x = margin_r0 + 28 + 2 + 28 / 2;
60 int y = 70 + 36 / 2;
59 kb_result_t r = kb_hit_test(x, y, KB_ALPHA_LOWER); 61 kb_result_t r = kb_hit_test(x, y, KB_ALPHA_LOWER);
60 ASSERT(r.action == KB_ACTION_CHAR, "Row 0 second key is a char"); 62 ASSERT(r.action == KB_ACTION_CHAR, "Row 0 second key is a char");
61 ASSERT_EQ_INT('w', r.ch, "Row 0 second key = 'w'"); 63 ASSERT_EQ_INT('w', r.ch, "Row 0 second key = 'w'");
62 } 64 }
63 65
64 { 66 {
65 int y_row1 = KB_START_Y + (KB_KEY_H + KB_KEY_GAP) + KB_KEY_H / 2; 67 int margin_r1 = (320 - (9 * 28 + 8 * 2)) / 2;
66 int x_row1 = 14 + KB_KEY_W / 2; 68 int y_row1 = 70 + (36 + 2) + 36 / 2;
69 int x_row1 = margin_r1 + 28 / 2 + 28 / 2;
67 kb_result_t r = kb_hit_test(x_row1, y_row1, KB_ALPHA_LOWER); 70 kb_result_t r = kb_hit_test(x_row1, y_row1, KB_ALPHA_LOWER);
68 ASSERT(r.action == KB_ACTION_CHAR, "Row 1 first key is a char"); 71 ASSERT(r.action == KB_ACTION_CHAR, "Row 1 first key is a char");
69 ASSERT_EQ_INT('a', r.ch, "Row 1 first key = 'a'"); 72 ASSERT_EQ_INT('a', r.ch, "Row 1 first key = 'a'");
70 } 73 }
71 74
72 { 75 {
73 int y_row2 = KB_START_Y + 2 * (KB_KEY_H + KB_KEY_GAP) + KB_KEY_H / 2; 76 int margin_r2 = (320 - (9 * 28 + 8 * 2)) / 2 + 28;
74 int x_row2 = 23 + 42 / 2; 77 int y_row2 = 70 + 2 * (36 + 2) + 36 / 2;
78 int x_row2 = margin_r2 + 28 / 2;
75 kb_result_t r = kb_hit_test(x_row2, y_row2, KB_ALPHA_LOWER); 79 kb_result_t r = kb_hit_test(x_row2, y_row2, KB_ALPHA_LOWER);
76 ASSERT(r.action == KB_ACTION_SHIFT, "Row 2 first key = SHIFT"); 80 ASSERT(r.action == KB_ACTION_SHIFT, "Row 2 first key = SHIFT");
77 } 81 }
diff --git a/tests/unit/test_touch b/tests/unit/test_touch
new file mode 100755
index 0000000..43c8790
--- /dev/null
+++ b/tests/unit/test_touch
Binary files differ
diff --git a/tests/unit/test_wifi_setup b/tests/unit/test_wifi_setup
new file mode 100755
index 0000000..aa0e0b4
--- /dev/null
+++ b/tests/unit/test_wifi_setup
Binary files differ