diff --git a/non_catalog_apps/dcf77_clock_sync/LICENSE b/non_catalog_apps/dcf77_clock_sync/LICENSE new file mode 100644 index 00000000000..bef517ea7fe --- /dev/null +++ b/non_catalog_apps/dcf77_clock_sync/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Milko Daskalov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/non_catalog_apps/dcf77_clock_sync/README.md b/non_catalog_apps/dcf77_clock_sync/README.md new file mode 100644 index 00000000000..92902122395 --- /dev/null +++ b/non_catalog_apps/dcf77_clock_sync/README.md @@ -0,0 +1,17 @@ +# Flipper-Zero DCF77 Clock Sync +Emulates the [DCF77](https://en.wikipedia.org/wiki/DCF77) time signal on the RFID antenna and on GPIO A4 pin. + +Uses PWM with frequency of 77.5 kHz on the GPIO pin to simulate the signal. + +# Usage + +Normally the clock gets syncrhonized in two to five minutes depending on the signal strength. + +The OK button changes the transmitted signal between CET and CEST (dst) time. + +# Antenna +The RFID antenna wokrs best at distances of up to 50cm. The signal gets recognized in few seconds. + +When using the GPIO, best results are achieved if you connect a ferrite antenna over 330 ohm resistor and a capactior to ground. + +It also works with analog beeper or small in-ear headphone connected to the GPIO pin. diff --git a/non_catalog_apps/dcf77_clock_sync/application.fam b/non_catalog_apps/dcf77_clock_sync/application.fam new file mode 100644 index 00000000000..2e9b5ac35d6 --- /dev/null +++ b/non_catalog_apps/dcf77_clock_sync/application.fam @@ -0,0 +1,15 @@ +App( + appid="dcf77_clock_sync", + name="[DCF77] Clock Sync", + apptype=FlipperAppType.EXTERNAL, + entry_point="dcf77_clock_sync_app_main", + requires=["gui"], + stack_size=1 * 1024, + order=10, + fap_icon="icons/app_10x10.png", + fap_category="Tools", + fap_author="@mdaskalov", + fap_weburl="https://github.com/mdaskalov/dcf77-clock-sync.git", + fap_version="1.1", + fap_description="Emulate DCF77 time signal on the RFID antena and the A4 GPIO pin", +) diff --git a/non_catalog_apps/dcf77_clock_sync/dcf77.c b/non_catalog_apps/dcf77_clock_sync/dcf77.c new file mode 100644 index 00000000000..a7bac6e910a --- /dev/null +++ b/non_catalog_apps/dcf77_clock_sync/dcf77.c @@ -0,0 +1,64 @@ +#include + +#define DST_BIT 17 +#define MIN_BIT 21 +#define HOUR_BIT 29 +#define DAY_BIT 36 +#define WEEKDAY_BIT 42 +#define MONTH_BIT 45 +#define YEAR_BIT 50 + +static uint8_t dcf77_bits[] = { + 0, // 00: Start of minute + 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 01: Weather broadcast / Civil warning bits + 8, // 15: Call bit: abnormal transmitter operation + 0, // 16: Summer time announcement. Set during hour before change + 0, 1, // 17: 01=CET, 10=CEST + 0, // 19: Leap second announcement. Set during hour before leap second + 1, // 20: Start of encoded time + 8, 0, 0, 0, 0, 0, 0, 0, // 21: Minutes (7bit + parity, 00-59) + 8, 0, 0, 0, 0, 0, 0, // 29: Hours (6bit + parity, 0-23) + 8, 0, 0, 0, 0, 0, // 36: Day of month (6bit, 1-31) + 8, 0, 0, // 42: Day of week (3bit, 1-7, Monday=1) + 8, 0, 0, 0, 0, // 45: Month number (5bit, 1-12) + 8, 0, 0, 0, 0, 0, 0, 0, 0, // 50: Year within century (8bit + parity, 00-99) + 0 // 59: Not used +}; + +void dcf77_encode(int start, int len, int val, int par) { + uint8_t parity = (par != -1 ? par : dcf77_bits[start]) & 1; + uint8_t byte = ((val / 10) << 4) + (val % 10); + for(int bit = 0; bit < len; bit++) { + uint8_t dcf77_bit = (byte >> bit) & 1; + parity ^= dcf77_bit; + dcf77_bits[start + bit] = (dcf77_bits[start + bit] & 0x0E) + dcf77_bit; + } + dcf77_bits[start + len] = (dcf77_bits[start + len] & 0xE) + (parity & 1); +} + +void set_dcf77_time(DateTime* dt, bool is_dst) { + dcf77_encode(DST_BIT, 2, is_dst > 0 ? 1 : 2, 1); // parity = leap second -> 0 + dcf77_encode(MIN_BIT, 7, dt->minute, 0); + dcf77_encode(HOUR_BIT, 6, dt->hour, 0); + dcf77_encode(DAY_BIT, 6, dt->day, 0); + dcf77_encode(WEEKDAY_BIT, 3, dt->weekday, -1); + dcf77_encode(MONTH_BIT, 5, dt->month, -1); + dcf77_encode(YEAR_BIT, 8, dt->year % 100, -1); +} + +bool get_dcf77_bit(int sec) { + return dcf77_bits[sec % 60] & 1; +} + +char* get_dcf77_data(int sec) { + static char data[70]; + + int idx = 0; + int start = sec > 25 ? sec - 25 : 0; + for(int bit = start; bit <= sec; bit++) { + if(dcf77_bits[bit] & 8) data[idx++] = '-'; + data[idx++] = '0' + (dcf77_bits[bit] & 1); + } + data[idx] = 0; + return data; +} \ No newline at end of file diff --git a/non_catalog_apps/dcf77_clock_sync/dcf77.h b/non_catalog_apps/dcf77_clock_sync/dcf77.h new file mode 100644 index 00000000000..32436848bb0 --- /dev/null +++ b/non_catalog_apps/dcf77_clock_sync/dcf77.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +void set_dcf77_time(DateTime* dt, bool is_dst); +int get_dcf77_bit(int sec); +char* get_dcf77_data(int sec); \ No newline at end of file diff --git a/non_catalog_apps/dcf77_clock_sync/dcf77_clock_sync.c b/non_catalog_apps/dcf77_clock_sync/dcf77_clock_sync.c new file mode 100644 index 00000000000..b1ddc3a7e47 --- /dev/null +++ b/non_catalog_apps/dcf77_clock_sync/dcf77_clock_sync.c @@ -0,0 +1,190 @@ +#include +#include +#include +#include +#include + +#include "dcf77.h" + +#define SCREEN_SIZE_X 128 +#define SCREEN_SIZE_Y 64 +#define DCF77_FREQ 77500 +#define DCF77_OFFSET 60 +#define SYNC_DELAY 50 +#define UPDATES 8 + +#define SECONDS_PER_MINUTE 60 +#define SECONDS_PER_HOUR (SECONDS_PER_MINUTE * 60) +#define SECONDS_PER_DAY (SECONDS_PER_HOUR * 24) +#define MONTHS_COUNT 12 +#define EPOCH_START_YEAR 1970 + +char* WEEKDAYS[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; + +typedef struct { + DateTime dt; + DateTime dcf_dt; + bool is_dst; +} AppData; + +static void app_draw_callback(Canvas* canvas, void* context) { + AppData* app_data = (AppData*)context; + + char buffer[64]; + + snprintf( + buffer, + sizeof(buffer), + "%02u:%02u:%02u", + app_data->dt.hour, + app_data->dt.minute, + app_data->dt.second); + + canvas_set_font(canvas, FontBigNumbers); + canvas_draw_str_aligned( + canvas, SCREEN_SIZE_X / 2, SCREEN_SIZE_Y / 2, AlignCenter, AlignCenter, buffer); + + const char* dow_str = WEEKDAYS[(app_data->dt.weekday - 1) % 7]; + const char* dst_str = app_data->is_dst ? "CEST" : "CET"; + snprintf( + buffer, + sizeof(buffer), + "%s %02u-%02u-%04u %s", + dow_str, + app_data->dt.day, + app_data->dt.month, + app_data->dt.year, + dst_str); + + canvas_set_font(canvas, FontSecondary); + canvas_draw_str_aligned(canvas, SCREEN_SIZE_X / 2, 0, AlignCenter, AlignTop, buffer); + + if(app_data->dt.second < 59) { + char* data = get_dcf77_data(app_data->dt.second); + canvas_draw_str_aligned( + canvas, SCREEN_SIZE_X, SCREEN_SIZE_Y, AlignRight, AlignBottom, data); + } +} + +static void app_input_callback(InputEvent* input_event, void* ctx) { + furi_assert(ctx); + FuriMessageQueue* event_queue = ctx; + furi_message_queue_put(event_queue, input_event, FuriWaitForever); +} + +void time_add(DateTime* from, DateTime* to, int add) { + uint32_t timestamp = datetime_datetime_to_timestamp(from) + add; + + uint32_t days = timestamp / SECONDS_PER_DAY; + uint32_t seconds_in_day = timestamp % SECONDS_PER_DAY; + + to->year = EPOCH_START_YEAR; + + while(days >= datetime_get_days_per_year(to->year)) { + days -= datetime_get_days_per_year(to->year); + (to->year)++; + } + + to->month = 1; + while(days >= datetime_get_days_per_month(datetime_is_leap_year(to->year), to->month)) { + days -= datetime_get_days_per_month(datetime_is_leap_year(to->year), to->month); + (to->month)++; + } + + to->weekday = ((days + 4) % 7) + 1; + + to->day = days + 1; + to->hour = seconds_in_day / SECONDS_PER_HOUR; + to->minute = (seconds_in_day % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE; + to->second = seconds_in_day % SECONDS_PER_MINUTE; +} + +int dcf77_clock_sync_app_main(void* p) { + UNUSED(p); + + AppData app_data; + InputEvent event; + + app_data.is_dst = false; + furi_hal_rtc_get_datetime(&app_data.dt); + time_add(&app_data.dt, &app_data.dcf_dt, DCF77_OFFSET); + set_dcf77_time(&app_data.dcf_dt, app_data.is_dst); + + ViewPort* view_port = view_port_alloc(); + FuriMessageQueue* event_queue = furi_messa ge_queue_alloc(8, sizeof(InputEvent)); + + view_port_draw_callback_set(view_port, app_draw_callback, &app_data); + view_port_input_callback_set(view_port, app_input_callback, event_queue); + + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); + notification_message_block(notification, &sequence_display_backlight_enforce_on); + + bool running = false; + bool exit = false; + int sec = app_data.dt.second; + while(!exit) { + int silence_ms = 0; + // wait next second + while(app_data.dt.second == sec) furi_hal_rtc_get_datetime(&app_data.dt); + + if(app_data.dt.second < 59) { + furi_hal_light_set(LightRed | LightGreen | LightBlue, 0); + if(running) { + furi_hal_rfid_tim_read_stop(); + furi_hal_pwm_stop(FuriHalPwmOutputIdLptim2PA4); + furi_hal_gpio_init( + &gpio_ext_pa4, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh); + } + silence_ms = get_dcf77_bit(app_data.dt.second) ? 200 : 100; + furi_delay_ms(silence_ms); + furi_hal_rfid_tim_read_start(DCF77_FREQ, 0.5); + furi_hal_pwm_start(FuriHalPwmOutputIdLptim2PA4, DCF77_FREQ, 50); + running = true; + furi_hal_light_set(LightBlue, 0xFF); + } else { + time_add(&app_data.dt, &app_data.dcf_dt, DCF77_OFFSET + 1); + set_dcf77_time(&app_data.dcf_dt, app_data.is_dst); + } + + sec = app_data.dt.second; + int wait_ms = (1000 - silence_ms - SYNC_DELAY) / UPDATES; + for(int i = 0; i < UPDATES; i++) { + if(furi_message_queue_get(event_queue, &event, wait_ms) == FuriStatusOk) { + if((event.type == InputTypePress) || (event.type == InputTypeRepeat)) { + switch(event.key) { + case InputKeyOk: + app_data.is_dst = !app_data.is_dst; + break; + case InputKeyBack: + exit = true; + break; + default: + break; + } + } + } + view_port_update(view_port); + if(exit) break; + } + } + + if(running) { + furi_hal_rfid_tim_read_stop(); + furi_hal_pwm_stop(FuriHalPwmOutputIdLptim2PA4); + furi_hal_light_set(LightRed | LightGreen | LightBlue, 0); + } + + notification_message_block(notification, &sequence_display_backlight_enforce_auto); + + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + furi_record_close(RECORD_NOTIFICATION); + furi_record_close(RECORD_GUI); + furi_message_queue_free(event_queue); + view_port_free(view_port); + + return 0; +} \ No newline at end of file diff --git a/non_catalog_apps/dcf77_clock_sync/icons/app_10x10.png b/non_catalog_apps/dcf77_clock_sync/icons/app_10x10.png new file mode 100644 index 00000000000..2cbe2e44a4c Binary files /dev/null and b/non_catalog_apps/dcf77_clock_sync/icons/app_10x10.png differ diff --git a/non_catalog_apps/dcf77_clock_sync/img/1.png b/non_catalog_apps/dcf77_clock_sync/img/1.png new file mode 100644 index 00000000000..b01bc765912 Binary files /dev/null and b/non_catalog_apps/dcf77_clock_sync/img/1.png differ