├── LICENSE ├── README.md ├── application.fam ├── dcf77.c ├── dcf77.h ├── dcf77_clock_sync.c ├── icons └── app_10x10.png └── img └── 1.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Milko Daskalov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flipper-Zero DCF77 Clock Sync 2 | Emulates the [DCF77](https://en.wikipedia.org/wiki/DCF77) time signal on the RFID antenna and on GPIO A4 pin. 3 | 4 | Uses PWM with frequency of 77.5 kHz on the GPIO pin to simulate the signal. 5 | 6 | # Usage 7 | 8 | Normally the clock gets syncrhonized in two to five minutes depending on the signal strength. 9 | 10 | The OK button changes the transmitted signal between CET and CEST (dst) time. 11 | 12 | # Antenna 13 | The RFID antenna wokrs best at distances of up to 50cm. The signal gets recognized in few seconds. 14 | 15 | When using the GPIO, best results are achieved if you connect a ferrite antenna over 330 ohm resistor and a capactior to ground. 16 | 17 | It also works with analog beeper or small in-ear headphone connected to the GPIO pin. 18 | -------------------------------------------------------------------------------- /application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="dcf77_clock_sync", 3 | name="DCF77 Clock Sync", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="dcf77_clock_sync_app_main", 6 | requires=["gui"], 7 | stack_size=1 * 1024, 8 | order=10, 9 | fap_icon="icons/app_10x10.png", 10 | fap_category="Tools", 11 | fap_author="mdaskalov", 12 | fap_weburl="https://github.com/mdaskalov/dcf77-clock-sync", 13 | fap_version="1.3", 14 | fap_description="Emulate DCF77 time signal on the RFID antenna and the A4 GPIO pin", 15 | ) -------------------------------------------------------------------------------- /dcf77.c: -------------------------------------------------------------------------------- 1 | #include "dcf77.h" 2 | 3 | #define DST_BIT 17 4 | #define MIN_BIT 21 5 | #define HOUR_BIT 29 6 | #define DAY_BIT 36 7 | #define WEEKDAY_BIT 42 8 | #define MONTH_BIT 45 9 | #define YEAR_BIT 50 10 | 11 | static uint8_t dcf77_bits[] = { 12 | 0, // 00: Start of minute 13 | 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 01: Weather broadcast / Civil warning bits 14 | 8, // 15: Call bit: abnormal transmitter operation 15 | 0, // 16: Summer time announcement. Set during hour before change 16 | 0, 1, // 17: 01=CET, 10=CEST 17 | 0, // 19: Leap second announcement. Set during hour before leap second 18 | 1, // 20: Start of encoded time 19 | 8, 0, 0, 0, 0, 0, 0, 0, // 21: Minutes (7bit + parity, 00-59) 20 | 8, 0, 0, 0, 0, 0, 0, // 29: Hours (6bit + parity, 0-23) 21 | 8, 0, 0, 0, 0, 0, // 36: Day of month (6bit, 1-31) 22 | 8, 0, 0, // 42: Day of week (3bit, 1-7, Monday=1) 23 | 8, 0, 0, 0, 0, // 45: Month number (5bit, 1-12) 24 | 8, 0, 0, 0, 0, 0, 0, 0, 0, // 50: Year within century (8bit + parity, 00-99) 25 | 0 // 59: Not used 26 | }; 27 | 28 | void dcf77_encode(int start, int len, int val, int par) { 29 | uint8_t parity = (par != -1 ? par : dcf77_bits[start]) & 1; 30 | uint8_t byte = ((val / 10) << 4) + (val % 10); 31 | for(int bit = 0; bit < len; bit++) { 32 | uint8_t dcf77_bit = (byte >> bit) & 1; 33 | parity ^= dcf77_bit; 34 | dcf77_bits[start + bit] = (dcf77_bits[start + bit] & 0x0E) + dcf77_bit; 35 | } 36 | dcf77_bits[start + len] = (dcf77_bits[start + len] & 0xE) + (parity & 1); 37 | } 38 | 39 | void set_dcf77_time(DateTime* dt, bool is_dst) { 40 | dcf77_encode(DST_BIT, 2, is_dst > 0 ? 1 : 2, 1); // parity = leap second -> 0 41 | dcf77_encode(MIN_BIT, 7, dt->minute, 0); 42 | dcf77_encode(HOUR_BIT, 6, dt->hour, 0); 43 | dcf77_encode(DAY_BIT, 6, dt->day, 0); 44 | dcf77_encode(WEEKDAY_BIT, 3, dt->weekday, -1); 45 | dcf77_encode(MONTH_BIT, 5, dt->month, -1); 46 | dcf77_encode(YEAR_BIT, 8, dt->year % 100, -1); 47 | } 48 | 49 | bool get_dcf77_bit(int sec) { 50 | return dcf77_bits[sec % 60] & 1; 51 | } 52 | 53 | char* get_dcf77_data(int sec) { 54 | static char data[70]; 55 | 56 | int idx = 0; 57 | int start = sec > 25 ? sec - 25 : 0; 58 | for(int bit = start; bit <= sec; bit++) { 59 | if(dcf77_bits[bit] & 8) data[idx++] = '-'; 60 | data[idx++] = '0' + (dcf77_bits[bit] & 1); 61 | } 62 | data[idx] = 0; 63 | return data; 64 | } -------------------------------------------------------------------------------- /dcf77.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | void set_dcf77_time(DateTime* dt, bool is_dst); 7 | bool get_dcf77_bit(int sec); 8 | char* get_dcf77_data(int sec); -------------------------------------------------------------------------------- /dcf77_clock_sync.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include "dcf77.h" 11 | 12 | #define SCREEN_SIZE_X 128 13 | #define SCREEN_SIZE_Y 64 14 | #define DCF77_FREQ 77500 15 | #define DCF77_OFFSET 60 16 | #define SYNC_DELAY 50 17 | 18 | char* WEEKDAYS[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; 19 | 20 | typedef struct { 21 | DateTime dt; 22 | bool is_dst; 23 | FuriString* str; 24 | LocaleTimeFormat tim_fmt; 25 | LocaleDateFormat dat_fmt; 26 | } AppData; 27 | 28 | static void app_draw_callback(Canvas* canvas, void* context) { 29 | AppData* app = (AppData*)context; 30 | furi_assert(app->str); 31 | 32 | uint8_t hour = app->dt.hour; 33 | bool fmt_12h = false; 34 | if(app->tim_fmt == LocaleTimeFormat12h) { 35 | hour = hour == 0 ? 12 : hour % 12; 36 | fmt_12h = true; 37 | } 38 | 39 | furi_string_printf(app->str, "%2u:%02u:%02u", hour, app->dt.minute, app->dt.second); 40 | const char* tim_cstr = furi_string_get_cstr(app->str); 41 | 42 | canvas_set_font(canvas, FontBigNumbers); 43 | canvas_draw_str_aligned( 44 | canvas, SCREEN_SIZE_X / 2, SCREEN_SIZE_Y / 2, AlignCenter, AlignCenter, tim_cstr); 45 | 46 | if(fmt_12h) { 47 | canvas_set_font(canvas, FontSecondary); 48 | canvas_draw_str_aligned( 49 | canvas, 50 | 0, 51 | (SCREEN_SIZE_Y / 2) - 7, 52 | AlignLeft, 53 | AlignTop, 54 | (app->dt.hour >= 12 ? "PM" : "AM")); 55 | } 56 | 57 | FuriString* dat = furi_string_alloc(); 58 | locale_format_date(dat, &app->dt, app->dat_fmt, "-"); 59 | const char* dow_str = WEEKDAYS[(app->dt.weekday - 1) % 7]; 60 | const char* dst_str = app->is_dst ? "CEST" : "CET"; 61 | furi_string_printf(app->str, "%s %s %s", dow_str, furi_string_get_cstr(dat), dst_str); 62 | furi_string_free(dat); 63 | 64 | canvas_set_font(canvas, FontSecondary); 65 | canvas_draw_str_aligned( 66 | canvas, SCREEN_SIZE_X / 2, 0, AlignCenter, AlignTop, furi_string_get_cstr(app->str)); 67 | 68 | if(app->dt.second < 59) { 69 | char* data = get_dcf77_data(app->dt.second); 70 | canvas_draw_str_aligned( 71 | canvas, SCREEN_SIZE_X, SCREEN_SIZE_Y, AlignRight, AlignBottom, data); 72 | } 73 | } 74 | 75 | static void app_input_callback(InputEvent* input_event, void* ctx) { 76 | furi_assert(ctx); 77 | FuriMessageQueue* event_queue = ctx; 78 | furi_message_queue_put(event_queue, input_event, FuriWaitForever); 79 | } 80 | 81 | void set_time(AppData* app, int offset) { 82 | DateTime dcf_dt; 83 | uint32_t timestamp = datetime_datetime_to_timestamp(&app->dt) + offset; 84 | datetime_timestamp_to_datetime(timestamp, &dcf_dt); 85 | set_dcf77_time(&dcf_dt, app->is_dst); 86 | } 87 | 88 | int dcf77_clock_sync_app_main(void* p) { 89 | UNUSED(p); 90 | 91 | AppData* app = malloc(sizeof(AppData)); 92 | furi_hal_rtc_get_datetime(&app->dt); 93 | app->is_dst = false; 94 | app->str = furi_string_alloc(); 95 | app->tim_fmt = locale_get_time_format(); 96 | app->dat_fmt = locale_get_date_format(); 97 | 98 | set_time(app, DCF77_OFFSET); 99 | 100 | ViewPort* view_port = view_port_alloc(); 101 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); 102 | 103 | view_port_draw_callback_set(view_port, app_draw_callback, app); 104 | view_port_input_callback_set(view_port, app_input_callback, event_queue); 105 | 106 | Gui* gui = furi_record_open(RECORD_GUI); 107 | gui_add_view_port(gui, view_port, GuiLayerFullscreen); 108 | 109 | NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); 110 | notification_message_block(notification, &sequence_display_backlight_enforce_on); 111 | 112 | InputEvent event; 113 | bool running = false; 114 | bool exit = false; 115 | int sec = app->dt.second; 116 | while(!exit) { 117 | int silence_ms = 0; 118 | // wait next second 119 | while(app->dt.second == sec) furi_hal_rtc_get_datetime(&app->dt); 120 | 121 | if(app->dt.second < 59) { 122 | if(running) { 123 | furi_hal_light_set(LightRed | LightGreen | LightBlue, 0); 124 | furi_hal_rfid_tim_read_stop(); 125 | furi_hal_pwm_stop(FuriHalPwmOutputIdLptim2PA4); 126 | furi_hal_gpio_init( 127 | &gpio_ext_pa4, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh); 128 | } 129 | silence_ms = get_dcf77_bit(app->dt.second) ? 200 : 100; 130 | furi_delay_ms(silence_ms); 131 | furi_hal_rfid_tim_read_start(DCF77_FREQ, 0.5); 132 | furi_hal_pwm_start(FuriHalPwmOutputIdLptim2PA4, DCF77_FREQ, 50); 133 | furi_hal_light_set(LightBlue, 0xFF); 134 | running = true; 135 | } else 136 | set_time(app, DCF77_OFFSET + 1); 137 | 138 | sec = app->dt.second; 139 | int wait_ms = 1000 - silence_ms - SYNC_DELAY; 140 | uint32_t tick_start = furi_get_tick(); 141 | while(wait_ms > 0) { 142 | FuriStatus status = furi_message_queue_get(event_queue, &event, wait_ms); 143 | if((status == FuriStatusOk) && (event.type == InputTypePress)) { 144 | if(event.key == InputKeyOk) 145 | app->is_dst = !app->is_dst; 146 | else if(event.key == InputKeyBack) { 147 | exit = true; 148 | break; 149 | } 150 | } 151 | view_port_update(view_port); 152 | if(status == FuriStatusErrorTimeout) break; 153 | wait_ms -= furi_get_tick() - tick_start; 154 | } 155 | } 156 | 157 | if(running) { 158 | furi_hal_rfid_tim_read_stop(); 159 | furi_hal_pwm_stop(FuriHalPwmOutputIdLptim2PA4); 160 | furi_hal_light_set(LightRed | LightGreen | LightBlue, 0); 161 | } 162 | 163 | notification_message_block(notification, &sequence_display_backlight_enforce_auto); 164 | 165 | view_port_enabled_set(view_port, false); 166 | gui_remove_view_port(gui, view_port); 167 | furi_record_close(RECORD_NOTIFICATION); 168 | furi_record_close(RECORD_GUI); 169 | furi_message_queue_free(event_queue); 170 | view_port_free(view_port); 171 | 172 | furi_string_free(app->str); 173 | free(app); 174 | 175 | return 0; 176 | } -------------------------------------------------------------------------------- /icons/app_10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdaskalov/dcf77-clock-sync/51846ed5071bebda1f499e25df3e00942cdc2c30/icons/app_10x10.png -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdaskalov/dcf77-clock-sync/51846ed5071bebda1f499e25df3e00942cdc2c30/img/1.png --------------------------------------------------------------------------------