Skip to content

Commit 1e6a122

Browse files
committed
add 2nd dcf77 clock sync app
1 parent d56472e commit 1e6a122

File tree

8 files changed

+314
-0
lines changed

8 files changed

+314
-0
lines changed
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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.git",
13+
fap_version="1.1",
14+
fap_description="Emulate DCF77 time signal on the RFID antena and the A4 GPIO pin",
15+
)
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#include <furi_hal.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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#pragma once
2+
3+
#include <furi_hal.h>
4+
5+
void set_dcf77_time(DateTime* dt, bool is_dst);
6+
int get_dcf77_bit(int sec);
7+
char* get_dcf77_data(int sec);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#include <furi_hal.h>
2+
#include <furi_hal_pwm.h>
3+
#include <gui/gui.h>
4+
#include <notification/notification.h>
5+
#include <notification/notification_messages.h>
6+
7+
#include "dcf77.h"
8+
9+
#define SCREEN_SIZE_X 128
10+
#define SCREEN_SIZE_Y 64
11+
#define DCF77_FREQ 77500
12+
#define DCF77_OFFSET 60
13+
#define SYNC_DELAY 50
14+
#define UPDATES 8
15+
16+
#define SECONDS_PER_MINUTE 60
17+
#define SECONDS_PER_HOUR (SECONDS_PER_MINUTE * 60)
18+
#define SECONDS_PER_DAY (SECONDS_PER_HOUR * 24)
19+
#define MONTHS_COUNT 12
20+
#define EPOCH_START_YEAR 1970
21+
22+
char* WEEKDAYS[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
23+
24+
typedef struct {
25+
DateTime dt;
26+
DateTime dcf_dt;
27+
bool is_dst;
28+
} AppData;
29+
30+
static void app_draw_callback(Canvas* canvas, void* context) {
31+
AppData* app_data = (AppData*)context;
32+
33+
char buffer[64];
34+
35+
snprintf(
36+
buffer,
37+
sizeof(buffer),
38+
"%02u:%02u:%02u",
39+
app_data->dt.hour,
40+
app_data->dt.minute,
41+
app_data->dt.second);
42+
43+
canvas_set_font(canvas, FontBigNumbers);
44+
canvas_draw_str_aligned(
45+
canvas, SCREEN_SIZE_X / 2, SCREEN_SIZE_Y / 2, AlignCenter, AlignCenter, buffer);
46+
47+
const char* dow_str = WEEKDAYS[(app_data->dt.weekday - 1) % 7];
48+
const char* dst_str = app_data->is_dst ? "CEST" : "CET";
49+
snprintf(
50+
buffer,
51+
sizeof(buffer),
52+
"%s %02u-%02u-%04u %s",
53+
dow_str,
54+
app_data->dt.day,
55+
app_data->dt.month,
56+
app_data->dt.year,
57+
dst_str);
58+
59+
canvas_set_font(canvas, FontSecondary);
60+
canvas_draw_str_aligned(canvas, SCREEN_SIZE_X / 2, 0, AlignCenter, AlignTop, buffer);
61+
62+
if(app_data->dt.second < 59) {
63+
char* data = get_dcf77_data(app_data->dt.second);
64+
canvas_draw_str_aligned(
65+
canvas, SCREEN_SIZE_X, SCREEN_SIZE_Y, AlignRight, AlignBottom, data);
66+
}
67+
}
68+
69+
static void app_input_callback(InputEvent* input_event, void* ctx) {
70+
furi_assert(ctx);
71+
FuriMessageQueue* event_queue = ctx;
72+
furi_message_queue_put(event_queue, input_event, FuriWaitForever);
73+
}
74+
75+
void time_add(DateTime* from, DateTime* to, int add) {
76+
uint32_t timestamp = datetime_datetime_to_timestamp(from) + add;
77+
78+
uint32_t days = timestamp / SECONDS_PER_DAY;
79+
uint32_t seconds_in_day = timestamp % SECONDS_PER_DAY;
80+
81+
to->year = EPOCH_START_YEAR;
82+
83+
while(days >= datetime_get_days_per_year(to->year)) {
84+
days -= datetime_get_days_per_year(to->year);
85+
(to->year)++;
86+
}
87+
88+
to->month = 1;
89+
while(days >= datetime_get_days_per_month(datetime_is_leap_year(to->year), to->month)) {
90+
days -= datetime_get_days_per_month(datetime_is_leap_year(to->year), to->month);
91+
(to->month)++;
92+
}
93+
94+
to->weekday = ((days + 4) % 7) + 1;
95+
96+
to->day = days + 1;
97+
to->hour = seconds_in_day / SECONDS_PER_HOUR;
98+
to->minute = (seconds_in_day % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
99+
to->second = seconds_in_day % SECONDS_PER_MINUTE;
100+
}
101+
102+
int dcf77_clock_sync_app_main(void* p) {
103+
UNUSED(p);
104+
105+
AppData app_data;
106+
InputEvent event;
107+
108+
app_data.is_dst = false;
109+
furi_hal_rtc_get_datetime(&app_data.dt);
110+
time_add(&app_data.dt, &app_data.dcf_dt, DCF77_OFFSET);
111+
set_dcf77_time(&app_data.dcf_dt, app_data.is_dst);
112+
113+
ViewPort* view_port = view_port_alloc();
114+
FuriMessageQueue* event_queue = furi_messa ge_queue_alloc(8, sizeof(InputEvent));
115+
116+
view_port_draw_callback_set(view_port, app_draw_callback, &app_data);
117+
view_port_input_callback_set(view_port, app_input_callback, event_queue);
118+
119+
Gui* gui = furi_record_open(RECORD_GUI);
120+
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
121+
122+
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
123+
notification_message_block(notification, &sequence_display_backlight_enforce_on);
124+
125+
bool running = false;
126+
bool exit = false;
127+
int sec = app_data.dt.second;
128+
while(!exit) {
129+
int silence_ms = 0;
130+
// wait next second
131+
while(app_data.dt.second == sec) furi_hal_rtc_get_datetime(&app_data.dt);
132+
133+
if(app_data.dt.second < 59) {
134+
furi_hal_light_set(LightRed | LightGreen | LightBlue, 0);
135+
if(running) {
136+
furi_hal_rfid_tim_read_stop();
137+
furi_hal_pwm_stop(FuriHalPwmOutputIdLptim2PA4);
138+
furi_hal_gpio_init(
139+
&gpio_ext_pa4, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
140+
}
141+
silence_ms = get_dcf77_bit(app_data.dt.second) ? 200 : 100;
142+
furi_delay_ms(silence_ms);
143+
furi_hal_rfid_tim_read_start(DCF77_FREQ, 0.5);
144+
furi_hal_pwm_start(FuriHalPwmOutputIdLptim2PA4, DCF77_FREQ, 50);
145+
running = true;
146+
furi_hal_light_set(LightBlue, 0xFF);
147+
} else {
148+
time_add(&app_data.dt, &app_data.dcf_dt, DCF77_OFFSET + 1);
149+
set_dcf77_time(&app_data.dcf_dt, app_data.is_dst);
150+
}
151+
152+
sec = app_data.dt.second;
153+
int wait_ms = (1000 - silence_ms - SYNC_DELAY) / UPDATES;
154+
for(int i = 0; i < UPDATES; i++) {
155+
if(furi_message_queue_get(event_queue, &event, wait_ms) == FuriStatusOk) {
156+
if((event.type == InputTypePress) || (event.type == InputTypeRepeat)) {
157+
switch(event.key) {
158+
case InputKeyOk:
159+
app_data.is_dst = !app_data.is_dst;
160+
break;
161+
case InputKeyBack:
162+
exit = true;
163+
break;
164+
default:
165+
break;
166+
}
167+
}
168+
}
169+
view_port_update(view_port);
170+
if(exit) break;
171+
}
172+
}
173+
174+
if(running) {
175+
furi_hal_rfid_tim_read_stop();
176+
furi_hal_pwm_stop(FuriHalPwmOutputIdLptim2PA4);
177+
furi_hal_light_set(LightRed | LightGreen | LightBlue, 0);
178+
}
179+
180+
notification_message_block(notification, &sequence_display_backlight_enforce_auto);
181+
182+
view_port_enabled_set(view_port, false);
183+
gui_remove_view_port(gui, view_port);
184+
furi_record_close(RECORD_NOTIFICATION);
185+
furi_record_close(RECORD_GUI);
186+
furi_message_queue_free(event_queue);
187+
view_port_free(view_port);
188+
189+
return 0;
190+
}
Loading
1.81 KB
Loading

0 commit comments

Comments
 (0)