Просмотр исходного кода

replace clock sync with clock spoof

MX 1 год назад
Родитель
Сommit
1105a26612

+ 1 - 1
ReadMe.md

@@ -260,7 +260,7 @@ The Flipper and its community wouldn't be as rich as it is without your contribu
 | Simple calendar app | ![Tools Badge] | [by Adiras](https://github.com/Adiras/flipperzero-calendar) |  | ![None Badge] |
 | Programmer Calculator | ![Tools Badge] | [by armixz](https://github.com/armixz/Flipper-Zero-Programmer-Calculator) |  | ![None Badge] |
 | Tone Generator | ![Tools Badge] | [by GEMISIS](https://github.com/GEMISIS/tone_gen/) |  | ![None Badge] |
-| DCF77 Clock Sync | ![Tools Badge] | [by mdaskalov](https://github.com/mdaskalov/dcf77-clock-sync) |  | ![None Badge] |
+| DCF77 Clock Sppof | ![Tools Badge] | [by molodos](https://github.com/molodos/dcf77-clock-spoof) | fork of [dcf77-clock-sync](https://github.com/mdaskalov/dcf77-clock-sync) | ![None Badge] |
 | Quac! Remote | ![Tools Badge] | [by rdefeo](https://github.com/rdefeo/quac) | Various fixes by @Willy-JL | [![Author Badge]](https://lab.flipper.net/apps/quac) |
 | Key Copier | ![Tools Badge] | [by zinongli](https://github.com/zinongli/KeyCopier) |  | ![None Badge] |
 | USB HID Autofire | ![USB Badge] | [by pbek](https://github.com/pbek/usb_hid_autofire) |  | ![None Badge] |

+ 7 - 0
non_catalog_apps/dcf77_clock_spoof/.gitignore

@@ -0,0 +1,7 @@
+dist/*
+.vscode
+.clang-format
+.clangd
+.editorconfig
+.env
+.ufbt

+ 0 - 0
non_catalog_apps/dcf77_clock_sync/LICENSE → non_catalog_apps/dcf77_clock_spoof/LICENSE


+ 41 - 0
non_catalog_apps/dcf77_clock_spoof/README.md

@@ -0,0 +1,41 @@
+# Flipper-Zero DCF77 Clock Spoof
+
+Spoofs a [DCF77](https://en.wikipedia.org/wiki/DCF77) time signal of a selectable time 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.
+
+## Roadmap
+
+(I will probably working on this very soon)
+
+- Add the option to also change the date. Currently the date can only implicitable be changed by overflow (constantly move hour up until in the next day and vice versa)
+- Add the option to also send "fake" signals that the DCF77 protocol does allow to mess with clocks who don't check that. For example the DCF77 protocol allows to send 25:63 as a "valid" time
+
+## Usage
+
+Normally a nearby clock listening to the DCF77 signal gets synchronized in two to five minutes depending on the signal strength.
+
+Default mode controls:
+- Press OK to toggle the LED blinking
+- Press up/down to toggle between CEST (dst) and CET time
+- Press right/left to enter edit mode 
+
+Edit mode controls:
+- Press right/left to select hours, minutes or seconds to edit
+- Press up/down to edit the selected part of the time (there us overfolow, so e.g. going below 12AM will lead to being at 11AM and subtracting one day from the date)
+- Press OK to save the changes and exit edit mode
+- Press back to discard the changes and exit edit mode
+
+## Antenna
+
+(Disclaimer: For research purposes only. Sending DCF77 is probably illegal in your country, so use this app responsible and only in a small range (therefore you will probably not need an antenna))
+
+The RFID antenna works 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.
+
+## Note
+
+This is a fork of https://github.com/mdaskalov/dcf77-clock-sync. Thanks to @mdaskalov for doing the initial work!

+ 15 - 0
non_catalog_apps/dcf77_clock_spoof/application.fam

@@ -0,0 +1,15 @@
+App(
+    appid="dcf77_clock_spoof",
+    name="DCF77 Clock Spoof",
+    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 & Molodos",
+    fap_weburl="https://github.com/molodos/dcf77-clock-spoof",
+    fap_version="1.1",
+    fap_description="Spoof a DCF77 time signal with a selectable time on the RFID antenna and the GPIO A4 pin",
+)

+ 108 - 0
non_catalog_apps/dcf77_clock_spoof/dcf77.c

@@ -0,0 +1,108 @@
+#include "dcf77.h"
+
+#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
+
+// The signal will contain the time of when the signal ends
+// Bits: 1-bit is signal, 2-bit is prepend dash for visual output and 4-bit is mark as "X" in visual output
+static uint8_t dcf77_bits[] = {
+    0, // 00: Start of minute (Always 0)
+    2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 01: Weather broadcast / Civil warning bits
+    2, // 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 (always 1)
+    2, 0, 0, 0, 0, 0, 0, 0, // 21: Minutes (7bit + even parity, 00-59)
+    2, 0, 0, 0, 0, 0, 0, // 29: Hours (6bit + even parity, 0-23)
+    2, 0, 0, 0, 0, 0, // 36: Day of month (6bit, 1-31)
+    2, 0, 0, // 42: Day of week (3bit, 1-7, Monday=1)
+    2, 0, 0, 0, 0, // 45: Month number (5bit, 1-12)
+    2, 0, 0, 0, 0, 0, 0, 0, 0, // 50: Year within century (8bit + even parity for 36-58, 00-99)
+    6 // 59: Minute mark
+};
+
+/**
+ * Encode a value into a part of a dcf77 signal
+ * @param start Index where the encoded value should start in the sinal bits array
+ * @param len Number of bits in the encoded value
+ * @param val The value to be encoded
+ * @param par The parity flag (0 top disable parity, 1 for even parity bit to be appended to the encoded value, -1 for even parity bit to be appended to the encoded value but being based on the value currently in the first position in the signal bits (overflow from before))
+ */
+void dcf77_encode(int start, int len, int val, int par) {
+    // Set parity init bit
+    uint8_t parity = 0;
+    if(par == -1) {
+        // Use first bit as parity init
+        parity = dcf77_bits[start] & 1;
+    }
+
+    // Parse value into bits (little endian)
+    uint8_t byte = ((val / 10) << 4) + (val % 10);
+
+    // Go through byte from right (low) to left (high)
+    for(int bit = 0; bit < len; bit++) {
+        // Get bit
+        uint8_t dcf77_bit = (byte >> bit) & 1;
+
+        // XOR onto parity
+        parity ^= dcf77_bit;
+
+        // Set bit in signal bits (keep flags for visual output)
+        dcf77_bits[start + bit] = (dcf77_bits[start + bit] & 0x6) + dcf77_bit;
+    }
+
+    // Append parity bit if parity bit is enabled (keep flags for visual output)
+    if(par != 0) {
+        dcf77_bits[start + len] = (dcf77_bits[start + len] & 0x6) + (parity & 1);
+    }
+}
+
+void set_dcf77_time(DateTime* datetime, bool is_dst) {
+    dcf77_encode(DST_BIT, 2, is_dst > 0 ? 0b01 : 0b10, 0); // Disable parity
+    dcf77_encode(MIN_BIT, 7, datetime->minute, 1);
+    dcf77_encode(HOUR_BIT, 6, datetime->hour, 1);
+    dcf77_encode(DAY_BIT, 6, datetime->day, 1);
+    dcf77_encode(WEEKDAY_BIT, 3, datetime->weekday, -1); // Use first bit as parity init
+    dcf77_encode(MONTH_BIT, 5, datetime->month, -1); // Use first bit as parity init
+    dcf77_encode(YEAR_BIT, 8, datetime->year % 100, -1); // Use first bit as parity init
+}
+
+bool get_dcf77_bit(int sec) {
+    // Return the bit for the second
+    return dcf77_bits[sec % 60] & 1;
+}
+
+char* get_dcf77_data(int sec) {
+    // Array for data to be displayed
+    static char data[70];
+
+    // Index
+    int idx = 0;
+
+    // Optimization: Only 25 charcters can be displayed -> don't calculate any more
+    int start = sec > 25 ? sec - 25 : 0;
+    for(int bit = start; bit <= sec; bit++) {
+        // Prepend a dash if 2-bit is set
+        if(dcf77_bits[bit] >> 1 & 1) {
+            data[idx++] = '-';
+        }
+
+        // Only set data is not displayed as "X"
+        if(dcf77_bits[bit] >> 2 & 1) {
+            data[idx++] = 'X';
+        } else {
+            // Set data to last bit (ascii id of 0 plus 0 or 1)
+            data[idx++] = '0' + (dcf77_bits[bit] & 1);
+        }
+    }
+
+    // Terminate string wit null byte and return
+    data[idx] = 0;
+    return data;
+}

+ 23 - 0
non_catalog_apps/dcf77_clock_spoof/dcf77.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <datetime/datetime.h>
+#include <furi.h>
+
+/**
+ * Set the current time and convert it into a dcf77 signal
+ * @param datetime The time to set
+ * @param is_dst If daylight saving time is active
+ */
+void set_dcf77_time(DateTime* datetime, bool is_dst);
+
+/**
+ * Get the signal bit for a second
+ * @param sec The second to get the bit for
+ */
+bool get_dcf77_bit(int sec);
+
+/**
+ * Get the visual signal string to display in UI for a second (cropped to the last 25 bits because more cannot be displayed in one line)
+ * @param sec The second to get the bit for
+ */
+char* get_dcf77_data(int sec);

+ 364 - 0
non_catalog_apps/dcf77_clock_spoof/dcf77_clock_spoof.c

@@ -0,0 +1,364 @@
+#include <furi_hal.h>
+#include <furi_hal_pwm.h>
+#include <gui/gui.h>
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+
+#include <datetime/datetime.h>
+#include <locale/locale.h>
+
+#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
+
+// Weekday translator
+char* WEEKDAYS[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
+
+// Struct for the app
+typedef struct {
+    DateTime dt;
+    bool is_dst;
+    FuriString* str;
+    LocaleTimeFormat tim_fmt;
+    LocaleDateFormat dat_fmt;
+    uint32_t time_offset;
+    int selection;
+    bool blink_led;
+} AppData;
+
+/**
+ * Draw an arrow
+ * @param canvas Canvas to draw to
+ * @param x X
+ * @param y Y
+ * @param up To make the arrow point upwards
+ */
+static void canvas_draw_arrow(Canvas* canvas, int32_t x, int32_t y, bool up) {
+    canvas_draw_box(canvas, x + 2, y + (up ? 0 : 2), 2, 1);
+    canvas_draw_box(canvas, x + 1, y + 1, 4, 1);
+    canvas_draw_box(canvas, x, y + (up ? 2 : 0), 6, 1);
+}
+
+/**
+ * Draw the apps UI
+ * @param canvas The canvas of the UI to draw to
+ * @param context Context (the app data)
+ */
+static void app_draw_callback(Canvas* canvas, void* context) {
+    // Load app data
+    AppData* app = (AppData*)context;
+    furi_assert(app->str);
+
+    // Get hour and format it according to 12h/24h time format
+    uint8_t hour = app->dt.hour;
+    bool fmt_12h = false;
+    if(app->tim_fmt == LocaleTimeFormat12h) {
+        hour = hour == 0 ? 12 : hour % 12;
+        fmt_12h = true;
+    }
+
+    // Create string for displayed time
+    furi_string_printf(app->str, "%2u:%02u:%02u", hour, app->dt.minute, app->dt.second);
+    const char* tim_cstr = furi_string_get_cstr(app->str);
+
+    // Print time string to UI
+    canvas_set_font(canvas, FontBigNumbers);
+    canvas_draw_str_aligned(
+        canvas, SCREEN_SIZE_X / 2, SCREEN_SIZE_Y / 2, AlignCenter, AlignCenter, tim_cstr);
+
+    // Print AM/PM to the UI for 12h format
+    if(fmt_12h) {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str_aligned(
+            canvas,
+            SCREEN_SIZE_X,
+            (SCREEN_SIZE_Y / 2),
+            AlignRight,
+            AlignTop,
+            (app->dt.hour >= 12 ? "PM" : "AM"));
+    }
+
+    // Create the date string ("lite")
+    FuriString* dat = furi_string_alloc();
+    locale_format_date(dat, &app->dt, app->dat_fmt, "-");
+
+    // Create strings for weekday and dst
+    const char* dow_str = WEEKDAYS[(app->dt.weekday - 1) % 7];
+    const char* dst_str = app->is_dst ? "CEST" : "CET";
+
+    // Combine to overall date string and free "lite" date string
+    furi_string_printf(app->str, "%s %s %s", dow_str, furi_string_get_cstr(dat), dst_str);
+    furi_string_free(dat);
+
+    // Print date string to the UI
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(
+        canvas, SCREEN_SIZE_X / 2, 0, AlignCenter, AlignTop, furi_string_get_cstr(app->str));
+
+    // Print signal data string to the UI
+    char* data = get_dcf77_data(app->dt.second);
+    canvas_draw_str_aligned(canvas, SCREEN_SIZE_X, SCREEN_SIZE_Y, AlignRight, AlignBottom, data);
+
+    // Add markers for selection
+    if(app->selection > 0) {
+        canvas_set_color(canvas, ColorBlack);
+        canvas_draw_arrow(canvas, 19 + 36 * (app->selection - 1), SCREEN_SIZE_Y / 2 + 8, true);
+        canvas_draw_arrow(canvas, 31 + 36 * (app->selection - 1), SCREEN_SIZE_Y / 2 + 8, false);
+    }
+}
+
+/**
+ * Handle an input event
+ * @param input_event The input event to handle
+ * @param context Context (the message queue)
+ */
+static void app_input_callback(InputEvent* input_event, void* context) {
+    // Get the event ant put it into the context message queue
+    furi_assert(context);
+    FuriMessageQueue* event_queue = context;
+    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
+}
+
+/**
+ * Set time for dcf77 converter
+ * @param app The app data
+ * @param offset The time offset to current time in seconds
+ */
+void set_signal_time(AppData* app, int offset) {
+    // Prepare date
+    DateTime dcf_dt;
+
+    // Get time and add offset because a later time will be transmitted
+    uint32_t timestamp = datetime_datetime_to_timestamp(&app->dt) + offset;
+
+    // Convert timestamp to date and date to dcf77 signal
+    datetime_timestamp_to_datetime(timestamp, &dcf_dt);
+    set_dcf77_time(&dcf_dt, app->is_dst);
+}
+
+/**
+ * Get datetime current offset
+ * @param app The app to update datetime for
+ */
+void update_offset_datetime(AppData* app) {
+    // Set offset time to current time from flipper
+    DateTime offset_time;
+    furi_hal_rtc_get_datetime(&offset_time);
+
+    // Get unix timestamp
+    uint32_t unix_timestamp = datetime_datetime_to_timestamp(&offset_time);
+
+    // Add offset
+    unix_timestamp += app->time_offset;
+
+    // Save to offset time
+    datetime_timestamp_to_datetime(unix_timestamp, &offset_time);
+
+    // Write to
+    app->dt = offset_time;
+}
+
+/**
+ * Init point of the app
+ * @param p Args (I guess)
+ */
+int dcf77_clock_sync_app_main(void* p) {
+    // Ignore args
+    UNUSED(p);
+
+    // Init app data struct
+    AppData* app = malloc(sizeof(AppData));
+    app->is_dst = true;
+    app->str = furi_string_alloc();
+    app->tim_fmt = locale_get_time_format();
+    app->dat_fmt = locale_get_date_format();
+    app->time_offset = 0;
+    app->selection = 0;
+    app->blink_led = true;
+    update_offset_datetime(app);
+
+    // Set the current signal time (add offset of one minute because signal of next minute is sent)
+    set_signal_time(app, DCF77_OFFSET);
+
+    // Create viewport as well as message queue for input events (max 8 events)
+    ViewPort* view_port = view_port_alloc();
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+    // Set viewport callbacks for drawing the UI and handling input events (using app and event queue as context parameters)
+    view_port_draw_callback_set(view_port, app_draw_callback, app);
+    view_port_input_callback_set(view_port, app_input_callback, event_queue);
+
+    // Create gui and display already created viewport
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    // Event store for later
+    InputEvent event;
+
+    // Flag to show that signal is being sent
+    bool running = false;
+
+    // Flag to show that user pressed back
+    bool exit = false;
+
+    // Current second
+    int sec = app->dt.second;
+
+    // Settings backup
+    uint32_t offset = app->time_offset;
+
+    // Main app loop
+    while(!exit) {
+        // Silence between signals
+        int silence_ms = 0;
+
+        // Wait until next second starts
+        while(app->dt.second == sec) {
+            update_offset_datetime(app);
+        }
+
+        // Control antennas
+        if(app->dt.second < 59) {
+            // Stop if running
+            if(running) {
+                // Turn led off
+                if(app->blink_led) {
+                    furi_hal_light_set(LightRed | LightGreen | LightBlue, 0);
+                }
+
+                // Stop signal
+                furi_hal_rfid_tim_read_stop();
+                furi_hal_pwm_stop(FuriHalPwmOutputIdLptim2PA4);
+                furi_hal_gpio_init(
+                    &gpio_ext_pa4, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
+            }
+
+            // Pause 200ms for a one and 100ms for a 0
+            silence_ms = get_dcf77_bit(app->dt.second) ? 200 : 100;
+            furi_delay_ms(silence_ms);
+
+            // Send signal
+            furi_hal_rfid_tim_read_start(DCF77_FREQ, 0.5);
+            furi_hal_pwm_start(FuriHalPwmOutputIdLptim2PA4, DCF77_FREQ, 50);
+
+            // Turn led on
+            if(app->blink_led) {
+                furi_hal_light_set(LightRed | LightGreen, 0xFF);
+            }
+
+            // Set flag that signal is bein sent
+            running = true;
+        } else {
+            // Update time for next minute (add offset because time of one minute later than next minute will be sent)
+            set_signal_time(app, DCF77_OFFSET + 1);
+        }
+
+        // Get current time and calculate wait time until just before next second
+        sec = app->dt.second;
+        int wait_ms = 1000 - silence_ms - SYNC_DELAY;
+        uint32_t tick_start = furi_get_tick();
+
+        // Number of selectible parts
+        int max_selection = 3;
+
+        // Wait and handle stuff
+        while((int)(furi_get_tick() - tick_start) < wait_ms) {
+            // Handle key inputs
+            FuriStatus status = furi_message_queue_get(event_queue, &event, wait_ms);
+            if(status == FuriStatusOk && event.type == InputTypePress) {
+                if(event.key == InputKeyBack) {
+                    if(app->selection != 0) {
+                        // End without saving if in edit mode (restore settings backup)
+                        app->time_offset = offset;
+                        update_offset_datetime(app);
+                        app->selection = 0;
+                    } else {
+                        // Signal to exit if not in edit mode
+                        exit = true;
+                        break;
+                    }
+                } else if(event.key == InputKeyRight) {
+                    // Circle selection
+                    app->selection = app->selection == max_selection ? 1 : app->selection + 1;
+                } else if(event.key == InputKeyLeft) {
+                    // Circle selection
+                    app->selection = app->selection <= 1 ? max_selection : app->selection - 1;
+                } else if(event.key == InputKeyOk) {
+                    if(app->selection != 0) {
+                        // End with saving if in edit mode (update settings backup)
+                        offset = app->time_offset;
+                        app->selection = 0;
+                    } else {
+                        // Toggle LED if not in edit mode
+                        app->blink_led = !app->blink_led;
+                        if(!app->blink_led) {
+                            furi_hal_light_set(LightRed | LightGreen | LightBlue, 0);
+                        }
+                    }
+                } else if(event.key == InputKeyUp) {
+                    if(app->selection == 1) {
+                        app->time_offset += 60 * 60;
+                    } else if(app->selection == 2) {
+                        app->time_offset += 60;
+                    } else if(app->selection == 3) {
+                        app->time_offset += 1;
+                    } else if(app->selection == 0) {
+                        app->is_dst = !app->is_dst;
+                    }
+                    update_offset_datetime(app);
+                } else if(event.key == InputKeyDown) {
+                    if(app->selection == 1) {
+                        app->time_offset -= 60 * 60;
+                    } else if(app->selection == 2) {
+                        app->time_offset -= 60;
+                    } else if(app->selection == 3) {
+                        app->time_offset -= 1;
+                    } else if(app->selection == 0) {
+                        app->is_dst = !app->is_dst;
+                    }
+                    update_offset_datetime(app);
+                }
+            }
+
+            // Refresh screen
+            view_port_update(view_port);
+
+            // Handle other stuff
+            if(status == FuriStatusErrorTimeout) {
+                break;
+            }
+        }
+    }
+
+    // Stop if running
+    if(running) {
+        furi_hal_rfid_tim_read_stop();
+        furi_hal_pwm_stop(FuriHalPwmOutputIdLptim2PA4);
+
+        if(app->blink_led) {
+            furi_hal_light_set(LightRed | LightGreen | LightBlue, 0);
+        }
+    }
+
+    // Disable the viewport
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+
+    // Close records previously opened
+    furi_record_close(RECORD_GUI);
+
+    // Free input event queue and viewport
+    furi_message_queue_free(event_queue);
+    view_port_free(view_port);
+
+    // Free string in app data as well as app data itself
+    furi_string_free(app->str);
+    free(app);
+
+    // Exit normally
+    return 0;
+}

BIN
non_catalog_apps/dcf77_clock_spoof/icons/app_10x10.png


+ 0 - 17
non_catalog_apps/dcf77_clock_sync/README.md

@@ -1,17 +0,0 @@
-# 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.

+ 0 - 15
non_catalog_apps/dcf77_clock_sync/application.fam

@@ -1,15 +0,0 @@
-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",
-    fap_version="1.3",
-    fap_description="Emulate DCF77 time signal on the RFID antenna and the A4 GPIO pin",
-)

+ 0 - 64
non_catalog_apps/dcf77_clock_sync/dcf77.c

@@ -1,64 +0,0 @@
-#include "dcf77.h"
-
-#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;
-}

+ 0 - 8
non_catalog_apps/dcf77_clock_sync/dcf77.h

@@ -1,8 +0,0 @@
-#pragma once
-
-#include <datetime/datetime.h>
-#include <furi.h>
-
-void set_dcf77_time(DateTime* dt, bool is_dst);
-bool get_dcf77_bit(int sec);
-char* get_dcf77_data(int sec);

+ 0 - 176
non_catalog_apps/dcf77_clock_sync/dcf77_clock_sync.c

@@ -1,176 +0,0 @@
-#include <furi_hal.h>
-#include <furi_hal_pwm.h>
-#include <gui/gui.h>
-#include <notification/notification.h>
-#include <notification/notification_messages.h>
-
-#include <datetime/datetime.h>
-#include <locale/locale.h>
-
-#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
-
-char* WEEKDAYS[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
-
-typedef struct {
-    DateTime dt;
-    bool is_dst;
-    FuriString* str;
-    LocaleTimeFormat tim_fmt;
-    LocaleDateFormat dat_fmt;
-} AppData;
-
-static void app_draw_callback(Canvas* canvas, void* context) {
-    AppData* app = (AppData*)context;
-    furi_assert(app->str);
-
-    uint8_t hour = app->dt.hour;
-    bool fmt_12h = false;
-    if(app->tim_fmt == LocaleTimeFormat12h) {
-        hour = hour == 0 ? 12 : hour % 12;
-        fmt_12h = true;
-    }
-
-    furi_string_printf(app->str, "%2u:%02u:%02u", hour, app->dt.minute, app->dt.second);
-    const char* tim_cstr = furi_string_get_cstr(app->str);
-
-    canvas_set_font(canvas, FontBigNumbers);
-    canvas_draw_str_aligned(
-        canvas, SCREEN_SIZE_X / 2, SCREEN_SIZE_Y / 2, AlignCenter, AlignCenter, tim_cstr);
-
-    if(fmt_12h) {
-        canvas_set_font(canvas, FontSecondary);
-        canvas_draw_str_aligned(
-            canvas,
-            0,
-            (SCREEN_SIZE_Y / 2) - 7,
-            AlignLeft,
-            AlignTop,
-            (app->dt.hour >= 12 ? "PM" : "AM"));
-    }
-
-    FuriString* dat = furi_string_alloc();
-    locale_format_date(dat, &app->dt, app->dat_fmt, "-");
-    const char* dow_str = WEEKDAYS[(app->dt.weekday - 1) % 7];
-    const char* dst_str = app->is_dst ? "CEST" : "CET";
-    furi_string_printf(app->str, "%s %s %s", dow_str, furi_string_get_cstr(dat), dst_str);
-    furi_string_free(dat);
-
-    canvas_set_font(canvas, FontSecondary);
-    canvas_draw_str_aligned(
-        canvas, SCREEN_SIZE_X / 2, 0, AlignCenter, AlignTop, furi_string_get_cstr(app->str));
-
-    if(app->dt.second < 59) {
-        char* data = get_dcf77_data(app->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 set_time(AppData* app, int offset) {
-    DateTime dcf_dt;
-    uint32_t timestamp = datetime_datetime_to_timestamp(&app->dt) + offset;
-    datetime_timestamp_to_datetime(timestamp, &dcf_dt);
-    set_dcf77_time(&dcf_dt, app->is_dst);
-}
-
-int dcf77_clock_sync_app_main(void* p) {
-    UNUSED(p);
-
-    AppData* app = malloc(sizeof(AppData));
-    furi_hal_rtc_get_datetime(&app->dt);
-    app->is_dst = false;
-    app->str = furi_string_alloc();
-    app->tim_fmt = locale_get_time_format();
-    app->dat_fmt = locale_get_date_format();
-
-    set_time(app, DCF77_OFFSET);
-
-    ViewPort* view_port = view_port_alloc();
-    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
-
-    view_port_draw_callback_set(view_port, app_draw_callback, app);
-    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);
-
-    InputEvent event;
-    bool running = false;
-    bool exit = false;
-    int sec = app->dt.second;
-    while(!exit) {
-        int silence_ms = 0;
-        // wait next second
-        while(app->dt.second == sec) furi_hal_rtc_get_datetime(&app->dt);
-
-        if(app->dt.second < 59) {
-            if(running) {
-                notification_message(notification, &sequence_reset_rgb);
-                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->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);
-            notification_message(notification, &sequence_set_only_blue_255);
-            running = true;
-        } else
-            set_time(app, DCF77_OFFSET + 1);
-
-        sec = app->dt.second;
-        int wait_ms = 1000 - silence_ms - SYNC_DELAY;
-        uint32_t tick_start = furi_get_tick();
-        while(wait_ms > 0) {
-            FuriStatus status = furi_message_queue_get(event_queue, &event, wait_ms);
-            if((status == FuriStatusOk) && (event.type == InputTypePress)) {
-                if(event.key == InputKeyOk)
-                    app->is_dst = !app->is_dst;
-                else if(event.key == InputKeyBack) {
-                    exit = true;
-                    break;
-                }
-            }
-            view_port_update(view_port);
-            if(status == FuriStatusErrorTimeout) break;
-            wait_ms -= furi_get_tick() - tick_start;
-        }
-    }
-
-    if(running) {
-        furi_hal_rfid_tim_read_stop();
-        furi_hal_pwm_stop(FuriHalPwmOutputIdLptim2PA4);
-    }
-
-    notification_message_block(notification, &sequence_display_backlight_enforce_auto);
-    notification_message(notification, &sequence_reset_rgb);
-
-    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);
-
-    furi_string_free(app->str);
-    free(app);
-
-    return 0;
-}

BIN
non_catalog_apps/dcf77_clock_sync/icons/app_10x10.png


BIN
non_catalog_apps/dcf77_clock_sync/img/1.png