#include #include #include #include #include #include #include enum ALIGN_MODE { ALIGN_MODE_LEFT, ALIGN_MODE_CENTRE, ALIGN_MODE_RIGHT, }; //#define DEBUG #ifdef DEBUG # define DEBUG_P(x...) printf("DEBUG: "x) #else # define DEBUG_P(x...) #endif #define WINDOW_TITLE "Clock Display" #define SCREEN_WIDTH 800 #define SCREEN_HEIGHT 480 #define TIME_X 0 #define TIME_Y -50 #define NO_LOCK_Y 150 #define DATE_1_X 20 #define DATE_1_Y 295 #define DATE_2_X 20 #define DATE_2_Y 350 #define SECONDS_X SCREEN_WIDTH - 80 #define SECONDS_Y SCREEN_HEIGHT - 55 #define NTP_INFO_X 240 #define NTP_INFO_Y SCREEN_HEIGHT - 55 #define SAT_INFO_X 20 #define SAT_INFO_Y NTP_INFO_Y #define SUN_INFO_X SCREEN_WIDTH -20 #define SUN_INFO_1_Y DATE_1_Y #define SUN_INFO_2_Y DATE_2_Y /* Common colours used */ #define COLOUR_WHITE ((SDL_Color){255,255,255,255}) #define COLOUR_YELLOW ((SDL_Color){255,191, 32,255}) #define COLOUR_40GRAY ((SDL_Color){100,100,100,255}) static sem_t data_lock; struct clock_data { int is_unread; char sunrise_time[8]; char sunset_time[8]; int sat_seen; int sat_used; int leap_second; int offset; char offset_units[8]; int no_gps_lock; }; static volatile struct clock_data shared_data; /** * Publish a struct clock_data to the volatile global clock data object. * Caller MUST already hold data_lock */ void publish_clock_data(struct clock_data *l) { char *d = (char*)&shared_data; char *s = (char*)l; size_t i = 0; for (i = 0; i < sizeof(struct clock_data); i++) { d[i] = s[i]; } shared_data.is_unread = 1; } /** * Copy the published struct clock_data to a non-volatile clock data object. * Caller MUST already hold data_lock */ void get_clock_data(struct clock_data *l) { char *d = (char*)l; char *s = (char*)&shared_data; size_t i = 0; for (i = 0; i < sizeof(struct clock_data); i++) { d[i] = s[i]; } } int calculate_brightness(int midday_mins, int now_mins) { #define MIN_BRIGHTNESS 11 #define MAX_BRIGHTNESS 255 #define MIN_BRIGHT_VIRTUAL (-150) #define MAX_BRIGHT_VIRTUAL 400 int ret = 0; DEBUG_P("midday %d, now %d minutes\n", midday_mins, now_mins); ret = MAX_BRIGHT_VIRTUAL - ((MAX_BRIGHT_VIRTUAL - MIN_BRIGHT_VIRTUAL) * abs(now_mins - midday_mins)) / midday_mins; if (ret < MIN_BRIGHTNESS) ret = MIN_BRIGHTNESS; if (ret > MAX_BRIGHTNESS) ret = MAX_BRIGHTNESS; return ret; } /** * Read clock's statistics from files and push them into shared data area for * display thread to pick up */ void *read_stats(void* brightness_f_p) { char buffer[32]; char *next = buffer; FILE *f = NULL; struct clock_data l; memset(&l, 0, sizeof(l)); FILE* brightness_f = brightness_f_p; while (1) { /* Read sunrise, sunset */ if (f = fopen("/tmp/stats-sun", "r")) { if (fgets(buffer, sizeof(buffer), f)) { /* format is %d/%d\n, first is used sats, second is total seen */ if (next = strchr(buffer, ',')) { *(next++) = '\0'; next[strcspn(next, "\n\r\t")] = '\0'; strncpy(l.sunrise_time, buffer, sizeof(l.sunrise_time)); strncpy(l.sunset_time, next, sizeof(l.sunset_time)); } } fclose(f); } /* Read satellite stats */ if (f = fopen("/tmp/stats-sats", "r")) { if (fgets(buffer, sizeof(buffer), f)) { /* format is %d,%d\n, first is used sats, second is total seen */ if (next = strchr(buffer, '/')) { *(next++) = '\0'; l.sat_used = strtod(buffer, NULL); l.sat_seen = strtod(next, NULL); } } fclose(f); } /* read leap second status file - also helps determine lock */ if (f = fopen("/tmp/stats-leap", "r")) { if (fgets(buffer, sizeof(buffer), f)) { buffer[strcspn(buffer, "\n\r\t")] = '\0'; l.no_gps_lock = !strcmp(buffer, "Not synchronised"); l.leap_second = strcmp(buffer, "Normal"); } fclose(f); } if (f = fopen("/tmp/stats-clock", "r")) { if (fgets(buffer, sizeof(buffer), f)) { const char *units[] = { "s", "ms", "µs", "ns", "ps", "fs" }; int i = 0; double offset = fabs(strtod(buffer, NULL)); while (offset < 1 && i < sizeof(units)/sizeof(units[0]) - 1) { i++; offset *= 1000; } l.offset = offset; strncpy(l.offset_units, units[i], sizeof(l.offset_units)); } fclose(f); } /* FIXME move decls to top */ int sunrise_hours = 0; int sunrise_minutes = 0; int sunset_hours = 0; int sunset_minutes = 0; if ( sscanf(l.sunrise_time, "%d:%d", &sunrise_hours, &sunrise_minutes) == 2 && sscanf(l.sunset_time, "%d:%d", &sunset_hours, &sunset_minutes) == 2) { sunrise_minutes += 60 * sunrise_hours; sunset_minutes += 60 * sunset_hours; int local_midday = (sunrise_minutes + sunset_minutes) / 2; time_t t; struct tm *time_s; t = time(NULL); if ((time_s = localtime(&t)) == NULL) { /* FIXME error */ continue; } int brightness = calculate_brightness(local_midday, time_s->tm_hour*60 + time_s->tm_min); DEBUG_P("Calculated brightness %d\n", brightness); if (!brightness_f) { DEBUG_P("Brightness file not open, not writing to it\n"); } else { snprintf(buffer, sizeof(buffer), "%d\n", brightness); if (fwrite(buffer, strlen(buffer), 1, brightness_f) != strlen(buffer)) { perror("fwrite"); } rewind(brightness_f); } } sem_wait(&data_lock); publish_clock_data(&l); sem_post(&data_lock); sleep(5); } } const char *get_th_suffix(unsigned int day) { if (11 <= day && day <= 19) { return "th"; } else { switch (day % 10) { case 1: return "st"; case 2: return "nd"; case 3: return "rd"; default: /* 0, 4-9 */ return "th"; } } } TTF_Font *loadFont(const char *file, int size) { TTF_Font *font = TTF_OpenFont(file, size); if (!font) { printf("Error: TTF_OpenFont: %s\n", TTF_GetError()); return NULL; } return font; } void renderTexture(SDL_Texture *tex, SDL_Renderer *ren, int x, int y){ //Setup the destination rectangle to be at the position we want SDL_Rect dst; dst.x = x; dst.y = y; //Query the texture to get its width and height to use SDL_QueryTexture(tex, NULL, NULL, &dst.w, &dst.h); SDL_RenderCopy(ren, tex, NULL, &dst); } SDL_Texture* renderText(const char *message, TTF_Font *font, SDL_Color color, SDL_Renderer *renderer) { //We need to first render to a surface as that's what TTF_RenderText //returns, then load that surface into a texture SDL_Surface *surf = TTF_RenderUTF8_Blended(font, message, color); if (!surf){ printf("Error in TTF_RenderUTF8_Blended\n"); return NULL; } SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surf); if (!texture){ printf("Error in SDL_CreateTextureFromSurface\n"); } //Clean up the surface and font SDL_FreeSurface(surf); return texture; } int drawText(const char *text, TTF_Font *font, SDL_Color color, SDL_Renderer *renderer, int x, int y, enum ALIGN_MODE mode) { /* FIXME does this need better error detection */ int text_w; SDL_Texture *image; image = renderText(text, font, color, renderer); SDL_QueryTexture(image, NULL, NULL, &text_w, NULL); if (mode == ALIGN_MODE_CENTRE) { x = SCREEN_WIDTH / 2 - text_w / 2; } else if (mode == ALIGN_MODE_RIGHT) { x -= text_w; } renderTexture(image, renderer, x, y); SDL_DestroyTexture(image); return x + (mode == ALIGN_MODE_RIGHT? 0 : 1) * text_w; } int main(void) { struct clock_data l; /* initial unread indicator value */ shared_data.is_unread = 0; strncpy(l.sunrise_time, "??:??", sizeof(l.sunrise_time)); strncpy(l.sunset_time, "??:??", sizeof(l.sunset_time)); l.sat_seen = 0; l.sat_used = 0; l.leap_second = 0; l.offset = 0; strncpy(l.offset_units, "?", sizeof(l.offset_units)); l.no_gps_lock = 1; if (sem_init(&data_lock, 0, 1)) { perror("sem_init"); return 1; } int stop = 0; SDL_Event e; SDL_Window *window; SDL_Init(SDL_INIT_VIDEO); TTF_Init(); TTF_Font *mainTimeFont = loadFont("/usr/share/fonts/TTF/RobotoMono-Regular.ttf", 325); TTF_Font *noLockFont = loadFont("/usr/share/fonts/TTF/Roboto-Regular.ttf", 130); TTF_Font *secondsFont = loadFont("/usr/share/fonts/TTF/RobotoMono-Regular.ttf", 30); TTF_Font *dateFont = loadFont("/usr/share/fonts/TTF/Roboto-Regular.ttf", 50); TTF_Font *dateSymbolFont = loadFont("/usr/share/fonts/TTF/Symbola.ttf", 50); TTF_Font *ntpInfoFont= loadFont("/usr/share/fonts/TTF/Roboto-Regular.ttf", 30); TTF_Font *ntpSymbolFont = loadFont("/usr/share/fonts/TTF/Symbola.ttf", 30); if ( !ntpSymbolFont || !dateSymbolFont || !mainTimeFont || !dateFont || !secondsFont || !ntpInfoFont) { return 1; } FILE* brightness_f = fopen("/sys/class/backlight/rpi_backlight/brightness", "w+"); if (!brightness_f) { perror("fopen"); } if (setgid(getgid()) < 0) { perror("setgid"); return 1; } if (setuid(getuid()) < 0) { perror("setuid"); return 1; } /* launch pthread */ pthread_t thread; if (pthread_create(&thread, NULL, read_stats, brightness_f) != 0) { perror("pthread_create"); return 1; } window = SDL_CreateWindow(WINDOW_TITLE, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_OPENGL); if (!window) { printf("Could not create window: %s\n", SDL_GetError()); return 1; } SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); if (!renderer) { SDL_DestroyWindow(window); printf("SDL_CreateRenderer Error\n"); SDL_Quit(); return 1; } while (!stop) { while (SDL_PollEvent(&e)){ if ( e.type == SDL_QUIT || e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_q) { stop = 1; } } char text[64]; time_t t; struct tm *time_s; SDL_Texture *image; int text_w; SDL_RenderClear(renderer); t = time(NULL); if ((time_s = localtime(&t)) == NULL) { /* FIXME error */ continue; } int next_x = 0; sem_wait(&data_lock); if (shared_data.is_unread) { get_clock_data(&l); shared_data.is_unread = 0; } sem_post(&data_lock); if (l.sat_seen == 0 || l.sat_used == 0 || l.no_gps_lock) { drawText("No GPS Lock", noLockFont, COLOUR_WHITE, renderer, 0, NO_LOCK_Y, ALIGN_MODE_CENTRE); } else { /* Have GPS lock, go for it */ /**/ strftime(text, sizeof(text), "%H%M", time_s); drawText(text, mainTimeFont, COLOUR_WHITE, renderer, TIME_X, TIME_Y, ALIGN_MODE_CENTRE); /**/ /********************/ /* Draws a date string, normally like: * Monday the 25th * February, 2019 */ strftime(text, sizeof(text), "%A", time_s); next_x = drawText(text, dateFont, COLOUR_WHITE, renderer, DATE_1_X, DATE_1_Y, ALIGN_MODE_LEFT); next_x = drawText(" the ", dateFont, COLOUR_40GRAY, renderer, next_x, DATE_1_Y, ALIGN_MODE_LEFT); snprintf(text, sizeof(text), "%d%s", time_s->tm_mday, get_th_suffix(time_s->tm_mday)); next_x = drawText(text, dateFont, COLOUR_WHITE, renderer, next_x, DATE_1_Y, ALIGN_MODE_LEFT); next_x = drawText(".", dateFont, COLOUR_40GRAY, renderer, next_x, DATE_1_Y, ALIGN_MODE_LEFT); strftime(text, sizeof(text), "%B", time_s); next_x = drawText(text, dateFont, COLOUR_WHITE, renderer, DATE_2_X, DATE_2_Y, ALIGN_MODE_LEFT); next_x = drawText(", ", dateFont, COLOUR_40GRAY, renderer, next_x, DATE_2_Y, ALIGN_MODE_LEFT); strftime(text, sizeof(text), "%Y", time_s); next_x = drawText(text, dateFont, COLOUR_WHITE, renderer, next_x, DATE_2_Y, ALIGN_MODE_LEFT); /********************/ /**/ strftime(text, sizeof(text), ":%S", time_s); next_x = drawText(text, secondsFont, COLOUR_WHITE, renderer, SECONDS_X, SECONDS_Y, ALIGN_MODE_LEFT); /**/ /**/ next_x = drawText("⏰ ", ntpSymbolFont, COLOUR_40GRAY, renderer, NTP_INFO_X, NTP_INFO_Y, ALIGN_MODE_LEFT); snprintf(text, sizeof(text), " RMS %d %s%s ", l.offset, l.offset_units, l.leap_second ? ", Leap second coming!" : ""); next_x = drawText(text, ntpInfoFont, COLOUR_40GRAY, renderer, next_x, NTP_INFO_Y, ALIGN_MODE_LEFT); /**/ /**/ next_x = drawText(l.sunrise_time, dateFont, COLOUR_WHITE, renderer, SUN_INFO_X, SUN_INFO_1_Y, ALIGN_MODE_RIGHT); next_x = drawText("☀⬆", dateSymbolFont, COLOUR_YELLOW, renderer, next_x, SUN_INFO_1_Y, ALIGN_MODE_RIGHT); next_x = drawText(l.sunset_time, dateFont, COLOUR_WHITE, renderer, SUN_INFO_X, SUN_INFO_2_Y, ALIGN_MODE_RIGHT); next_x = drawText("☀⬇", dateSymbolFont, COLOUR_YELLOW, renderer, next_x, SUN_INFO_2_Y, ALIGN_MODE_RIGHT); // next_x = drawText("⭡05:45 ↓20:13", dateFont, COLOUR_WHITE, renderer, SUN_INFO_X, SUN_INFO_Y); /**/ } /**/ next_x = drawText("🛰 ", ntpSymbolFont, COLOUR_40GRAY, renderer, SAT_INFO_X, SAT_INFO_Y, ALIGN_MODE_LEFT); snprintf(text, sizeof(text), "%d (%d used)", l.sat_seen, l.sat_used); next_x = drawText(text, ntpInfoFont, COLOUR_40GRAY, renderer, next_x, NTP_INFO_Y, ALIGN_MODE_LEFT); /**/ SDL_RenderPresent(renderer); SDL_Delay(100); } SDL_DestroyWindow(window); SDL_Quit(); return 0; }