This commit is contained in:
Clara K
2023-01-19 13:46:15 +01:00
committed by GitHub
29 changed files with 610 additions and 272 deletions

View File

@@ -3,6 +3,7 @@
#include <furi.h>
#include <furi_hal.h>
#include <storage/storage.h>
#include <input/input.h>
#include <gui/gui.h>
#include <stdlib.h>
@@ -10,24 +11,23 @@
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <math.h>
#ifndef PI
#define PI 3.14159265358979f
#endif
#include <notification/notification.h>
#include <notification/notification_messages.h>
#define TAG "Asteroids" // Used for logging
#define DEBUG_MSG 1
#define SCREEN_XRES 128
#define SCREEN_YRES 64
#define GAME_START_LIVES 3
/* The game uses the OK button both to fire and to accelerate the ship.
* This makes it a lot more playable since the finger does not have to
* move between two keys. However it is important that the extra time the
* player needs to press the button to accelerate instead of just firing
* is precisely selected to provide a smooth experience. After a few
* attempts, it looks like 70 milliseconds is the right spot. */
#define SHIP_ACCELERATION_KEYPRESS_TIME 70
#define TTLBUL 30 /* Bullet time to live, in ticks. */
#define MAXBUL 5 /* Max bullets on the screen. */
#define MAXAST 32 /* Max asteroids on the screen. */
#define SHIP_HIT_ANIMATION_LEN 15
#define SAVING_DIRECTORY "/ext/apps_data/asteroids"
#define SAVING_FILENAME SAVING_DIRECTORY "/game_asteroids.save"
#ifndef PI
#define PI 3.14159265358979f
#endif
/* ============================ Data structures ============================= */
@@ -36,7 +36,7 @@ typedef struct Ship {
y, /* Ship y position. */
vx, /* x velocity. */
vy, /* y velocity. */
rot; /* Current rotation. 2*PI full rotation. */
rot; /* Current rotation. 2*PI full ortation. */
} Ship;
typedef struct Bullet {
@@ -51,21 +51,20 @@ typedef struct Asteroid {
uint8_t shape_seed; /* Seed to give random shape. */
} Asteroid;
#define MAXBUL 10 /* Max bullets on the screen. */
#define MAXAST 32 /* Max asteroids on the screen. */
#define SHIP_HIT_ANIMATION_LEN 15
typedef struct AsteroidsApp {
/* GUI */
Gui* gui;
ViewPort* view_port; /* We just use a raw viewport and we render
everything into the low level canvas. */
FuriMessageQueue* event_queue; /* Key press events go here. */
FuriMessageQueue* event_queue; /* Keypress events go here. */
/* Game state. */
int running; /* Once false exists the app. */
bool gameover; /* Game over status. */
bool gameover; /* Gameover status. */
uint32_t ticks; /* Game ticks. Increments at each refresh. */
uint32_t score; /* Game score. */
uint32_t highscore; /* Highscore. Shown on Game Over Screen */
bool is_new_highscore; /* Is the last score a new highscore? */
uint32_t lives; /* Number of lives in the current game. */
uint32_t ship_hit; /* When non zero, the ship was hit by an asteroid
and we need to show an animation as long as
@@ -90,10 +89,55 @@ typedef struct AsteroidsApp {
bool fire; /* Short press detected: fire a bullet. */
} AsteroidsApp;
/* ============================== Prototypes ================================ */
const NotificationSequence sequence_thrusters = {
&message_vibro_on,
&message_delay_10,
&message_vibro_off,
NULL,
};
const NotificationSequence sequence_brake = {
&message_vibro_on,
&message_delay_10,
&message_delay_1,
&message_delay_1,
&message_vibro_off,
NULL,
};
const NotificationSequence sequence_crash = {
&message_red_255,
&message_vibro_on,
// &message_note_g5, // Play sound but currently disabled
&message_delay_25,
// &message_note_e5,
&message_vibro_off,
&message_sound_off,
NULL,
};
const NotificationSequence sequence_bullet_fired = {
&message_vibro_on,
// &message_note_g5, // Play sound but currently disabled. Need On/Off menu setting
&message_delay_10,
&message_delay_1,
&message_delay_1,
&message_delay_1,
&message_delay_1,
&message_delay_1,
// &message_note_e5,
&message_vibro_off,
&message_sound_off,
NULL,
};
/* ============================== Prototyeps ================================ */
// Only functions called before their definition are here.
bool load_game(AsteroidsApp* app);
void save_game(AsteroidsApp* app);
void restart_game_after_gameover(AsteroidsApp* app);
uint32_t key_pressed_time(AsteroidsApp* app, InputKey key);
@@ -114,7 +158,7 @@ Poly ShipPoly = {{-3, 0, 3}, {-3, 6, -3}, 3};
Poly ShipFirePoly = {{-1.5, 0, 1.5}, {-3, -6, -3}, 3};
/* Rotate the point of the polygon 'poly' and store the new rotated
/* Rotate the point of the poligon 'poly' and store the new rotated
* polygon in 'rot'. The polygon is rotated by an angle 'a', with
* center at 0,0. */
void rotate_poly(Poly* rot, Poly* poly, float a) {
@@ -165,7 +209,7 @@ void draw_bullet(Canvas* const canvas, Bullet* b) {
/* Draw an asteroid. The asteroid shapes is computed on the fly and
* is not stored in a permanent shape structure. In order to generate
* the shape, we use an initial fixed shape that we resize according
* to the asteroid size, perturbed according to the asteroid shape
* to the asteroid size, perturbate according to the asteroid shape
* seed, and finally draw it rotated of the right amount. */
void draw_asteroid(Canvas* const canvas, Asteroid* ast) {
Poly ap;
@@ -241,8 +285,11 @@ void render_callback(Canvas* const canvas, void* ctx) {
/* Draw ship, asteroids, bullets. */
draw_poly(canvas, &ShipPoly, app->ship.x, app->ship.y, app->ship.rot);
if(key_pressed_time(app, InputKeyOk) > SHIP_ACCELERATION_KEYPRESS_TIME)
if(key_pressed_time(app, InputKeyUp) > 0) {
notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_thrusters);
draw_poly(canvas, &ShipFirePoly, app->ship.x, app->ship.y, app->ship.rot);
}
for(int j = 0; j < app->bullets_num; j++) draw_bullet(canvas, &app->bullets[j]);
@@ -252,6 +299,30 @@ void render_callback(Canvas* const canvas, void* ctx) {
if(app->gameover) {
canvas_set_color(canvas, ColorBlack);
canvas_set_font(canvas, FontPrimary);
// TODO: if new highscore, display blinking "New High Score"
// Display High Score
if(app->is_new_highscore) {
canvas_draw_str(canvas, 22, 9, "New High Score!");
} else {
canvas_draw_str(canvas, 36, 9, "High Score");
}
// Convert highscore to string
int length = snprintf(NULL, 0, "%lu", app->highscore);
char* str_high_score = malloc(length + 1);
snprintf(str_high_score, length + 1, "%lu", app->highscore);
// Get length to center on screen
int nDigits = 0;
if(app->highscore > 0) {
nDigits = floor(log10(app->highscore)) + 1;
}
// Draw highscore centered
canvas_draw_str(canvas, (SCREEN_XRES / 2) - (nDigits * 2), 20, str_high_score);
free(str_high_score);
canvas_draw_str(canvas, 28, 35, "GAME OVER");
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 25, 50, "Press OK to restart");
@@ -295,6 +366,7 @@ bool objects_are_colliding(float x1, float y1, float r1, float x2, float y2, flo
/* Create a new bullet headed in the same direction of the ship. */
void ship_fire_bullet(AsteroidsApp* app) {
if(app->bullets_num == MAXBUL) return;
notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_bullet_fired);
Bullet* b = &app->bullets[app->bullets_num];
b->x = app->ship.x;
b->y = app->ship.y;
@@ -316,7 +388,7 @@ void ship_fire_bullet(AsteroidsApp* app) {
b->vx += app->ship.vx;
b->vy += app->ship.vy;
b->ttl = 50; /* The bullet will disappear after N ticks. */
b->ttl = TTLBUL; /* The bullet will disappear after N ticks. */
app->bullets_num++;
}
@@ -366,7 +438,7 @@ void remove_asteroid(AsteroidsApp* app, int id) {
/* Called when an asteroid was reached by a bullet. The asteroid
* hit is the one with the specified 'id'. */
void asteroid_was_hit(AsteroidsApp* app, int id) {
float sizelimit = 6; // Smaller than that, they disappear in one shot.
float sizelimit = 6; // Smaller than that polverize in one shot.
Asteroid* a = &app->asteroids[id];
/* Asteroid is large enough to break into fragments. */
@@ -387,17 +459,19 @@ void asteroid_was_hit(AsteroidsApp* app, int id) {
}
} else {
app->score++;
if(app->score > app->highscore) {
app->is_new_highscore = true;
app->highscore = app->score; // Show on Game Over Screen and future main menu
}
}
}
/* Set game over state. When in game-over mode, the game displays a
* game over text with a background of many asteroids floating around. */
/* Set gameover state. When in game-over mode, the game displays a gameover
* text with a background of many asteroids floating around. */
void game_over(AsteroidsApp* app) {
restart_game_after_gameover(app);
if(app->is_new_highscore) save_game(app); // Save highscore but only on change
app->gameover = true;
int asteroids = 8;
while(asteroids-- && add_asteroid(app) != NULL)
;
app->lives = GAME_START_LIVES; // Show 3 lives in game over screen to match new game start
}
/* Function called when a collision between the asteroid and the
@@ -422,16 +496,17 @@ void restart_game(AsteroidsApp* app) {
app->bullets_num = 0;
app->last_bullet_tick = 0;
app->asteroids_num = 0;
app->ship_hit = 0;
}
/* Called after game over to restart the game. This function
/* Called after gameover to restart the game. This function
* also calls restart_game(). */
void restart_game_after_gameover(AsteroidsApp* app) {
app->gameover = false;
app->ticks = 0;
app->score = 0;
app->ship_hit = 0;
app->lives = GAME_START_LIVES - 1; /* -1 to account for current one. */
app->is_new_highscore = false;
app->lives = GAME_START_LIVES - 1;
restart_game(app);
}
@@ -505,6 +580,7 @@ void game_tick(void* ctx) {
* 1. Ship was hit, we frozen the game as long as ship_hit isn't zero
* again, and show an animation of a rotating ship. */
if(app->ship_hit) {
notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_crash);
app->ship.rot += 0.5;
app->ship_hit--;
view_port_update(app->view_port);
@@ -515,7 +591,8 @@ void game_tick(void* ctx) {
} else if(app->gameover) {
/* 2. Game over. We need to update only background asteroids. In this
* state the game just displays a GAME OVER text with the floating
* asteroids in background. */
* asteroids in backgroud. */
if(key_pressed_time(app, InputKeyOk) > 100) {
restart_game_after_gameover(app);
}
@@ -524,13 +601,14 @@ void game_tick(void* ctx) {
return;
}
/* Handle key presses. */
/* Handle keypresses. */
if(app->pressed[InputKeyLeft]) app->ship.rot -= .35;
if(app->pressed[InputKeyRight]) app->ship.rot += .35;
if(key_pressed_time(app, InputKeyOk) > SHIP_ACCELERATION_KEYPRESS_TIME) {
if(app->pressed[InputKeyUp]) {
app->ship.vx -= 0.5 * (float)sin(app->ship.rot);
app->ship.vy += 0.5 * (float)cos(app->ship.rot);
} else if(app->pressed[InputKeyDown]) {
notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_brake);
app->ship.vx *= 0.75;
app->ship.vy *= 0.75;
}
@@ -567,6 +645,41 @@ void game_tick(void* ctx) {
/* ======================== Flipper specific code =========================== */
bool load_game(AsteroidsApp* app) {
Storage* storage = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(storage);
uint16_t bytes_readed = 0;
if(storage_file_open(file, SAVING_FILENAME, FSAM_READ, FSOM_OPEN_EXISTING)) {
bytes_readed = storage_file_read(file, app, sizeof(AsteroidsApp));
}
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
return bytes_readed == sizeof(AsteroidsApp);
}
void save_game(AsteroidsApp* app) {
Storage* storage = furi_record_open(RECORD_STORAGE);
if(storage_common_stat(storage, SAVING_DIRECTORY, NULL) == FSE_NOT_EXIST) {
if(!storage_simply_mkdir(storage, SAVING_DIRECTORY)) {
return;
}
}
File* file = storage_file_alloc(storage);
if(storage_file_open(file, SAVING_FILENAME, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
storage_file_write(file, app, sizeof(AsteroidsApp));
}
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
}
/* Here all we do is putting the events into the queue that will be handled
* in the while() loop of the app entry point function. */
void input_callback(InputEvent* input_event, void* ctx) {
@@ -579,6 +692,8 @@ void input_callback(InputEvent* input_event, void* ctx) {
AsteroidsApp* asteroids_app_alloc() {
AsteroidsApp* app = malloc(sizeof(AsteroidsApp));
load_game(app);
app->gui = furi_record_open(RECORD_GUI);
app->view_port = view_port_alloc();
view_port_draw_callback_set(app->view_port, render_callback, app);
@@ -587,6 +702,7 @@ AsteroidsApp* asteroids_app_alloc() {
app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
app->running = 1; /* Turns 0 when back is pressed. */
restart_game_after_gameover(app);
memset(app->pressed, 0, sizeof(app->pressed));
return app;
@@ -617,12 +733,15 @@ uint32_t key_pressed_time(AsteroidsApp* app, InputKey key) {
/* Handle keys interaction. */
void asteroids_update_keypress_state(AsteroidsApp* app, InputEvent input) {
// Allow Rapid fire
if(input.key == InputKeyOk) {
app->fire = true;
}
if(input.type == InputTypePress) {
app->pressed[input.key] = furi_get_tick();
} else if(input.type == InputTypeRelease) {
uint32_t dur = key_pressed_time(app, input.key);
app->pressed[input.key] = 0;
if(dur < 200 && input.key == InputKeyOk) app->fire = true;
}
}
@@ -646,7 +765,9 @@ int32_t asteroids_app_entry(void* p) {
/* Handle navigation here. Then handle view-specific inputs
* in the view specific handling function. */
if(input.type == InputTypeShort && input.key == InputKeyBack) {
if(input.type == InputTypeLong && input.key == InputKeyBack) {
// Save High Score even if player didn't finish game
if(app->is_new_highscore) save_game(app); // Save highscore but only on change
app->running = 0;
} else {
asteroids_update_keypress_state(app, input);

View File

@@ -39,11 +39,12 @@ static void clock_render_callback(Canvas* const canvas, void* ctx) {
} else {
bool pm = curr_dt.hour > 12;
bool pm12 = curr_dt.hour >= 12;
bool am12 = curr_dt.hour == 0;
snprintf(
time_string,
TIME_LEN,
CLOCK_TIME_FORMAT,
pm ? curr_dt.hour - 12 : curr_dt.hour,
pm ? curr_dt.hour - 12 : (am12 ? 12 : curr_dt.hour),
curr_dt.minute,
curr_dt.second);
@@ -237,4 +238,4 @@ int32_t clock_app(void* p) {
free(plugin_state);
return 0;
}
}

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Alexander Kopachov
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.

View File

@@ -14,6 +14,8 @@
#define TOTP_CLI_COMMAND_ADD_ARG_DIGITS "digits"
#define TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX "-d"
#define TOTP_CLI_COMMAND_ADD_ARG_UNSECURE_PREFIX "-u"
#define TOTP_CLI_COMMAND_ADD_ARG_DURATION "duration"
#define TOTP_CLI_COMMAND_ADD_ARG_DURATION_PREFIX "-l"
static bool token_info_set_algo_from_str(TokenInfo* token_info, const FuriString* str) {
if(furi_string_cmpi_str(str, TOTP_CONFIG_TOKEN_ALGO_SHA1_NAME) == 0) {
@@ -34,6 +36,16 @@ static bool token_info_set_algo_from_str(TokenInfo* token_info, const FuriString
return false;
}
static bool args_read_uint8_and_trim(FuriString* args, uint8_t* value) {
int int_value;
if(!args_read_int_and_trim(args, &int_value) || int_value < 0 || int_value > UINT8_MAX) {
return false;
}
*value = (uint8_t)int_value;
return true;
}
void totp_cli_command_add_docopt_commands() {
TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_ADD ", " TOTP_CLI_COMMAND_ADD_ALT
", " TOTP_CLI_COMMAND_ADD_ALT2 " Add new token\r\n");
@@ -42,11 +54,11 @@ void totp_cli_command_add_docopt_commands() {
void totp_cli_command_add_docopt_usage() {
TOTP_CLI_PRINTF(
" " TOTP_CLI_COMMAND_NAME
" " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_ADD " | " TOTP_CLI_COMMAND_ADD_ALT " | " TOTP_CLI_COMMAND_ADD_ALT2) " " DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ADD_ARG_NAME) " " DOCOPT_OPTIONAL(
" " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_ADD " | " TOTP_CLI_COMMAND_ADD_ALT " | " TOTP_CLI_COMMAND_ADD_ALT2) " " DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ADD_ARG_NAME) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ADD_ARG_ALGO))) " " DOCOPT_OPTIONAL(
DOCOPT_OPTION(
TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX,
TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX,
DOCOPT_ARGUMENT(
TOTP_CLI_COMMAND_ADD_ARG_ALGO))) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ADD_ARG_DIGITS))) " " DOCOPT_OPTIONAL(DOCOPT_SWITCH(TOTP_CLI_COMMAND_ADD_ARG_UNSECURE_PREFIX)) "\r\n");
TOTP_CLI_COMMAND_ADD_ARG_DIGITS))) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ADD_ARG_DURATION_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ADD_ARG_DURATION))) " " DOCOPT_OPTIONAL(DOCOPT_SWITCH(TOTP_CLI_COMMAND_ADD_ARG_UNSECURE_PREFIX)) "\r\n");
}
void totp_cli_command_add_docopt_arguments() {
@@ -64,6 +76,10 @@ void totp_cli_command_add_docopt_options() {
TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX,
DOCOPT_ARGUMENT(
TOTP_CLI_COMMAND_ADD_ARG_DIGITS)) " Number of digits to generate, one of: 6, 8 " DOCOPT_DEFAULT("6") "\r\n");
TOTP_CLI_PRINTF(" " DOCOPT_OPTION(
TOTP_CLI_COMMAND_ADD_ARG_DURATION_PREFIX,
DOCOPT_ARGUMENT(
TOTP_CLI_COMMAND_ADD_ARG_DURATION)) " Token lifetime duration in seconds, between: 15 and 255 " DOCOPT_DEFAULT("30") "\r\n");
TOTP_CLI_PRINTF(" " DOCOPT_SWITCH(
TOTP_CLI_COMMAND_ADD_ARG_UNSECURE_PREFIX) " Show console user input as-is without masking\r\n");
}
@@ -110,16 +126,32 @@ void totp_cli_command_add_handle(PluginState* plugin_state, FuriString* args, Cl
parsed = true;
}
} else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX) == 0) {
if(!args_read_string_and_trim(args, temp_str)) {
uint8_t digit_value;
if(!args_read_uint8_and_trim(args, &digit_value)) {
TOTP_CLI_PRINTF(
"Missed value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX
"Missed or incorrect value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX
"\"\r\n");
} else if(!token_info_set_digits_from_int(
token_info, CONVERT_CHAR_TO_DIGIT(furi_string_get_char(temp_str, 0)))) {
} else if(!token_info_set_digits_from_int(token_info, digit_value)) {
TOTP_CLI_PRINTF(
"\"%s\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX
"\"%" PRIu8
"\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX
"\"\r\n",
furi_string_get_cstr(temp_str));
digit_value);
} else {
parsed = true;
}
} else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_ADD_ARG_DURATION_PREFIX) == 0) {
uint8_t duration_value;
if(!args_read_uint8_and_trim(args, &duration_value)) {
TOTP_CLI_PRINTF(
"Missed or incorrect value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_DURATION_PREFIX
"\"\r\n");
} else if(!token_info_set_duration_from_int(token_info, duration_value)) {
TOTP_CLI_PRINTF(
"\"%" PRIu8
"\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_DURATION_PREFIX
"\"\r\n",
duration_value);
} else {
parsed = true;
}

View File

@@ -40,19 +40,21 @@ void totp_cli_command_list_handle(PluginState* plugin_state, Cli* cli) {
return;
}
TOTP_CLI_PRINTF("+-----+-----------------------------+--------+--------+\r\n");
TOTP_CLI_PRINTF("| %-*s | %-*s | %-*s | %-s |\r\n", 3, "#", 27, "Name", 6, "Algo", "Digits");
TOTP_CLI_PRINTF("+-----+-----------------------------+--------+--------+\r\n");
TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+-----+\r\n");
TOTP_CLI_PRINTF(
"| %-*s | %-*s | %-*s | %-s | %-s |\r\n", 3, "#", 25, "Name", 6, "Algo", "Ln", "Dur");
TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+-----+\r\n");
uint16_t index = 1;
TOTP_LIST_FOREACH(plugin_state->tokens_list, node, {
TokenInfo* token_info = (TokenInfo*)node->data;
TOTP_CLI_PRINTF(
"| %-3" PRIu16 " | %-27.27s | %-6s | %-6" PRIu8 " |\r\n",
"| %-3" PRIu16 " | %-25.25s | %-6s | %-2" PRIu8 " | %-3" PRIu8 " |\r\n",
index,
token_info->name,
get_algo_as_cstr(token_info->algo),
token_info->digits);
token_info->digits,
token_info->duration);
index++;
});
TOTP_CLI_PRINTF("+-----+-----------------------------+--------+--------+\r\n");
TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+-----+\r\n");
}

View File

@@ -5,6 +5,7 @@
#include "../../types/common.h"
#include "../../types/token_info.h"
#include "migrations/config_migration_v1_to_v2.h"
#include "migrations/config_migration_v2_to_v3.h"
#define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/authenticator")
#define CONFIG_FILE_PATH CONFIG_FILE_DIRECTORY_PATH "/totp.conf"
@@ -173,6 +174,13 @@ static TotpConfigFileOpenResult totp_open_config_file(Storage* storage, FlipperF
flipper_format_write_comment(fff_data_file, temp_str);
flipper_format_write_comment_cstr(fff_data_file, " ");
flipper_format_write_comment_cstr(
fff_data_file,
"# Token lifetime duration in seconds. Should be between 15 and 255. Majority websites requires 30, however some rare websites may require custom lifetime. If you are not sure which one to use - use 30");
furi_string_printf(temp_str, "%s: 30", TOTP_CONFIG_KEY_TOKEN_DURATION);
flipper_format_write_comment(fff_data_file, temp_str);
flipper_format_write_comment_cstr(fff_data_file, " ");
flipper_format_write_comment_cstr(fff_data_file, "=== TOKEN SAMPLE END ===");
flipper_format_write_comment_cstr(fff_data_file, " ");
@@ -232,6 +240,12 @@ TotpConfigFileUpdateResult
break;
}
tmp_uint32 = token_info->duration;
if(!flipper_format_write_uint32(file, TOTP_CONFIG_KEY_TOKEN_DURATION, &tmp_uint32, 1)) {
update_result = TotpConfigFileUpdateError;
break;
}
update_result = TotpConfigFileUpdateSuccess;
} while(false);
@@ -483,6 +497,7 @@ TotpConfigFileOpenResult totp_config_file_load_base(PluginState* const plugin_st
if(file_version == 1) {
if(totp_config_migrate_v1_to_v2(fff_data_file, fff_backup_data_file)) {
FURI_LOG_I(LOGGING_TAG, "Applied migration from v1 to v2");
file_version = 2;
} else {
FURI_LOG_W(
LOGGING_TAG, "An error occurred during migration from v1 to v2");
@@ -491,6 +506,18 @@ TotpConfigFileOpenResult totp_config_file_load_base(PluginState* const plugin_st
}
}
if(file_version == 2) {
if(totp_config_migrate_v2_to_v3(fff_data_file, fff_backup_data_file)) {
FURI_LOG_I(LOGGING_TAG, "Applied migration from v2 to v3");
file_version = 3;
} else {
FURI_LOG_W(
LOGGING_TAG, "An error occurred during migration from v2 to v3");
result = TotpConfigFileOpenError;
break;
}
}
flipper_format_file_close(fff_backup_data_file);
flipper_format_free(fff_backup_data_file);
flipper_format_rewind(fff_data_file);
@@ -669,6 +696,12 @@ TokenLoadingResult totp_config_file_load_tokens(PluginState* const plugin_state)
tokenInfo->digits = TOTP_6_DIGITS;
}
if(!flipper_format_read_uint32(
fff_data_file, TOTP_CONFIG_KEY_TOKEN_DURATION, &temp_data32, 1) ||
!token_info_set_duration_from_int(tokenInfo, temp_data32)) {
tokenInfo->duration = TOTP_TOKEN_DURATION_DEFAULT;
}
FURI_LOG_D(LOGGING_TAG, "Found token \"%s\"", tokenInfo->name);
TOTP_LIST_INIT_OR_ADD(plugin_state->tokens_list, tokenInfo, furi_check);
@@ -736,4 +769,4 @@ void totp_config_file_reset() {
Storage* storage = totp_open_storage();
storage_simply_remove(storage, CONFIG_FILE_PATH);
totp_close_storage();
}
}

View File

@@ -1,13 +1,14 @@
#pragma once
#define CONFIG_FILE_HEADER "Flipper TOTP plugin config file"
#define CONFIG_FILE_ACTUAL_VERSION 2
#define CONFIG_FILE_ACTUAL_VERSION 3
#define TOTP_CONFIG_KEY_TIMEZONE "Timezone"
#define TOTP_CONFIG_KEY_TOKEN_NAME "TokenName"
#define TOTP_CONFIG_KEY_TOKEN_SECRET "TokenSecret"
#define TOTP_CONFIG_KEY_TOKEN_ALGO "TokenAlgo"
#define TOTP_CONFIG_KEY_TOKEN_DIGITS "TokenDigits"
#define TOTP_CONFIG_KEY_TOKEN_DURATION "TokenDuration"
#define TOTP_CONFIG_KEY_CRYPTO_VERIFY "Crypto"
#define TOTP_CONFIG_KEY_BASE_IV "BaseIV"
#define TOTP_CONFIG_KEY_PINSET "PinIsSet"

View File

@@ -1,6 +1,7 @@
#include "config_migration_v1_to_v2.h"
#include <flipper_format/flipper_format.h>
#include "../constants.h"
#include "../../../types/token_info.h"
#define NEW_VERSION 2
@@ -36,7 +37,7 @@ bool totp_config_migrate_v1_to_v2(
flipper_format_write_string_cstr(
fff_data_file, TOTP_CONFIG_KEY_TOKEN_ALGO, TOTP_CONFIG_TOKEN_ALGO_SHA1_NAME);
uint32_t default_digits = 6;
const uint32_t default_digits = TOTP_6_DIGITS;
flipper_format_write_uint32(
fff_data_file, TOTP_CONFIG_KEY_TOKEN_DIGITS, &default_digits, 1);
}

View File

@@ -0,0 +1,70 @@
#include "config_migration_v2_to_v3.h"
#include <flipper_format/flipper_format.h>
#include "../constants.h"
#include "../../../types/token_info.h"
#define NEW_VERSION 3
bool totp_config_migrate_v2_to_v3(
FlipperFormat* fff_data_file,
FlipperFormat* fff_backup_data_file) {
flipper_format_write_header_cstr(fff_data_file, CONFIG_FILE_HEADER, NEW_VERSION);
FuriString* temp_str = furi_string_alloc();
if(flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_BASE_IV, temp_str)) {
flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_BASE_IV, temp_str);
}
flipper_format_rewind(fff_backup_data_file);
if(flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_CRYPTO_VERIFY, temp_str)) {
flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_CRYPTO_VERIFY, temp_str);
}
flipper_format_rewind(fff_backup_data_file);
if(flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_TIMEZONE, temp_str)) {
flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_TIMEZONE, temp_str);
}
flipper_format_rewind(fff_backup_data_file);
if(flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_PINSET, temp_str)) {
flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_PINSET, temp_str);
}
flipper_format_rewind(fff_backup_data_file);
if(flipper_format_read_string(
fff_backup_data_file, TOTP_CONFIG_KEY_NOTIFICATION_METHOD, temp_str)) {
flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_NOTIFICATION_METHOD, temp_str);
}
flipper_format_rewind(fff_backup_data_file);
while(true) {
if(!flipper_format_read_string(
fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_NAME, temp_str)) {
break;
}
flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_TOKEN_NAME, temp_str);
flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_SECRET, temp_str);
flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_TOKEN_SECRET, temp_str);
flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_ALGO, temp_str);
flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_TOKEN_ALGO, temp_str);
flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_DIGITS, temp_str);
flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_TOKEN_DIGITS, temp_str);
const uint32_t default_duration = TOTP_TOKEN_DURATION_DEFAULT;
flipper_format_write_uint32(
fff_data_file, TOTP_CONFIG_KEY_TOKEN_DURATION, &default_duration, 1);
}
furi_string_free(temp_str);
return true;
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include <flipper_format/flipper_format.h>
bool totp_config_migrate_v2_to_v3(
FlipperFormat* fff_data_file,
FlipperFormat* fff_backup_data_file);

View File

@@ -61,3 +61,12 @@ bool token_info_set_digits_from_int(TokenInfo* token_info, uint8_t digits) {
return false;
}
bool token_info_set_duration_from_int(TokenInfo* token_info, uint8_t duration) {
if(duration >= 15) {
token_info->duration = duration;
return true;
}
return false;
}

View File

@@ -2,6 +2,8 @@
#include <inttypes.h>
#define TOTP_TOKEN_DURATION_DEFAULT 30
typedef uint8_t TokenHashAlgo;
typedef uint8_t TokenDigitsCount;
@@ -70,6 +72,11 @@ typedef struct {
* @brief Desired TOTP token length
*/
TokenDigitsCount digits;
/**
* @brief Desired TOTP token duration in seconds
*/
uint8_t duration;
} TokenInfo;
/**
@@ -102,6 +109,14 @@ bool token_info_set_secret(
* @brief Sets token digits count from \c uint8_t value
* @param token_info instance whichs token digits count length should be updated
* @param digits desired token digits count length
* @return \c true if token digits count length has been updated; \c false p
* @return \c true if token digits count length has been updated; \c false otherwise
*/
bool token_info_set_digits_from_int(TokenInfo* token_info, uint8_t digits);
/**
* @brief Sets token duration from \c uint8_t value
* @param token_info instance whichs token digits count length should be updated
* @param duration desired token duration in seconds
* @return \c true if token duration has been updated; \c false otherwise
*/
bool token_info_set_duration_from_int(TokenInfo* token_info, uint8_t duration);

View File

@@ -21,6 +21,7 @@ typedef enum {
TokenSecretTextBox,
TokenAlgoSelect,
TokenLengthSelect,
TokenDurationSelect,
ConfirmButton,
} Control;
@@ -39,6 +40,8 @@ typedef struct {
int16_t screen_y_offset;
TokenHashAlgo algo;
uint8_t digits_count_index;
uint8_t duration;
FuriString* duration_text;
} SceneState;
void totp_scene_add_new_token_init(const PluginState* plugin_state) {
@@ -63,6 +66,10 @@ static void on_token_secret_user_comitted(InputTextSceneCallbackResult* result)
free(result);
}
static void update_duration_text(SceneState* scene_state) {
furi_string_printf(scene_state->duration_text, "%d sec.", scene_state->duration);
}
void totp_scene_add_new_token_activate(
PluginState* plugin_state,
const TokenAddEditSceneContext* context) {
@@ -89,6 +96,9 @@ void totp_scene_add_new_token_activate(
scene_state->screen_y_offset = 0;
scene_state->input_state = NULL;
scene_state->duration = TOTP_TOKEN_DURATION_DEFAULT;
scene_state->duration_text = furi_string_alloc();
update_duration_text(scene_state);
if(context == NULL) {
TOTP_NULLABLE_NULL(scene_state->current_token_index);
@@ -124,14 +134,23 @@ void totp_scene_add_new_token_render(Canvas* const canvas, PluginState* plugin_s
ui_control_select_render(
canvas,
0,
63 - scene_state->screen_y_offset,
61 - scene_state->screen_y_offset,
SCREEN_WIDTH,
TOKEN_DIGITS_TEXT_LIST[scene_state->digits_count_index],
scene_state->selected_control == TokenLengthSelect);
ui_control_select_render(
canvas,
0,
78 - scene_state->screen_y_offset,
SCREEN_WIDTH,
furi_string_get_cstr(scene_state->duration_text),
scene_state->selected_control == TokenDurationSelect);
ui_control_button_render(
canvas,
SCREEN_WIDTH_CENTER - 24,
85 - scene_state->screen_y_offset,
101 - scene_state->screen_y_offset,
48,
13,
"Confirm",
@@ -146,8 +165,12 @@ void totp_scene_add_new_token_render(Canvas* const canvas, PluginState* plugin_s
}
void update_screen_y_offset(SceneState* scene_state) {
if(scene_state->selected_control > TokenAlgoSelect) {
scene_state->screen_y_offset = 35;
if(scene_state->selected_control > TokenLengthSelect) {
scene_state->screen_y_offset = 51;
} else if(scene_state->selected_control > TokenAlgoSelect) {
scene_state->screen_y_offset = 34;
} else if(scene_state->selected_control > TokenSecretTextBox) {
scene_state->screen_y_offset = 17;
} else {
scene_state->screen_y_offset = 0;
}
@@ -197,6 +220,9 @@ bool totp_scene_add_new_token_handle_event(PluginEvent* const event, PluginState
} else if(scene_state->selected_control == TokenLengthSelect) {
totp_roll_value_uint8_t(
&scene_state->digits_count_index, 1, 0, 1, RollOverflowBehaviorRoll);
} else if(scene_state->selected_control == TokenDurationSelect) {
totp_roll_value_uint8_t(&scene_state->duration, 15, 15, 255, RollOverflowBehaviorStop);
update_duration_text(scene_state);
}
break;
case InputKeyLeft:
@@ -206,6 +232,10 @@ bool totp_scene_add_new_token_handle_event(PluginEvent* const event, PluginState
} else if(scene_state->selected_control == TokenLengthSelect) {
totp_roll_value_uint8_t(
&scene_state->digits_count_index, -1, 0, 1, RollOverflowBehaviorRoll);
} else if(scene_state->selected_control == TokenDurationSelect) {
totp_roll_value_uint8_t(
&scene_state->duration, -15, 15, 255, RollOverflowBehaviorStop);
update_duration_text(scene_state);
}
break;
case InputKeyOk:
@@ -230,6 +260,8 @@ bool totp_scene_add_new_token_handle_event(PluginEvent* const event, PluginState
break;
case TokenLengthSelect:
break;
case TokenDurationSelect:
break;
case ConfirmButton: {
TokenInfo* tokenInfo = token_info_alloc();
bool token_secret_set = token_info_set_secret(
@@ -245,6 +277,7 @@ bool totp_scene_add_new_token_handle_event(PluginEvent* const event, PluginState
tokenInfo->name, scene_state->token_name, scene_state->token_name_length + 1);
tokenInfo->algo = scene_state->algo;
tokenInfo->digits = TOKEN_DIGITS_VALUE_LIST[scene_state->digits_count_index];
tokenInfo->duration = scene_state->duration;
TOTP_LIST_INIT_OR_ADD(plugin_state->tokens_list, tokenInfo, furi_check);
plugin_state->tokens_count++;
@@ -310,6 +343,8 @@ void totp_scene_add_new_token_deactivate(PluginState* plugin_state) {
free(scene_state->token_secret_input_context->header_text);
free(scene_state->token_secret_input_context);
furi_string_free(scene_state->duration_text);
if(scene_state->input_state != NULL) {
totp_input_text_free(scene_state->input_state);
}

View File

@@ -16,13 +16,11 @@
#include "../token_menu/totp_scene_token_menu.h"
#include "../../../workers/type_code/type_code.h"
#define TOKEN_LIFETIME 30
typedef struct {
uint16_t current_token_index;
char last_code[TOTP_TOKEN_DIGITS_MAX_COUNT + 1];
char* last_code_name;
bool need_token_update;
TokenInfo* current_token;
uint32_t last_token_gen_time;
TotpTypeCodeWorkerContext* type_code_worker_context;
NotificationMessage const** notification_sequence_new_token;
@@ -151,7 +149,7 @@ static void update_totp_params(PluginState* const plugin_state) {
list_element_at(plugin_state->tokens_list, scene_state->current_token_index)->data;
scene_state->need_token_update = true;
scene_state->last_code_name = tokenInfo->name;
scene_state->current_token = tokenInfo;
}
}
@@ -229,7 +227,7 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_
furi_hal_rtc_get_datetime(&curr_dt);
uint32_t curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt);
bool is_new_token_time = curr_ts % TOKEN_LIFETIME == 0;
bool is_new_token_time = curr_ts % scene_state->current_token->duration == 0;
if(is_new_token_time && scene_state->last_token_gen_time != curr_ts) {
scene_state->need_token_update = true;
}
@@ -238,10 +236,7 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_
scene_state->need_token_update = false;
scene_state->last_token_gen_time = curr_ts;
const TokenInfo* tokenInfo =
(TokenInfo*)(list_element_at(
plugin_state->tokens_list, scene_state->current_token_index)
->data);
const TokenInfo* tokenInfo = scene_state->current_token;
if(tokenInfo->token != NULL && tokenInfo->token_length > 0) {
furi_mutex_acquire(
@@ -258,7 +253,7 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_
key_length,
curr_ts,
plugin_state->timezone_offset,
TOKEN_LIFETIME),
tokenInfo->duration),
scene_state->last_code,
tokenInfo->digits);
memset_s(key, key_length, 0, key_length);
@@ -279,7 +274,7 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_
}
canvas_set_font(canvas, FontPrimary);
uint16_t token_name_width = canvas_string_width(canvas, scene_state->last_code_name);
uint16_t token_name_width = canvas_string_width(canvas, scene_state->current_token->name);
if(SCREEN_WIDTH - token_name_width > 18) {
canvas_draw_str_aligned(
canvas,
@@ -287,7 +282,7 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_
SCREEN_HEIGHT_CENTER - 20,
AlignCenter,
AlignCenter,
scene_state->last_code_name);
scene_state->current_token->name);
} else {
canvas_draw_str_aligned(
canvas,
@@ -295,7 +290,7 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_
SCREEN_HEIGHT_CENTER - 20,
AlignLeft,
AlignCenter,
scene_state->last_code_name);
scene_state->current_token->name);
canvas_set_color(canvas, ColorWhite);
canvas_draw_box(canvas, 0, SCREEN_HEIGHT_CENTER - 24, 9, 9);
canvas_draw_box(canvas, SCREEN_WIDTH - 10, SCREEN_HEIGHT_CENTER - 24, 9, 9);
@@ -313,6 +308,7 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_
const uint8_t BAR_MARGIN = 3;
const uint8_t BAR_HEIGHT = 4;
const uint8_t TOKEN_LIFETIME = scene_state->current_token->duration;
float percentDone = (float)(TOKEN_LIFETIME - curr_ts % TOKEN_LIFETIME) / (float)TOKEN_LIFETIME;
uint8_t barWidth = (uint8_t)((float)(SCREEN_WIDTH - (BAR_MARGIN << 1)) * percentDone);
uint8_t barX = ((SCREEN_WIDTH - (BAR_MARGIN << 1) - barWidth) >> 1) + BAR_MARGIN;