Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
|
Be noted that this is not a sponsored project and is shared for non-profit purposes. The watch is named "Saiko(u)" (Japanese さいこう, superb/the best) simply as a pun for Seiko.The Idea
I know, I know - I've made too many Internet-connected clocks already, and I have multiple ones placed at home and office (including this one that I am using in my bedroom for some years). But this is what you'll get to fight boredom during the slow business days in the office.
I bought the round GC9A01A 1.28" RGB TFT LCD round display (resolution 240x240) a while ago. After some initial test, I was surprised that the color actually looks pretty good. Unlike the common ST7735s and ILI9341s which have a noticeable while shine of the backlight, the GC9A01A looks closer to OLED displays, and can still be seen clearly from slightly wider angles.
Wiring:
- GND -> GND
- VCC -> 3.3V
- SCL -> 18 (SCK)
- SDA -> 23 (MOSI)
- RES (RST) -> 22
- DC -> 21
- CS -> 5
- BLK (backlight) -> 3.3V
The nature of the round display is, of course, suitable for creating clocks, watches or any sort of analog instrument display, and many people have already done so. I wanted to do a little more than that; I'd like to create a watch face in a completely (or almost at least) code-generated way without using bitmaps, and the watch has to work almost like an actual mechanical watch, with hands moving in a more realistic way, but also can be synced to the NTP service to stay accurate at all times.
The MakingBelow is the list of Arduino-based libraries I used:
You also have to, of course, install the ESP32 support in your Arduino IDE board manager. I am using the new Arduino IDE 2.x.
One issue I've encountered early on was how to draw a smooth animation on the TFT. Adafruit's GFX library does provide a way to create a canvas or a buffer: you draw first on the buffer, then overwrite the buffer onto the display at once, which effectively eliminate the flicker.
But there is a catch: I cannot create a buffer as big as the TFT.
I think it is an memory limitation, even though I tried to use an ESP32 (I used my ESP32 PICO D4 since it's the smallest one I had). The buffer can only be as big as a little more than 200x200. This means I can only re-draw the center part of the display.
And then I had the idea. I decided to draw a diver watch, which has a outer ring called bezel. The watch hands never touch the bezel, so it's an area that can be drawn only once. Of course, for the bezel I have to draw both on TFT and the buffer, since the buffer is rectangular and covers parts of the bezel.
As I mentioned before, I want to draw the watch face with code instead of using bitmaps. For that I have to combine the basic functionalities in the Adafruit GFX library to create complex patterns, including using two triangles to create an angled rectangle or thick line. Many watch face elements are drawn twice to create the illusion of "outlines" (a slightly bigger background and a foreground on top of it).
The watch face and dial design are based on the Seiko 5 Sports SRPK33K1 diver (not really a diver; more like a casual sport watch). The size of the GC9A01A display (1.28" or 32 mm) and the PCB around it makes it not too smaller from the actual watch case size (38 mm).
I am not trying to replicate the exact design though, because
- The resolution is simply not high enough for creating such subtle details.
- I prefer to draw everything as much as in the customizable way by code, including the fonts, instead of using fixed bitmaps.
- And more importantly...I don't want to get sued by Seiko, that's why.
I don't have the SRPK33K1 but I do have a black Seiko 5 Sports flieger, which uses the same 4R36 caliber. One important detail to imitate is to make the second hand of my "watch" to move 6 times per second like the real one (the 4R36 operates at 3 Hz or 6 vibrations per second).
The only parts I had to use bitmaps are the numbers around the bezel (generated using image2cpp), since it's impossible to rotate them as such angles using the Adafruit library. All other elements are code-based and can be customized to some extent.
I did consider to reproduce the behavior of the rotating dates of the 4R36 caliber, or at least move the dates vertically in and out the display area to simulate the effect - the day would slowly rotate between 23:00 to 0:00 and the weekday would rotate twice (there are two languages on it), first between 0:00 to 1:00 then at 3:00 to 4:00 (I think?).
In the end I figured this is way too complex and unnecessary, since the rotating are designed to take place after you go to sleep, and mechanical watches have no other way to do so (which also requires re-calibration when the month is not ended with 31st). No one with the sane mind will try to wait for the date rotating on an electronic fake analog watch. So for now the dates change directly when the queried NTP time does.Display Box
There are people making and selling the so-called open sourced ESP32 watches, although I don't want to spend the cost and time to buy one. I decided to make a watch display box instead.
I bought a wooden box with glass panels on either side, and a paper gift box to put the "watch" in it. I also took off the steel bracelet of one of my dad's cheap watch (found after he passed away late last year) and hooked them around the display by wire. Not pretty, but passable.
Configure The WatchThe code provides a few options:
- If DEMO_MODE is set to true, the watch will not connect to WiFi and shows a fixed date/time, in the similar manner that can be found in real watch ads.
- If BENCHMARK is set to true, the serial port will print the time it needs to draw one cycle. For my test it only took a little more than 20 ms (the second hand needs ~167 ms per cycle to maintain the 6-vibration illusion). (You can in fact change the vibration number via SECOND_HAND_VIBRATION.)
- You can also change the displayed watch logo and description name via LOGO_NAME and DESCRIPTION. I'm afraid there are no smaller fonts unless you try to compile ones yourself.
- NTP_HOUR_OFFSET is your timezone hour offset, for example, 8 = UTC+8.
- Finally, create a file secrets.h alongside the Arduino script and enter your WiFi SSID and password:
#define SECRET_SSID "your_WiFi_ssid"
#define SECRET_PASS "your_WiFi_password"
For now I'd like to keep this display as simple as possible without extra devices. Although there are a few fun thinks I can think of:
- The backlight LED pin on the TFT controls the brightness level. So you can hook up a potentiometer to turn it "on" or "off". The display is not that bright though, and I can simply unplug the power cable to achieve the same thing.
- I also considered to add a light sensor or switch to make the watch to show "night mode" (the dots and hands glows in green like the real watch exposed under sunlight long enough). Again, this is complicated and not necessarily look good (how do you simulate the ambient light from the glow on the watch face?) compared to the present result.
The script had been improved to print out time in serial port as well as reducing NTP updates to every minute to avoid potential bottlenecks.
#include "secrets.h" // imports SECRET_SSID and SECRET_PASS
/*
Create a <secrets.h> in the same project:
#define SECRET_SSID "your_WiFi_ssid"
#define SECRET_PASS "your_WiFi_password"
*/
// user configuration
#define DEMO_MODE false // demo display mode (fixed time, will not connect wifi)
#define BENCHMARK false // print dial drawing time (ms) into serial port
#define BAUD_RATE 115200 // serial port baud rate
#define CONNECT_TIMEOUT 30 // WiFi connection timeout (seconds)
#define NTP_SERVER "pool.ntp.org" // NTP server
#define NTP_INTERVAL 60 // interval for querying NTP server (minutes)
#define NTP_HOUR_OFFSET 0 // timezone offset (hours; 1 = +1, -1 = -1)
#define TFT_CS 5 // GC9A01A CS pin
#define TFT_DC 21 // GC9A01A DC pin
#define TFT_RST 22 // GC9A01A reset pin
#define TFT_WIDTH 240 // GC9A01A width
#define TFT_HEIGHT 240 // GC9A01A height
#define TFT_ROTATION 0 // GC9A01A rotation (0~3)
// import dependencies
#include <math.h>
#include <SPI.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_GC9A01A.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeSerifItalic9pt7b.h>
#include <Fonts/TomThumb.h>
// configuration for the watch face
// dial specs
#define DIAL_WIDTH 192
#define DIAL_HEIGHT 192
#define BEZEL_TICK_W 6
#define BEZEL_TICK_PAD 5
#define BEZEL_NOON_PAD 5
#define BEZEL_DOT_SELF_R 2
#define BEZEL_DOT_POS_R_P 0.3
#define DIAL_RING_W 10
#define INNER_TICK_W 6
#define INNER_TICK_NOON_W 6
#define INNER_TICK_PAD 3
#define ROUND_DOT_POS_R_P 0.84
#define ROUND_DOT_SELF_R 6
#define ROUND_DOT_BORDER_W 2
#define LOGO_NAME "SAIKO"
#define LOGO_POS_R_P 0.35
#define DESCRIPTION "IoTomatic"
#define DESCRIPTION_POS_R_P 0.38
#define DATE_W 18
#define DATE_H 14
#define WEEK_W 28
#define WEEK_H 14
#define HANDS_AXIS_R 1
#define HOUR_MIN_HAND_BORDER 2
#define HOUR_MIN_HAND_POINT_W 3
#define HOUR_MIN_HAND_POINT_H 4
#define HOUR_HAND_A_W 16
#define HOUR_HAND_A_H 6
#define HOUR_HAND_B_ALT 8
#define HOUR_HAND_C_W 24
#define HOUR_HAND_C_H 12
#define HOUR_HAND_D_BASE 16
#define HOUR_HAND_D_ALT 10
#define MIN_HAND_A_ALT 6
#define MIN_HAND_B_W 18
#define MIN_HAND_B_H 18
#define MIN_HAND_B_OPPS_R 8
#define MIN_HAND_C_W 14
#define MIN_HAND_C_H 14
#define MIN_HAND_D_W 30
#define MIN_HAND_D_H 8
#define MIN_HAND_E_BASE 24
#define MIN_HAND_E_ALT 22
#define MIN_HAND_F_ALT 8
#define SECOND_HAND_VIBRATION 6 // 3 Hz for second hand
#define SECOND_HAND_R_P 0.92
#define SECOND_HAND_BALANCE_R_P 0.42
#define SECOND_HAND_BALANCE_DOT_SELF_R 5
#define SECOND_HAND_BALANCE_DOT_BORDER_W 1
#define SECOND_HAND_BASE_R 5
// dial colors
#define BEZEL_COLOR 0x4E4B47
#define DIAL_COLOR 0x0ABAB5
#define DIAL_RING_COLOR 0x089E9A
#define BEZEL_TICK_COLOR 0xD3D3D3
#define INNER_TICK_COLOR 0x4E4B47
#define ROUND_DOT_COLOR 0xFAEED8
#define ROUND_DOT_BORDER 0x6A6B6E
#define LOGO_COLOR 0x56534E
#define DESCRIPTION_COLOR 0x675b5A
#define DATE_COLOR 0x4E4B47
#define DATE_SUN_COLOR 0xD90000
#define HOUR_MIN_HAND_BORDER_COLOR 0xBE8A0B
#define HOUR_MIN_HAND_FRONT_COLOR 0xFAEED8
#define SECOND_HAND_COLOR 0x373130
#define SECOND_HAND_BALANCE_COLOR 0xFAEED8
#define SECOND_HAND_AXIS_COLOR 0xB5B6B8
// calculated specs
const uint16_t TFT_CENTER_X = round(TFT_WIDTH / 2);
const uint16_t TFT_CENTER_Y = round(TFT_HEIGHT / 2);
const uint16_t TFT_R = TFT_CENTER_X < TFT_CENTER_Y ? TFT_CENTER_X : TFT_CENTER_Y;
const uint16_t DIAL_CENTER_X = round(DIAL_WIDTH / 2);
const uint16_t DIAL_CENTER_Y = round(DIAL_HEIGHT / 2);
const uint16_t DIAL_RING_R = DIAL_CENTER_X < DIAL_CENTER_Y ? DIAL_CENTER_X : DIAL_CENTER_Y;
const uint16_t BEZEL_R = TFT_R;
const uint16_t BEZEL_W = TFT_R - DIAL_RING_R;
const uint16_t DIAL_INNER_R = DIAL_RING_R - DIAL_RING_W;
const uint16_t ROUND_DOT_R = round(DIAL_INNER_R * ROUND_DOT_POS_R_P);
// '10' on bezel ring, 23x26px
const unsigned char bitmap_bezel_10[] PROGMEM = {
0xff, 0x7f, 0xfe, 0xfe, 0x07, 0xfe, 0xfe, 0x03, 0xfe, 0xfe, 0x01, 0xfe, 0xff, 0x81, 0xfe, 0xfe,
0x01, 0xfe, 0xf8, 0x07, 0xfe, 0xe0, 0x1f, 0xfe, 0xc0, 0x7f, 0xfe, 0x80, 0xff, 0xfe, 0x83, 0xff,
0xfe, 0xcf, 0xfe, 0x1e, 0xff, 0xf8, 0x0e, 0xff, 0xe0, 0x06, 0xff, 0xc0, 0x02, 0xff, 0x01, 0xc2,
0xfe, 0x03, 0xe0, 0xfe, 0x0f, 0xe0, 0xfe, 0x3f, 0xe0, 0xfe, 0x3f, 0x80, 0xfe, 0x1f, 0x02, 0xfe,
0x0c, 0x06, 0xff, 0x00, 0x0e, 0xff, 0x80, 0x3e, 0xff, 0x80, 0xfe, 0xff, 0xe1, 0xfe
};
// '20' on bezel ring, 25x31px
const unsigned char bitmap_bezel_20[] PROGMEM = {
0xff, 0xff, 0xff, 0x80, 0xff, 0xf3, 0xff, 0x80, 0xff, 0xe0, 0xff, 0x80, 0xff, 0xe0, 0x7f, 0x80,
0xff, 0xc0, 0x3f, 0x80, 0xff, 0xc0, 0x37, 0x80, 0xff, 0x84, 0x31, 0x80, 0xff, 0x84, 0x20, 0x80,
0xff, 0x0c, 0x20, 0x80, 0xfe, 0x0c, 0x38, 0x00, 0xfe, 0x18, 0x78, 0x80, 0xff, 0x38, 0x70, 0x80,
0xff, 0xf8, 0x60, 0x80, 0xff, 0xf8, 0x01, 0x80, 0xff, 0xfc, 0x03, 0x80, 0xf8, 0xfe, 0x03, 0x80,
0xe0, 0x3f, 0x87, 0x80, 0xe0, 0x0f, 0xff, 0x80, 0xc0, 0x03, 0xff, 0x80, 0x83, 0x01, 0xff, 0x80,
0x87, 0x80, 0xff, 0x80, 0x8f, 0xe0, 0x7f, 0x80, 0x8f, 0xf8, 0x7f, 0x80, 0x83, 0xf8, 0x7f, 0x80,
0x81, 0xf8, 0x7f, 0x80, 0xc0, 0x70, 0xff, 0x80, 0xe0, 0x00, 0xff, 0x80, 0xf8, 0x01, 0xff, 0x80,
0xfe, 0x03, 0xff, 0x80, 0xff, 0x07, 0xff, 0x80, 0xff, 0xff, 0xff, 0x80
};
// '30' on bezel ring, 29x16px
const unsigned char bitmap_bezel_30[] PROGMEM = {
0xe0, 0x1f, 0xc0, 0x38, 0xc0, 0x0f, 0x80, 0x18, 0x80, 0x0f, 0x00, 0x08, 0x80, 0x07, 0x00, 0x08,
0x87, 0x87, 0x0f, 0x08, 0x87, 0x87, 0x0f, 0x08, 0x87, 0x87, 0x00, 0x88, 0x87, 0x87, 0x80, 0xf8,
0x87, 0x87, 0x80, 0xf8, 0x87, 0x87, 0x00, 0x18, 0x87, 0x87, 0x0f, 0x08, 0x87, 0x87, 0x0f, 0x08,
0x80, 0x07, 0x00, 0x08, 0x80, 0x0f, 0x80, 0x18, 0xc0, 0x0f, 0x80, 0x18, 0xf0, 0x7f, 0xe0, 0xf8
};
// '40' on bezel ring, 23x27px
const unsigned char bitmap_bezel_40[] PROGMEM = {
0xff, 0xff, 0xfe, 0xff, 0x07, 0xfe, 0xfc, 0x03, 0xfe, 0xf0, 0x01, 0xfe, 0xe0, 0x21, 0xfe, 0x80,
0x70, 0xfe, 0x81, 0xf0, 0xfe, 0x07, 0xf8, 0x7e, 0x0f, 0xf0, 0x7e, 0x0f, 0xc0, 0xfe, 0x87, 0x80,
0xfe, 0x86, 0x01, 0xfe, 0xc0, 0x07, 0xfe, 0xe0, 0x1f, 0xfe, 0xe0, 0x3e, 0x66, 0xf9, 0xfc, 0x02,
0xff, 0xfc, 0x02, 0xff, 0xfc, 0x06, 0xff, 0xf0, 0x0e, 0xff, 0xc0, 0x0e, 0xff, 0x80, 0x86, 0xff,
0x01, 0x86, 0xff, 0x00, 0x02, 0xff, 0x80, 0x00, 0xff, 0x80, 0x00, 0xff, 0xff, 0x06, 0xff, 0xff,
0xfe
};
// '50' on bezel ring, 26x30px
const unsigned char bitmap_bezel_50[] PROGMEM = {
0xff, 0xff, 0xff, 0xc0, 0xff, 0xf8, 0x7f, 0xc0, 0xff, 0xf0, 0x1f, 0xc0, 0xff, 0xe0, 0x07, 0xc0,
0xff, 0xc0, 0x03, 0xc0, 0xff, 0xc3, 0x80, 0xc0, 0xff, 0x87, 0xc0, 0x40, 0xff, 0x87, 0xf0, 0x40,
0xff, 0x87, 0xf8, 0x40, 0xff, 0x81, 0xf8, 0x40, 0xff, 0xc0, 0xf8, 0x40, 0xff, 0xe0, 0x30, 0xc0,
0xff, 0xf0, 0x00, 0xc0, 0xff, 0xfc, 0x01, 0xc0, 0xf9, 0xff, 0x03, 0xc0, 0xf0, 0xff, 0x87, 0xc0,
0xf0, 0xdf, 0xff, 0xc0, 0xe1, 0x03, 0xff, 0xc0, 0xc0, 0x01, 0xff, 0xc0, 0xc2, 0x00, 0x7f, 0xc0,
0x84, 0x20, 0x7f, 0xc0, 0x80, 0x38, 0x7f, 0xc0, 0x00, 0x7c, 0x7f, 0xc0, 0x80, 0x78, 0x7f, 0xc0,
0xe0, 0x78, 0x7f, 0xc0, 0xf8, 0x00, 0xff, 0xc0, 0xfc, 0x00, 0xff, 0xc0, 0xff, 0x01, 0xff, 0xc0,
0xff, 0x83, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xc0
};
const uint8_t BEAT_DELAY = round(1000 / SECOND_HAND_VIBRATION);
const uint8_t SECOND_DELTA_DEGREE = round(6 / SECOND_HAND_VIBRATION);
const String weekDays[7] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
const char ssid[] = SECRET_SSID;
const char pass[] = SECRET_PASS;
// internal variables
String weekday = weekDays[0];
uint8_t day = 31;
uint8_t hour = 10;
uint8_t minute = 9;
uint8_t second = 42;
uint8_t second_prev = 42;
uint8_t second_vibrate_count = 0;
unsigned long t;
int16_t text_x, text_y;
uint16_t text_w, text_h;
bool firstTimeUpdated = false;
bool firstDialDrawn = false;
// functionalities and devices
Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_RST);
GFXcanvas16 canvas(DIAL_WIDTH, DIAL_HEIGHT); // buffer
WiFiUDP ntpUDP;
NTPClient timeClient(
ntpUDP,
NTP_SERVER,
NTP_HOUR_OFFSET * 60 * 60,
NTP_INTERVAL * 60 * 1000);
void setup() {
// set ESP32 freq to save power
setCpuFrequencyMhz(80);
Serial.begin(BAUD_RATE);
// initialize screen and buffer
tft.begin();
tft.setRotation(TFT_ROTATION);
tft.fillScreen(GC9A01A_BLACK);
tft.fillScreen(GC9A01A_BLACK);
tft.setTextWrap(false);
tft.cp437(true);
canvas.fillScreen(GC9A01A_BLACK);
canvas.setTextWrap(false);
canvas.cp437(true);
if (!DEMO_MODE) {
// connect to WiFi and initialize NTP client
delay(500);
Serial.printf("Connecting to WiFi '%s'...\n", SECRET_SSID);
tft.setFont(&FreeMonoBold9pt7b);
tft.setTextSize(1);
tft.getTextBounds("Connecting WiFi...",
0,
0,
&text_x,
&text_y,
&text_w,
&text_h);
uint8_t count = 0;
bool startup_switch = true;
WiFi.disconnect();
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED * 2) {
if (count >= CONNECT_TIMEOUT) ESP.restart();
count++;
Serial.print(".");
if (startup_switch)
tft.setTextColor(startup_switch ? GC9A01A_WHITE : GC9A01A_BLACK);
tft.setCursor(round(TFT_CENTER_X - text_w / 2),
round(TFT_CENTER_Y - text_h / 2));
tft.print("Connecting WiFi...");
startup_switch = !startup_switch;
delay(500);
}
Serial.printf("\nConnected.\n[IP] %s\n", WiFi.localIP().toString());
timeClient.begin();
}
drawInitial();
}
// main loop
void loop() {
t = millis();
if (!DEMO_MODE) getTime();
if (!DEMO_MODE || !firstDialDrawn) {
drawDial();
if (BENCHMARK) Serial.printf("[benchmark] dial drawing: %d ms\n", millis() - t);
firstDialDrawn = true;
if (DEMO_MODE) Serial.println("[demo mode]");
}
while ((millis() - t) < BEAT_DELAY)
;
}
// read date and time from NTP client
void getTime() {
second = timeClient.getSeconds();
if (second >= 60) second = 0;
// update time every second
if (second != second_prev) {
second_vibrate_count = 0;
time_t rawtime = timeClient.getEpochTime();
struct tm *ti = localtime(&rawtime);
int year = ti->tm_year + 1900;
int month = ti->tm_mon + 1;
day = ti->tm_mday;
hour = ti->tm_hour;
minute = ti->tm_min;
weekday = weekDays[ti->tm_wday];
Serial.printf("[time] %4d-%02d-%02d (%s) %02d:%02d:%02d\n", year, month, day, weekday, hour, minute, second);
// try update NTP client every minute
if (second == 0 || !firstTimeUpdated) {
if (timeClient.update()) {
Serial.println("[clock] NTP time synced");
if (!firstTimeUpdated) firstTimeUpdated = true;
}
}
} else {
if (second_vibrate_count < SECOND_HAND_VIBRATION - 1)
second_vibrate_count += SECOND_DELTA_DEGREE;
}
second_prev = second;
}
// draw the initial parts that do not require redrawing
void drawInitial() {
// diver bezel ring
tft.fillCircle(TFT_CENTER_X,
TFT_CENTER_Y,
BEZEL_R,
RGB565(BEZEL_COLOR));
canvas.fillCircle(DIAL_CENTER_X,
DIAL_CENTER_Y,
BEZEL_R,
RGB565(BEZEL_COLOR));
// ticks on diver bezel ring
for (uint8_t tick_pos = 5; tick_pos <= 55; tick_pos += 10) {
drawAngledBox(tft,
TFT_CENTER_X,
TFT_CENTER_Y,
DIAL_RING_R + BEZEL_TICK_PAD,
BEZEL_TICK_W, BEZEL_W - BEZEL_TICK_PAD * 2,
handToDegree(tick_pos, false),
RGB565(BEZEL_TICK_COLOR));
drawAngledBox(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
DIAL_RING_R + BEZEL_TICK_PAD,
BEZEL_TICK_W, BEZEL_W - BEZEL_TICK_PAD * 2,
handToDegree(tick_pos, false),
RGB565(BEZEL_TICK_COLOR));
}
// bezel noon (triangle on 12 o'clock)
const uint16_t x_noon = TFT_CENTER_X;
const uint16_t y_noon = TFT_CENTER_Y - DIAL_RING_R - BEZEL_TICK_PAD;
const float x_delta = (BEZEL_W - BEZEL_TICK_PAD) * cos(50 * M_PI / 180);
const float y_delta = (BEZEL_W - BEZEL_TICK_PAD) * sin(50 * M_PI / 180);
tft.fillTriangle(x_noon,
y_noon,
round(x_noon + x_delta),
round(y_noon - y_delta),
round(x_noon - x_delta),
round(y_noon - y_delta),
RGB565(BEZEL_TICK_COLOR));
// numbers on bezel ring
for (uint8_t tick_pos = 10; tick_pos <= 50; tick_pos += 10) {
const float x_delta = cos(handToDegree(tick_pos, false) * M_PI / 180);
const float y_delta = sin(handToDegree(tick_pos, false) * M_PI / 180);
uint8_t w = 0;
uint8_t h = 0;
switch (tick_pos) {
case 10:
w = 23;
h = 26;
tft.drawBitmap(round(TFT_CENTER_X + (DIAL_RING_R + BEZEL_W / 2) * x_delta - w / 2),
round(TFT_CENTER_Y + (DIAL_RING_R + BEZEL_W / 2) * y_delta - h / 2),
bitmap_bezel_10,
w,
h,
RGB565(BEZEL_COLOR),
RGB565(BEZEL_TICK_COLOR));
canvas.drawBitmap(round(DIAL_CENTER_X + (DIAL_RING_R + BEZEL_W / 2) * x_delta - w / 2),
round(DIAL_CENTER_Y + (DIAL_RING_R + BEZEL_W / 2) * y_delta - h / 2),
bitmap_bezel_10,
w,
h,
RGB565(BEZEL_COLOR),
RGB565(BEZEL_TICK_COLOR));
break;
case 20:
w = 25;
h = 31;
tft.drawBitmap(round(TFT_CENTER_X + (DIAL_RING_R + BEZEL_W / 2) * x_delta - w / 2),
round(TFT_CENTER_Y + (DIAL_RING_R + BEZEL_W / 2) * y_delta - h / 2),
bitmap_bezel_20,
w,
h,
RGB565(BEZEL_COLOR),
RGB565(BEZEL_TICK_COLOR));
canvas.drawBitmap(round(DIAL_CENTER_X + (DIAL_RING_R + BEZEL_W / 2) * x_delta - w / 2),
round(DIAL_CENTER_Y + (DIAL_RING_R + BEZEL_W / 2) * y_delta - h / 2),
bitmap_bezel_20,
w,
h,
RGB565(BEZEL_COLOR),
RGB565(BEZEL_TICK_COLOR));
break;
case 30:
w = 29;
h = 16;
tft.drawBitmap(round(TFT_CENTER_X + (DIAL_RING_R + BEZEL_W / 2) * x_delta - w / 2),
round(TFT_CENTER_Y + (DIAL_RING_R + BEZEL_W / 2) * y_delta - h / 2),
bitmap_bezel_30,
w,
h,
RGB565(BEZEL_COLOR),
RGB565(BEZEL_TICK_COLOR));
canvas.drawBitmap(round(DIAL_CENTER_X + (DIAL_RING_R + BEZEL_W / 2) * x_delta - w / 2),
round(DIAL_CENTER_Y + (DIAL_RING_R + BEZEL_W / 2) * y_delta - h / 2),
bitmap_bezel_30,
w,
h,
RGB565(BEZEL_COLOR),
RGB565(BEZEL_TICK_COLOR));
break;
case 40:
w = 23;
h = 27;
tft.drawBitmap(round(TFT_CENTER_X + (DIAL_RING_R + BEZEL_W / 2) * x_delta - w / 2),
round(TFT_CENTER_Y + (DIAL_RING_R + BEZEL_W / 2) * y_delta - h / 2),
bitmap_bezel_40,
w,
h,
RGB565(BEZEL_COLOR),
RGB565(BEZEL_TICK_COLOR));
canvas.drawBitmap(round(DIAL_CENTER_X + (DIAL_RING_R + BEZEL_W / 2) * x_delta - w / 2), round(DIAL_CENTER_Y + (DIAL_RING_R + BEZEL_W / 2) * y_delta - h / 2), bitmap_bezel_40, w, h, RGB565(BEZEL_COLOR), RGB565(BEZEL_TICK_COLOR));
break;
case 50:
w = 26;
h = 30;
tft.drawBitmap(round(TFT_CENTER_X + (DIAL_RING_R + BEZEL_W / 2) * x_delta - w / 2),
round(TFT_CENTER_Y + (DIAL_RING_R + BEZEL_W / 2) * y_delta - h / 2),
bitmap_bezel_50,
w,
h,
RGB565(BEZEL_COLOR),
RGB565(BEZEL_TICK_COLOR));
canvas.drawBitmap(round(DIAL_CENTER_X + (DIAL_RING_R + BEZEL_W / 2) * x_delta - w / 2),
round(DIAL_CENTER_Y + (DIAL_RING_R + BEZEL_W / 2) * y_delta - h / 2), bitmap_bezel_50,
w,
h,
RGB565(BEZEL_COLOR),
RGB565(BEZEL_TICK_COLOR));
break;
}
}
// small dots on bezel ring
for (uint8_t tick_pos = 1; tick_pos <= 18; tick_pos++) {
switch (tick_pos) {
case 5:
case 9:
case 10:
case 11:
case 15:
continue;
}
const float x_delta = cos(handToDegree(tick_pos, false) * M_PI / 180);
const float y_delta = sin(handToDegree(tick_pos, false) * M_PI / 180);
tft.fillCircle(round(TFT_CENTER_X + (DIAL_RING_R + BEZEL_W * BEZEL_DOT_POS_R_P) * x_delta),
round(TFT_CENTER_Y + (DIAL_RING_R + BEZEL_W * BEZEL_DOT_POS_R_P) * y_delta),
BEZEL_DOT_SELF_R,
RGB565(BEZEL_TICK_COLOR));
canvas.fillCircle(round(DIAL_CENTER_X + (DIAL_RING_R + BEZEL_W * BEZEL_DOT_POS_R_P) * x_delta),
round(DIAL_CENTER_Y + (DIAL_RING_R + BEZEL_W * BEZEL_DOT_POS_R_P) * y_delta),
BEZEL_DOT_SELF_R,
RGB565(BEZEL_TICK_COLOR));
}
// dial ring
canvas.fillCircle(DIAL_CENTER_X,
DIAL_CENTER_Y,
DIAL_RING_R,
RGB565(DIAL_RING_COLOR));
// second ticks on dial ring
for (uint8_t tick_pos = 0; tick_pos < 60; tick_pos++) {
drawLine(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
DIAL_INNER_R + INNER_TICK_PAD,
DIAL_RING_W - INNER_TICK_PAD * 2,
handToDegree(tick_pos, false),
RGB565(INNER_TICK_COLOR));
}
// 5-minute ticks on dial ring
for (uint8_t tick_pos = 5; tick_pos <= 55; tick_pos += 5) {
drawAngledBox(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
DIAL_INNER_R + INNER_TICK_PAD,
INNER_TICK_W,
DIAL_RING_W - INNER_TICK_PAD * 2,
handToDegree(tick_pos, false),
RGB565(INNER_TICK_COLOR));
}
// the thicker tick on dial ring's 12 o'clock
drawAngledBox(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
DIAL_INNER_R + INNER_TICK_PAD,
INNER_TICK_NOON_W,
DIAL_RING_W - INNER_TICK_PAD * 2,
handToDegree(0, false),
RGB565(INNER_TICK_COLOR));
// dial face
canvas.fillCircle(DIAL_CENTER_X,
DIAL_CENTER_Y,
DIAL_INNER_R,
RGB565(DIAL_COLOR));
}
void drawDial() {
float x_delta;
float y_delta;
// dial face
canvas.fillCircle(DIAL_CENTER_X,
DIAL_CENTER_Y,
DIAL_INNER_R,
RGB565(DIAL_COLOR));
for (uint8_t tick_pos = 0; tick_pos <= 55; tick_pos += 5) {
switch (tick_pos) {
case 0: // face noon (triangle) at 12 o'clock
canvas.fillTriangle(DIAL_CENTER_X,
round(DIAL_CENTER_Y - ROUND_DOT_R + ROUND_DOT_SELF_R * 2.5 + ROUND_DOT_BORDER_W * 2),
round(DIAL_CENTER_X - ROUND_DOT_SELF_R * 2 - ROUND_DOT_BORDER_W),
DIAL_CENTER_Y - ROUND_DOT_R - ROUND_DOT_SELF_R - ROUND_DOT_BORDER_W,
round(DIAL_CENTER_X + ROUND_DOT_SELF_R * 2 + ROUND_DOT_BORDER_W),
DIAL_CENTER_Y - ROUND_DOT_R - ROUND_DOT_SELF_R - ROUND_DOT_BORDER_W,
RGB565(ROUND_DOT_BORDER));
canvas.drawLine(DIAL_CENTER_X,
round(DIAL_CENTER_Y - ROUND_DOT_R + ROUND_DOT_SELF_R * 2.5 + ROUND_DOT_BORDER_W * 2),
DIAL_CENTER_X,
round(DIAL_CENTER_Y - ROUND_DOT_R + ROUND_DOT_SELF_R * (2.5 + 2)),
RGB565(ROUND_DOT_BORDER));
canvas.fillTriangle(DIAL_CENTER_X,
round(DIAL_CENTER_Y - ROUND_DOT_R + ROUND_DOT_SELF_R * 2.5),
round(DIAL_CENTER_X - ROUND_DOT_SELF_R * 2),
DIAL_CENTER_Y - ROUND_DOT_R - ROUND_DOT_SELF_R,
round(DIAL_CENTER_X + ROUND_DOT_SELF_R * 2), DIAL_CENTER_Y - ROUND_DOT_R - ROUND_DOT_SELF_R,
RGB565(ROUND_DOT_COLOR));
continue;
case 15: // date and week at 3 o'clock
canvas.fillRect(round(DIAL_CENTER_X + ROUND_DOT_R - DATE_W / 2 - 1),
round(DIAL_CENTER_Y - DATE_H / 2 - 1),
DATE_W + 2,
DATE_H + 2,
RGB565(ROUND_DOT_BORDER));
canvas.fillRect(round(DIAL_CENTER_X + ROUND_DOT_R - DATE_W / 2 - WEEK_W - 2),
round(DIAL_CENTER_Y - WEEK_H / 2 - 1),
WEEK_W + 2,
WEEK_H + 2,
RGB565(ROUND_DOT_BORDER));
canvas.fillRect(round(DIAL_CENTER_X + ROUND_DOT_R - DATE_W / 2),
round(DIAL_CENTER_Y - DATE_H / 2),
DATE_W,
DATE_H,
RGB565(ROUND_DOT_COLOR));
canvas.fillRect(round(DIAL_CENTER_X + ROUND_DOT_R - DATE_W / 2 - WEEK_W - 1),
round(DIAL_CENTER_Y - WEEK_H / 2),
WEEK_W,
WEEK_H,
RGB565(ROUND_DOT_COLOR));
canvas.setFont(&TomThumb);
canvas.setTextSize(2);
canvas.setTextColor(RGB565(DATE_COLOR));
canvas.getTextBounds(String(day),
0,
0,
&text_x,
&text_y,
&text_w,
&text_h);
canvas.setCursor(round(DIAL_CENTER_X + ROUND_DOT_R - text_w / 2),
round(DIAL_CENTER_Y + text_h / 2));
canvas.print(String(day));
if (weekday == weekDays[0]) canvas.setTextColor(RGB565(DATE_SUN_COLOR));
canvas.getTextBounds(weekday,
0,
0,
&text_x,
&text_y,
&text_w,
&text_h);
canvas.setCursor(round(DIAL_CENTER_X + ROUND_DOT_R - DATE_W / 2 - WEEK_W - 1 + WEEK_W / 2 - text_w / 2),
round(DIAL_CENTER_Y + text_h / 2));
canvas.print(weekday);
continue;
case 30: // longer round dot at 6 o'clock
canvas.fillRoundRect(DIAL_CENTER_X - ROUND_DOT_SELF_R - ROUND_DOT_BORDER_W,
DIAL_CENTER_Y + ROUND_DOT_R - ROUND_DOT_SELF_R * 2 - ROUND_DOT_BORDER_W,
ROUND_DOT_SELF_R * 2 + ROUND_DOT_BORDER_W * 2, ROUND_DOT_SELF_R * 4 + ROUND_DOT_BORDER_W * 2,
ROUND_DOT_SELF_R + ROUND_DOT_BORDER_W,
RGB565(ROUND_DOT_BORDER));
canvas.drawLine(DIAL_CENTER_X,
DIAL_CENTER_Y + ROUND_DOT_R - ROUND_DOT_SELF_R * 2,
DIAL_CENTER_X,
DIAL_CENTER_Y + ROUND_DOT_R - ROUND_DOT_SELF_R * 4,
RGB565(ROUND_DOT_BORDER));
canvas.fillRoundRect(DIAL_CENTER_X - ROUND_DOT_SELF_R,
DIAL_CENTER_Y + ROUND_DOT_R - ROUND_DOT_SELF_R * 2,
ROUND_DOT_SELF_R * 2,
ROUND_DOT_SELF_R * 4,
ROUND_DOT_SELF_R,
RGB565(ROUND_DOT_COLOR));
continue;
case 45: // longer round dot at 9 o'clock
canvas.fillRoundRect(DIAL_CENTER_X - ROUND_DOT_R - ROUND_DOT_SELF_R * 2 - ROUND_DOT_BORDER_W,
DIAL_CENTER_Y - ROUND_DOT_SELF_R - ROUND_DOT_BORDER_W,
ROUND_DOT_SELF_R * 4 + ROUND_DOT_BORDER_W * 2,
ROUND_DOT_SELF_R * 2 + ROUND_DOT_BORDER_W * 2,
ROUND_DOT_SELF_R + ROUND_DOT_BORDER_W,
RGB565(ROUND_DOT_BORDER));
canvas.drawLine(DIAL_CENTER_X - ROUND_DOT_R + ROUND_DOT_SELF_R * 2 + ROUND_DOT_BORDER_W,
DIAL_CENTER_Y,
DIAL_CENTER_X - ROUND_DOT_R + ROUND_DOT_SELF_R * 4,
DIAL_CENTER_Y,
RGB565(ROUND_DOT_BORDER));
canvas.fillRoundRect(DIAL_CENTER_X - ROUND_DOT_R - ROUND_DOT_SELF_R * 2,
DIAL_CENTER_Y - ROUND_DOT_SELF_R,
ROUND_DOT_SELF_R * 4,
ROUND_DOT_SELF_R * 2,
ROUND_DOT_SELF_R,
RGB565(ROUND_DOT_COLOR));
continue;
default: // the rest of the round dots on the face
const float x_delta = cos(handToDegree(tick_pos, false) * M_PI / 180);
const float y_delta = sin(handToDegree(tick_pos, false) * M_PI / 180);
const uint16_t dot_x = round(ROUND_DOT_R * x_delta);
const uint16_t dot_y = round(ROUND_DOT_R * y_delta);
canvas.fillCircle(DIAL_CENTER_X + dot_x,
DIAL_CENTER_Y + dot_y,
ROUND_DOT_SELF_R + ROUND_DOT_BORDER_W,
RGB565(ROUND_DOT_BORDER));
canvas.fillCircle(DIAL_CENTER_X + dot_x,
DIAL_CENTER_Y + dot_y,
ROUND_DOT_SELF_R,
RGB565(ROUND_DOT_COLOR));
}
}
// logo
canvas.setFont(&FreeMonoBold9pt7b);
canvas.setTextSize(1);
canvas.setTextColor(RGB565(LOGO_COLOR));
canvas.getTextBounds(LOGO_NAME,
0,
0,
&text_x,
&text_y,
&text_w,
&text_h);
canvas.setCursor(round(DIAL_CENTER_X - text_w / 2),
round(DIAL_CENTER_Y - DIAL_INNER_R * LOGO_POS_R_P + text_h / 2));
canvas.print(LOGO_NAME);
// description
canvas.setFont(&FreeSerifItalic9pt7b);
canvas.setTextSize(1);
canvas.setTextColor(RGB565(DESCRIPTION_COLOR));
canvas.getTextBounds(DESCRIPTION,
0,
0,
&text_x,
&text_y,
&text_w,
&text_h);
canvas.setCursor(round(DIAL_CENTER_X - text_w / 2),
round(DIAL_CENTER_Y + DIAL_INNER_R * DESCRIPTION_POS_R_P + text_h / 2));
canvas.print(DESCRIPTION);
// hour hand
x_delta = cos((handToDegree(hour, true) + 30 * minute / 60) * M_PI / 180);
y_delta = sin((handToDegree(hour, true) + 30 * minute / 60) * M_PI / 180);
float x_delta_90 = cos((handToDegree(hour, true) + 30 * minute / 60 + 90) * M_PI / 180);
float y_delta_90 = sin((handToDegree(hour, true) + 30 * minute / 60 + 90) * M_PI / 180);
drawAngledBox(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
0,
HOUR_HAND_A_H,
HOUR_HAND_A_W,
handToDegree(hour, true) + 30 * minute / 60,
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (HOUR_HAND_A_W - HOUR_HAND_B_ALT) * x_delta),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W - HOUR_HAND_B_ALT) * y_delta),
round(DIAL_CENTER_X + HOUR_HAND_A_W * x_delta - HOUR_HAND_C_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + HOUR_HAND_A_W * y_delta - HOUR_HAND_C_H / 2 * y_delta_90),
round(DIAL_CENTER_X + HOUR_HAND_A_W * x_delta + HOUR_HAND_C_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + HOUR_HAND_A_W * y_delta + HOUR_HAND_C_H / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W) * x_delta - HOUR_HAND_D_BASE / 2 * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W) * y_delta - HOUR_HAND_D_BASE / 2 * y_delta_90),
round(DIAL_CENTER_X + HOUR_HAND_A_W * x_delta - HOUR_HAND_C_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + HOUR_HAND_A_W * y_delta - HOUR_HAND_C_H / 2 * y_delta_90),
round(DIAL_CENTER_X + HOUR_HAND_A_W * x_delta + HOUR_HAND_C_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + HOUR_HAND_A_W * y_delta + HOUR_HAND_C_H / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W) * x_delta - HOUR_HAND_D_BASE / 2 * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W) * y_delta - HOUR_HAND_D_BASE / 2 * y_delta_90),
round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W) * x_delta + HOUR_HAND_D_BASE / 2 * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W) * y_delta + HOUR_HAND_D_BASE / 2 * y_delta_90),
round(DIAL_CENTER_X + HOUR_HAND_A_W * x_delta + HOUR_HAND_C_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + HOUR_HAND_A_W * y_delta + HOUR_HAND_C_H / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W + HOUR_HAND_D_ALT) * x_delta),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W + HOUR_HAND_D_ALT) * y_delta),
round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W) * x_delta - HOUR_HAND_D_BASE / 2 * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W) * y_delta - HOUR_HAND_D_BASE / 2 * y_delta_90),
round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W) * x_delta + HOUR_HAND_D_BASE / 2 * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W) * y_delta + HOUR_HAND_D_BASE / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
drawAngledBox(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
HOUR_HAND_A_W + HOUR_HAND_C_W + HOUR_HAND_D_ALT,
HOUR_MIN_HAND_POINT_W,
HOUR_MIN_HAND_POINT_H,
handToDegree(hour, true) + 30 * minute / 60,
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W) * x_delta - (HOUR_HAND_D_BASE / 2 - HOUR_MIN_HAND_BORDER * 1.5) * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W) * y_delta - (HOUR_HAND_D_BASE / 2 - HOUR_MIN_HAND_BORDER * 1.5) * y_delta_90),
round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_MIN_HAND_BORDER) * x_delta - (HOUR_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_MIN_HAND_BORDER) * y_delta - (HOUR_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_MIN_HAND_BORDER) * x_delta + (HOUR_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_MIN_HAND_BORDER) * y_delta + (HOUR_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
RGB565(HOUR_MIN_HAND_FRONT_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W) * x_delta - (HOUR_HAND_D_BASE / 2 - HOUR_MIN_HAND_BORDER * 1.5) * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W) * y_delta - (HOUR_HAND_D_BASE / 2 - HOUR_MIN_HAND_BORDER * 1.5) * y_delta_90),
round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W) * x_delta + (HOUR_HAND_D_BASE / 2 - HOUR_MIN_HAND_BORDER * 1.5) * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W) * y_delta + (HOUR_HAND_D_BASE / 2 - HOUR_MIN_HAND_BORDER * 1.5) * y_delta_90),
round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_MIN_HAND_BORDER) * x_delta + (HOUR_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_MIN_HAND_BORDER) * y_delta + (HOUR_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
RGB565(HOUR_MIN_HAND_FRONT_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W + HOUR_HAND_D_ALT - HOUR_MIN_HAND_BORDER * 2) * x_delta),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W + HOUR_HAND_D_ALT - HOUR_MIN_HAND_BORDER * 2) * y_delta),
round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W) * x_delta - (HOUR_HAND_D_BASE / 2 - HOUR_MIN_HAND_BORDER * 1.5) * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W) * y_delta - (HOUR_HAND_D_BASE / 2 - HOUR_MIN_HAND_BORDER * 1.5) * y_delta_90),
round(DIAL_CENTER_X + (HOUR_HAND_A_W + HOUR_HAND_C_W) * x_delta + (HOUR_HAND_D_BASE / 2 - HOUR_MIN_HAND_BORDER * 1.5) * x_delta_90),
round(DIAL_CENTER_Y + (HOUR_HAND_A_W + HOUR_HAND_C_W) * y_delta + (HOUR_HAND_D_BASE / 2 - HOUR_MIN_HAND_BORDER * 1.5) * y_delta_90),
RGB565(HOUR_MIN_HAND_FRONT_COLOR));
// minute hand
x_delta = cos((handToDegree(minute, false) + 6 * second / 60) * M_PI / 180);
y_delta = sin((handToDegree(minute, false) + 6 * second / 60) * M_PI / 180);
x_delta_90 = cos((handToDegree(minute, false) + 6 * second / 60 + 90) * M_PI / 180);
y_delta_90 = sin((handToDegree(minute, false) + 6 * second / 60 + 90) * M_PI / 180);
canvas.fillTriangle(round(DIAL_CENTER_X - (MIN_HAND_A_ALT + MIN_HAND_B_OPPS_R) * x_delta),
round(DIAL_CENTER_Y - (MIN_HAND_A_ALT + MIN_HAND_B_OPPS_R) * y_delta),
round(DIAL_CENTER_X - MIN_HAND_B_OPPS_R * x_delta - MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y - MIN_HAND_B_OPPS_R * y_delta - MIN_HAND_B_H / 2 * y_delta_90),
round(DIAL_CENTER_X - MIN_HAND_B_OPPS_R * x_delta + MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y - MIN_HAND_B_OPPS_R * y_delta + MIN_HAND_B_H / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X - MIN_HAND_B_OPPS_R * x_delta - MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y - MIN_HAND_B_OPPS_R * y_delta - MIN_HAND_B_H / 2 * y_delta_90),
round(DIAL_CENTER_X - MIN_HAND_B_OPPS_R * x_delta + MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y - MIN_HAND_B_OPPS_R * y_delta + MIN_HAND_B_H / 2 * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * x_delta - MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * y_delta - MIN_HAND_B_H / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X - MIN_HAND_B_OPPS_R * x_delta + MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y - MIN_HAND_B_OPPS_R * y_delta + MIN_HAND_B_H / 2 * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * x_delta - MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * y_delta - MIN_HAND_B_H / 2 * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * x_delta + MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * y_delta + MIN_HAND_B_H / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * x_delta - MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * y_delta - MIN_HAND_B_H / 2 * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * x_delta + MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * y_delta + MIN_HAND_B_H / 2 * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * x_delta - MIN_HAND_C_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * y_delta - MIN_HAND_C_H / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * x_delta + MIN_HAND_B_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R) * y_delta + MIN_HAND_B_H / 2 * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * x_delta - MIN_HAND_C_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * y_delta - MIN_HAND_C_H / 2 * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * x_delta + MIN_HAND_C_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * y_delta + MIN_HAND_C_H / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
drawAngledBox(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W,
MIN_HAND_D_H,
MIN_HAND_D_W,
handToDegree(minute, false) + 6 * second / 60,
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_F_ALT) * x_delta),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_F_ALT) * y_delta),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * x_delta - MIN_HAND_C_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * y_delta - MIN_HAND_C_H / 2 * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * x_delta + MIN_HAND_C_H / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * y_delta + MIN_HAND_C_H / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W + MIN_HAND_E_ALT) * x_delta),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W + MIN_HAND_E_ALT) * y_delta),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W) * x_delta - MIN_HAND_E_BASE / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W) * y_delta - MIN_HAND_E_BASE / 2 * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W) * x_delta + MIN_HAND_E_BASE / 2 * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W) * y_delta + MIN_HAND_E_BASE / 2 * y_delta_90),
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
drawAngledBox(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W + MIN_HAND_E_ALT,
HOUR_MIN_HAND_POINT_W,
HOUR_MIN_HAND_POINT_H,
handToDegree(minute, false) + 6 * second / 60,
RGB565(HOUR_MIN_HAND_BORDER_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + HOUR_MIN_HAND_BORDER) * x_delta - (MIN_HAND_B_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + HOUR_MIN_HAND_BORDER) * y_delta - (MIN_HAND_B_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + HOUR_MIN_HAND_BORDER) * x_delta + (MIN_HAND_B_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + HOUR_MIN_HAND_BORDER) * y_delta + (MIN_HAND_B_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * x_delta - (MIN_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * y_delta - (MIN_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
RGB565(HOUR_MIN_HAND_FRONT_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + HOUR_MIN_HAND_BORDER) * x_delta + (MIN_HAND_B_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + HOUR_MIN_HAND_BORDER) * y_delta + (MIN_HAND_B_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * x_delta - (MIN_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * y_delta - (MIN_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * x_delta + (MIN_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * y_delta + (MIN_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
RGB565(HOUR_MIN_HAND_FRONT_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_F_ALT - HOUR_MIN_HAND_BORDER * 2) * x_delta),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_F_ALT - HOUR_MIN_HAND_BORDER * 2) * y_delta),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * x_delta - (MIN_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * y_delta - (MIN_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * x_delta + (MIN_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W) * y_delta + (MIN_HAND_C_H / 2 - HOUR_MIN_HAND_BORDER) * y_delta_90),
RGB565(HOUR_MIN_HAND_FRONT_COLOR));
canvas.fillTriangle(round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W + MIN_HAND_E_ALT - HOUR_MIN_HAND_BORDER * 2) * x_delta),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W + MIN_HAND_E_ALT - HOUR_MIN_HAND_BORDER * 2) * y_delta),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W + HOUR_MIN_HAND_BORDER) * x_delta - (MIN_HAND_E_BASE / 2 - HOUR_MIN_HAND_BORDER * 2) * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W + HOUR_MIN_HAND_BORDER) * y_delta - (MIN_HAND_E_BASE / 2 - HOUR_MIN_HAND_BORDER * 2) * y_delta_90),
round(DIAL_CENTER_X + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W + HOUR_MIN_HAND_BORDER) * x_delta + (MIN_HAND_E_BASE / 2 - HOUR_MIN_HAND_BORDER * 2) * x_delta_90),
round(DIAL_CENTER_Y + (MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W + MIN_HAND_D_W + HOUR_MIN_HAND_BORDER) * y_delta + (MIN_HAND_E_BASE / 2 - HOUR_MIN_HAND_BORDER * 2) * y_delta_90),
RGB565(HOUR_MIN_HAND_FRONT_COLOR));
drawAngledBox(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
MIN_HAND_B_W - MIN_HAND_B_OPPS_R + MIN_HAND_C_W,
MIN_HAND_D_H - HOUR_MIN_HAND_BORDER * 2,
MIN_HAND_D_W + HOUR_MIN_HAND_BORDER * 2,
handToDegree(minute, false) + 6 * second / 60,
RGB565(HOUR_MIN_HAND_FRONT_COLOR));
// second hand
x_delta = cos((handToDegree(second, false) + second_vibrate_count * SECOND_HAND_VIBRATION / 6) * M_PI / 180);
y_delta = sin((handToDegree(second, false) + second_vibrate_count * SECOND_HAND_VIBRATION / 6) * M_PI / 180);
drawLine(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
0,
DIAL_INNER_R * SECOND_HAND_R_P,
handToDegree(second, false) + second_vibrate_count * SECOND_HAND_VIBRATION / 6,
RGB565(SECOND_HAND_COLOR));
drawAngledBox(canvas,
DIAL_CENTER_X,
DIAL_CENTER_Y,
0,
2,
DIAL_INNER_R * SECOND_HAND_BALANCE_R_P,
(handToDegree(second, false) + second_vibrate_count * SECOND_HAND_VIBRATION / 6) + 180,
RGB565(SECOND_HAND_COLOR));
canvas.fillCircle(round(DIAL_CENTER_X - DIAL_INNER_R * SECOND_HAND_BALANCE_R_P * x_delta),
round(DIAL_CENTER_Y - DIAL_INNER_R * SECOND_HAND_BALANCE_R_P * y_delta),
SECOND_HAND_BALANCE_DOT_SELF_R + SECOND_HAND_BALANCE_DOT_BORDER_W,
RGB565(SECOND_HAND_COLOR));
canvas.fillCircle(round(DIAL_CENTER_X - DIAL_INNER_R * SECOND_HAND_BALANCE_R_P * x_delta),
round(DIAL_CENTER_Y - DIAL_INNER_R * SECOND_HAND_BALANCE_R_P * y_delta),
SECOND_HAND_BALANCE_DOT_SELF_R,
RGB565(SECOND_HAND_BALANCE_COLOR));
canvas.fillCircle(DIAL_CENTER_X,
DIAL_CENTER_Y,
SECOND_HAND_BASE_R,
RGB565(SECOND_HAND_COLOR));
canvas.fillCircle(DIAL_CENTER_X,
DIAL_CENTER_Y,
HANDS_AXIS_R,
RGB565(SECOND_HAND_AXIS_COLOR));
// overwrite buffer to screen
tft.drawRGBBitmap(round((TFT_WIDTH - DIAL_WIDTH) / 2),
round((TFT_HEIGHT - DIAL_HEIGHT) / 2),
canvas.getBuffer(),
canvas.width(),
canvas.height());
}
// draw an angled rect box on screen
void drawAngledBox(Adafruit_GC9A01A &canvas, uint8_t center_x, uint8_t center_y, uint8_t r, float w, float h, float angle, uint16_t color) {
const float x_delta = cos(angle * M_PI / 180);
const float y_delta = sin(angle * M_PI / 180);
const float x_side_delta = cos((angle + 90) * M_PI / 180);
const float y_side_delta = sin((angle + 90) * M_PI / 180);
const uint16_t x1 = round(center_x + r * x_delta - (w / 2) * x_side_delta);
const uint16_t x2 = round(center_x + r * x_delta + (w / 2) * x_side_delta);
const uint16_t x3 = round(center_x + (r + h) * x_delta - (w / 2) * x_side_delta);
const uint16_t x4 = round(center_x + (r + h) * x_delta + (w / 2) * x_side_delta);
const uint16_t y1 = round(center_y + r * y_delta - (w / 2) * y_side_delta);
const uint16_t y2 = round(center_y + r * y_delta + (w / 2) * y_side_delta);
const uint16_t y3 = round(center_y + (r + h) * y_delta - (w / 2) * y_side_delta);
const uint16_t y4 = round(center_y + (r + h) * y_delta + (w / 2) * y_side_delta);
canvas.fillTriangle(x1, y1, x2, y2, x4, y4, color);
canvas.fillTriangle(x1, y1, x3, y3, x4, y4, color);
}
// draw an angled rect box on buffer
void drawAngledBox(GFXcanvas16 &canvas, uint8_t center_x, uint8_t center_y, uint8_t r, float w, float h, float angle, uint16_t color) {
const float x_delta = cos(angle * M_PI / 180);
const float y_delta = sin(angle * M_PI / 180);
const float x_side_delta = cos((angle + 90) * M_PI / 180);
const float y_side_delta = sin((angle + 90) * M_PI / 180);
const uint16_t x1 = round(center_x + r * x_delta - (w / 2) * x_side_delta);
const uint16_t x2 = round(center_x + r * x_delta + (w / 2) * x_side_delta);
const uint16_t x3 = round(center_x + (r + h) * x_delta - (w / 2) * x_side_delta);
const uint16_t x4 = round(center_x + (r + h) * x_delta + (w / 2) * x_side_delta);
const uint16_t y1 = round(center_y + r * y_delta - (w / 2) * y_side_delta);
const uint16_t y2 = round(center_y + r * y_delta + (w / 2) * y_side_delta);
const uint16_t y3 = round(center_y + (r + h) * y_delta - (w / 2) * y_side_delta);
const uint16_t y4 = round(center_y + (r + h) * y_delta + (w / 2) * y_side_delta);
canvas.fillTriangle(x1, y1, x2, y2, x4, y4, color);
canvas.fillTriangle(x1, y1, x3, y3, x4, y4, color);
}
// draw angled line on buffer
void drawLine(GFXcanvas16 &canvas, uint8_t center_x, uint8_t center_y, float r, uint8_t length, int16_t angle, uint16_t color) {
const float x_delta = cos(angle * M_PI / 180);
const float y_delta = sin(angle * M_PI / 180);
canvas.drawLine(round(center_x + r * x_delta),
round(center_y + r * y_delta),
round(DIAL_CENTER_X + (r + length) * x_delta),
round(DIAL_CENTER_Y + (r + length) * y_delta),
color);
}
// convert seconds to degree
int16_t handToDegree(int16_t hand, bool hour) {
if (hour) hand = (hand > 12 ? hand - 12 : hand) * 5;
return hand * 6 - 90;
}
// convert RGB888 to RGB565
uint16_t RGB565(unsigned long rgb32) {
return (rgb32 >> 8 & 0xf800) | (rgb32 >> 5 & 0x07e0) | (rgb32 >> 3 & 0x001f);
}
Comments