Ver código fonte

add 2nd dcf77 clock sync app

MX 1 ano atrás
pai
commit
1e6a122c54

+ 21 - 0
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.

+ 17 - 0
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.

+ 15 - 0
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",
+)

+ 64 - 0
non_catalog_apps/dcf77_clock_sync/dcf77.c

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

+ 7 - 0
non_catalog_apps/dcf77_clock_sync/dcf77.h

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

+ 190 - 0
non_catalog_apps/dcf77_clock_sync/dcf77_clock_sync.c

@@ -0,0 +1,190 @@
+#include <furi_hal.h>
+#include <furi_hal_pwm.h>
+#include <gui/gui.h>
+#include <notification/notification.h>
+#include <notification/notification_messages.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
+#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;
+}

BIN
non_catalog_apps/dcf77_clock_sync/icons/app_10x10.png


BIN
non_catalog_apps/dcf77_clock_sync/img/1.png