Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add longwave_clock app #43

Open
wants to merge 16 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions longwave_clock/.github/workflows/build-longwaveclock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: "longwave_clock: build for multiple SDK sources"
on:
push:
branches:
- main
pull_request:
branches:
- '**'
jobs:
ufbt-build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: Official Dev channel
sdk-channel: dev
- name: Official Release channel
sdk-channel: release
- name: Unleashed Dev
sdk-index-url: https://up.unleashedflip.com/directory.json
sdk-channel: dev
- name: Unleashed Release
sdk-index-url: https://up.unleashedflip.com/directory.json
sdk-channel: release
- name: Momentum Dev
sdk-index-url: https://up.momentum-fw.dev/firmware/directory.json
sdk-channel: dev
- name: Momentum Release
sdk-index-url: https://up.momentum-fw.dev/firmware/directory.json
sdk-channel: release
name: 'longwave_clock: ufbt build for ${{ matrix.name }}'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build with ufbt
uses: flipperdevices/[email protected]
id: build-app
with:
sdk-channel: ${{ matrix.sdk-channel }}
sdk-index-url: ${{ matrix.sdk-index-url }}
app-dir: .
- name: Upload app artifacts
uses: actions/upload-artifact@v4
with:
name: longwave_clock-${{ steps.build-app.outputs.suffix }}
path: ${{ steps.build-app.outputs.fap-artifacts }}
- name: Lint sources
uses: flipperdevices/[email protected]
with:
# skip SDK setup, we already did it in previous step
skip-setup: true
task: lint
3 changes: 3 additions & 0 deletions longwave_clock/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.*
!.gitignore
!.github
2 changes: 2 additions & 0 deletions longwave_clock/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
v0.1:
First version, working DCF77 and MSF both demo and GPIO modes.
661 changes: 661 additions & 0 deletions longwave_clock/LICENSE

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions longwave_clock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Longwave Clock

This is a Flipper Zero app to receive and decode, or simulate, multiple time signal broadcasts with different protocols and time formats. For receiving via GPIO, an inexpensive receiver connected to a receiving pin is required.

![](screenshots/v0.1/animation.gif)

## Protocol support

### DCF77 (Europe, Germany)

![](screenshots/v0.1/menu_dcf77.png) ![](screenshots/v0.1/dcf77_1.png) ![](screenshots/v0.1/dcf77_2.png)

[DCF77](https://en.wikipedia.org/wiki/DCF77) is broadcasted from Frankfurt am Main in Germany ([50.0155,9.0108](https://www.openstreetmap.org/?mlat=50.0155&mlon=9.0108#map=4/50.01/9.01)) and requires an antenna tuned to 77.5 kHz.

- The radio transmission can be received all over Europe (~2000 km from the sender).
- 1 bit per second is transmitted by reducing carrier power at the beginning of every second.
- The transmission encodes time, date as well as catastrophe and weather information (encrypted, not decoded).

### MSF (Europe, United Kingdom)

![](screenshots/v0.1/menu_msf.png) ![](screenshots/v0.1/msf_1.png) ![](screenshots/v0.1/msf_2.png)

[MSF (Time from NPL/Rugby clock)](https://en.wikipedia.org/wiki/Time_from_NPL_(MSF)) is broadcasted from Anthorn in the UK ([54.9116,-3.2785](https://www.openstreetmap.org/?mlat=54.9116&mlon=-3.2785#map=5/54.91/-3.27)) and requires an antenna tuned to 60 kHz (as does [WWVB](#wwvb-north-america-us)).

- The radio transmission can be received over most of western and northern Europe.
- The transmission encodes time, date as well as DUT1 bits (difference between atomic and astronomical time).
- The app only supports the slow code at 120 bits per minute, of which only 60bits are encoded.
- Same frequency as WWVB - in Europe you will receive this signal through a WWVB receiver.

### WWVB (North America, US)

[WWVB](https://en.wikipedia.org/wiki/WWVB) transmits on 60 kHz and is on the backlog for the Longwave app.

If you're based in the US and would like to help: PRs are welcome!

## GPIO modules

The app supports a demonstration mode as well as GPIO mode. For GPIO mode, external modules are required.

In GPIO mode the following configuration is available (per protocol):
- GPIO data: use "inverted" if the module outputs logic high to the data pin when the sender signal is low (hopefully rare).
- Data pin: this configures the receiving pin on the flipper, C0 is the default and recommended pin.

### Supported modules

The following shows modules that I own and have been successfully tested for reception.

#### 77.5 kHz module (DCF77)

![](screenshots/modules/module_775.jpg)

You can find this type of module by searching for "DCF77 module" in any electronics online shop.

#### 60.0 kHz module (MSF, WWVB)

![](screenshots/modules/module_600.jpg)

Search for "WWVB module" in any electronics online shop.
This applies even if you want to receive MSF instead: they use the same frequency.

### Pinout

The modules I checked are pretty much the same, here is the common pinout configuration I found and tested.
Please check with the manufacturer, as yours might be different and you may cause damage by wiring the module incorrectly.

- **VDD**: flipper pin 9 (3V3)
- **GND** / unlabeled: flipper pin 11 (GND)
- **PON** / **P**: power on, asks for "logic low", using flipper pin 11 (GND)
- **OUT** / **T**: the data signal, using flipper pin 16 (C0)

## Picture credits

- The [Font “HaxrCorp 4089”](https://fontstruct.com/fontstructions/show/192981) by “sahwar” is licensed under a CC-BY-SA license.
- The picture of a flipper zero is (C) [Flipper Devices Inc](https://flipperzero.one/).
16 changes: 16 additions & 0 deletions longwave_clock/application.fam
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# For details & more options, see documentation/AppManifests.md in firmware repo

App(
appid="longwave_clock", # Must be unique
name="Longwave Clock", # Displayed in menus
apptype=FlipperAppType.EXTERNAL,
entry_point="longwave_clock_app",
stack_size=2 * 1024,
fap_icon_assets="images",
fap_category="GPIO",
fap_version="0.1",
fap_icon="lcw.png",
fap_description="Decode or demonstrate long wave time signals",
fap_author="@m7i-org",
fap_weburl="https://github.com/flipper_longwave_clock",
)
Binary file added longwave_clock/images/lwc_dcf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/images/lwc_msf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/images/lwc_sender.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/lcw.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions longwave_clock/screenshots/bin/00_take_screenshots.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

sleep 2;
echo -n START;
while true; do
echo -n '.'
scrot '%Y-%m-%d-%H:%M:%S.png' &
sleep 0.9
done;
21 changes: 21 additions & 0 deletions longwave_clock/screenshots/bin/10_to_gif.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash

res="$(cd "$(dirname $0)"; pwd;)/../res"
t=$(mktemp -d);
mkdir $t/1 $t/2 $t/3 $t/4
echo "Working in $t";

n=0;
for i in $(ls *.png | sort); do
convert $i -alpha remove +repage -crop 512x256+1120+626 +repage $t/1/$i;
convert $t/1/$i -resize 250x125 $t/2/$i;
composite -geometry +195+420 $t/2/$i $res/gpio_simple.png $t/3/$i
composite -geometry +415+0 $res/wave_$((n%3+1)).png $t/3/$i $t/4/$i
n=$(($n+1));
done;

convert -delay 100 -loop 0 -layers Optimize $t/4/*.png animation.gif
# Open in GIMP
# 1. Image>Mode>Indexed>Generate optimum palette
# 2. Filters>Animation>Optimize (for GIF)
# 3. File>Export As...
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/modules/module_775.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/res/gpio_simple.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/res/wave_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/res/wave_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/res/wave_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/v0.1/animation.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/v0.1/big_dcf77.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/v0.1/big_menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/v0.1/big_msf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/v0.1/dcf77_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/v0.1/dcf77_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/v0.1/menu_dcf77.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/v0.1/menu_msf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/v0.1/msf_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added longwave_clock/screenshots/v0.1/msf_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
139 changes: 139 additions & 0 deletions longwave_clock/src/app_state.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#include "app_state.h"
#include "longwave_clock_app.h"

static LWCScene protocol_scene[] = {LWCDCF77Scene, LWCMSFScene};

App* app_alloc() {
App* app = malloc(sizeof(App));
app->scene_manager = scene_manager_alloc(&lwc_scene_manager_handlers, app);
app->view_dispatcher = view_dispatcher_alloc();
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
view_dispatcher_set_custom_event_callback(app->view_dispatcher, lwc_custom_callback);
view_dispatcher_set_navigation_event_callback(app->view_dispatcher, lwc_back_event_callback);
view_dispatcher_set_tick_event_callback(
app->view_dispatcher, lwc_tick_event_callback, furi_ms_to_ticks(100));

app->main_menu = submenu_alloc();
app->sub_menu = variable_item_list_alloc();
app->about = text_box_alloc();
app->info_text = text_box_alloc();
app->dcf77_view = lwc_dcf77_scene_alloc();
app->msf_view = lwc_msf_scene_alloc();
app->notifications = furi_record_open(RECORD_NOTIFICATION);

view_dispatcher_add_view(
app->view_dispatcher, LWCMainMenuView, submenu_get_view(app->main_menu));
view_dispatcher_add_view(
app->view_dispatcher, LWCSubMenuView, variable_item_list_get_view(app->sub_menu));
view_dispatcher_add_view(app->view_dispatcher, LWCAboutView, text_box_get_view(app->about));
view_dispatcher_add_view(app->view_dispatcher, LWCInfoView, text_box_get_view(app->info_text));
view_dispatcher_add_view(app->view_dispatcher, LWCDCF77View, app->dcf77_view);
view_dispatcher_add_view(app->view_dispatcher, LWCMSFView, app->msf_view);
return app;
}

AppState* app_state_alloc() {
AppState* state = malloc(sizeof(AppState));

state->storage = furi_record_open(RECORD_STORAGE);

for(uint8_t i = 0; i < __lwc_number_of_protocols; i++) {
state->proto_configs[i] = malloc(sizeof(ProtoConfig));
File* file = storage_file_alloc(state->storage);
bool read = false;
if(storage_file_open(
file, get_protocol_config_filename((LWCType)i), FSAM_READ, FSOM_OPEN_EXISTING)) {
read = storage_file_read(file, state->proto_configs[i], sizeof(ProtoConfig)) ==
sizeof(ProtoConfig);
}

if(!read) {
state->proto_configs[i]->run_mode = Demo;
state->proto_configs[i]->data_pin = GPIOPinC0;
state->proto_configs[i]->data_mode = Regular;
}
storage_file_close(file);
storage_file_free(file);
}

state->display_on = false;
state->gpio = NULL;

return state;
}

void store_proto_config(AppState* app_state) {
File* file = storage_file_alloc(app_state->storage);
if(storage_file_open(
file,
get_protocol_config_filename(app_state->lwc_type),
FSAM_WRITE,
FSOM_CREATE_ALWAYS)) {
if(!storage_file_write(
file, app_state->proto_configs[app_state->lwc_type], sizeof(ProtoConfig))) {
FURI_LOG_E(TAG, "Failed to write to open proto config file.");
}
} else {
FURI_LOG_E(TAG, "Failed to open proto config file for writing.");
}

storage_file_close(file);
storage_file_free(file);
}

void app_init_lwc(App* app, LWCType type) {
app->state->lwc_type = type;
}

void app_quit(App* app) {
scene_manager_stop(app->scene_manager);
}

void app_free(App* app) {
furi_assert(app);

FURI_LOG_D(TAG, "Removing the view dispatcher views.");
view_dispatcher_remove_view(app->view_dispatcher, LWCMainMenuView);
view_dispatcher_remove_view(app->view_dispatcher, LWCSubMenuView);
view_dispatcher_remove_view(app->view_dispatcher, LWCAboutView);
view_dispatcher_remove_view(app->view_dispatcher, LWCInfoView);
view_dispatcher_remove_view(app->view_dispatcher, LWCDCF77View);
view_dispatcher_remove_view(app->view_dispatcher, LWCMSFView);

FURI_LOG_D(TAG, "Removing the scene manager and view dispatcher.");
scene_manager_free(app->scene_manager);
view_dispatcher_free(app->view_dispatcher);

FURI_LOG_D(TAG, "Removing the single scenes...");
submenu_free(app->main_menu);
variable_item_list_free(app->sub_menu);
text_box_free(app->about);
text_box_free(app->info_text);
FURI_LOG_D(TAG, "Removing the DCF77 scene...");
lwc_dcf77_scene_free(app->dcf77_view);
FURI_LOG_D(TAG, "Removing the MSF scene...");
lwc_msf_scene_free(app->msf_view);

FURI_LOG_D(TAG, "Desubscribing from notification...");
furi_record_close(RECORD_NOTIFICATION);

FURI_LOG_D(TAG, "Removing the protocol configs.");
for(uint8_t i = 0; i < __lwc_number_of_protocols; i++) {
free(app->state->proto_configs[i]);
}
furi_record_close(RECORD_STORAGE);

FURI_LOG_D(TAG, "Removing the AppState*.");
free(app->state);

FURI_LOG_D(TAG, "Freeing the App*...");
free(app);
}

ProtoConfig* lwc_get_protocol_config(AppState* app_state) {
return app_state->proto_configs[app_state->lwc_type];
}

LWCScene lwc_get_start_scene_for_protocol(AppState* app_state) {
return protocol_scene[app_state->lwc_type];
}
57 changes: 57 additions & 0 deletions longwave_clock/src/app_state.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#ifndef LWC_STATE_HEADERS
#define LWC_STATE_HEADERS

#include "flipper.h"
#include "logic_dcf77.h"
#include "logic_msf.h"
#include "protocols.h"
#include "scenes.h"
#include "gpio.h"

typedef struct ProtoConfig {
LWCRunMode run_mode;
LWCDataMode data_mode;
LWCDataPin data_pin;
} ProtoConfig;

typedef enum {
EventReceiveSync,
EventReceiveZero,
EventReceiveOne,
EventReceiveUnknown
} LWCEventType;

typedef struct AppState {
LWCType lwc_type;
ProtoConfig* proto_configs[__lwc_number_of_protocols];
GPIOContext* gpio;
FuriTimer* seconds_timer;
MinuteData* simulation_data;
Storage* storage;
bool display_on;
} AppState;

typedef struct App {
SceneManager* scene_manager;
ViewDispatcher* view_dispatcher;
Submenu* main_menu;
NotificationApp* notifications;
VariableItemList* sub_menu;
TextBox* about;
TextBox* info_text;
View* dcf77_view;
View* msf_view;
AppState* state;
} App;

App* app_alloc();
AppState* app_state_alloc();
void app_quit(App* app);
void app_free(App* app);
void app_init_lwc(App* app, LWCType rtype);
LWCScene lwc_get_start_scene_for_protocol(AppState* app_state);
void store_proto_config(AppState* app_state);

ProtoConfig* lwc_get_protocol_config(AppState* app_state);

#endif
Loading