Save game results w/contact info to SD card.

This commit is contained in:
Derek Jamison 2023-03-06 14:48:32 -05:00
parent 3e5acab130
commit 615e307bf5
3 changed files with 145 additions and 14 deletions

View File

@ -18,10 +18,10 @@ Completed work:
- Config - Allow changing game number. - Config - Allow changing game number.
- Config - Allow changing frequency. - Config - Allow changing frequency.
- Receiving a Join game does an ACK (to cause game on joiner to start). - Receiving a Join game does an ACK (to cause game on joiner to start).
- Log game results & contact info onto SD card.
Remaining work (for subghz version): Remaining work (for subghz version):
- Log game results & contact info onto SD card.
- Config - Allow changing hard-coded CONTACT_INFO message. - Config - Allow changing hard-coded CONTACT_INFO message.
- Allow viewing past games/scores. - Allow viewing past games/scores.
- A join ACK removes it from the list of available games. - A join ACK removes it from the list of available games.

View File

@ -156,7 +156,7 @@ static void rps_receive_data(GameContext* game_context, uint32_t tick) {
message[MESSAGE_MAX_LEN - 1] = 0; message[MESSAGE_MAX_LEN - 1] = 0;
unsigned int game_number; unsigned int game_number;
char randomInfo[MESSAGE_MAX_LEN]; char sender_contact[MESSAGE_MAX_LEN];
char sender_name[9]; char sender_name[9];
char tmp; char tmp;
Move move = MoveUnknown; Move move = MoveUnknown;
@ -213,17 +213,20 @@ static void rps_receive_data(GameContext* game_context, uint32_t tick) {
(const char*)message + game_name_len + 2, (const char*)message + game_name_len + 2,
"%03u%s :%8s", "%03u%s :%8s",
&game_number, &game_number,
randomInfo, sender_contact,
sender_name) == 3) { sender_name) == 3) {
FURI_LOG_T(TAG, "Join had randomInfo of >%s<", randomInfo); FURI_LOG_T(TAG, "Join had contact of >%s<", sender_contact);
// IMPORTANT: The code processing the event needs to furi_string_free the senderName! // IMPORTANT: The code processing the event needs to furi_string_free the senderName!
FuriString* name = furi_string_alloc(); FuriString* name = furi_string_alloc();
furi_string_set(name, sender_name); furi_string_set(name, sender_name);
FuriString* contact = furi_string_alloc();
furi_string_set(contact, sender_contact);
GameEvent event = { GameEvent event = {
.type = GameEventRemoteJoined, .type = GameEventRemoteJoined,
.sender_name = name, .sender_name = name,
.sender_contact = contact,
.game_number = game_number}; .game_number = game_number};
furi_message_queue_put(game_context->queue, &event, FuriWaitForever); furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
} else { } else {
@ -234,12 +237,23 @@ static void rps_receive_data(GameContext* game_context, uint32_t tick) {
case GameRfPurposeJoinAcknowledge: case GameRfPurposeJoinAcknowledge:
if(sscanf( if(sscanf(
(const char*)message + game_name_len + 2, (const char*)message + game_name_len + 2,
"%03u :%8s", "%03u%s :%8s",
&game_number, &game_number,
sender_name) == 2) { sender_contact,
sender_name) == 3) {
FURI_LOG_T(TAG, "Join acknowledge for game %d.", game_number); FURI_LOG_T(TAG, "Join acknowledge for game %d.", game_number);
FURI_LOG_T(TAG, "Join ack had contact of >%s<", sender_contact);
FuriString* name = furi_string_alloc();
furi_string_set(name, sender_name);
FuriString* contact = furi_string_alloc();
furi_string_set(contact, sender_contact);
GameEvent event = { GameEvent event = {
.type = GameEventRemoteJoinAcknowledged, .game_number = game_number}; .type = GameEventRemoteJoinAcknowledged,
.sender_name = name,
.sender_contact = contact,
.game_number = game_number};
furi_message_queue_put(game_context->queue, &event, FuriWaitForever); furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
} else { } else {
FURI_LOG_W(TAG, "Failed to parse join acknowledge message. >%s<", message); FURI_LOG_W(TAG, "Failed to parse join acknowledge message. >%s<", message);
@ -742,21 +756,22 @@ static void rps_broadcast_join(GameContext* game_context) {
} }
// Send message that acknowledges Flipper joining a specific game. // Send message that acknowledges Flipper joining a specific game.
// We broadcast - "RPS:" + joinAck"A" + version"A" + game"###" + " :" + "YourFlip" + "\r\n" // We broadcast - "RPS:" + joinAck"A" + version"A" + game"###" + "NYourNameHere" +" :" + "YourFlip" + "\r\n"
// @param game_context pointer to a GameContext. // @param game_context pointer to a GameContext.
static void rps_broadcast_join_acknowledge(GameContext* game_context) { static void rps_broadcast_join_acknowledge(GameContext* game_context) {
GameData* data = game_context->data; GameData* data = game_context->data;
unsigned int gameNumber = data->game_number; unsigned int gameNumber = data->game_number;
FURI_LOG_I(TAG, "Acknowledge joining game %d.", gameNumber); FURI_LOG_I(TAG, "Acknowledge joining game %d.", gameNumber);
// The message for game 42 should look like... "RPS:AA042 :YourFlip\r\n" // The message for game 42 should look like... "RPS:AA042NYourNameHere :YourFlip\r\n"
furi_string_printf( furi_string_printf(
data->buffer, data->buffer,
"%s%c%c%03u :%s\r\n", "%s%c%c%03u%s :%s\r\n",
RPS_GAME_NAME, RPS_GAME_NAME,
GameRfPurposeJoinAcknowledge, GameRfPurposeJoinAcknowledge,
MAJOR_VERSION, MAJOR_VERSION,
data->game_number, data->game_number,
CONTACT_INFO,
furi_hal_version_get_name_ptr()); furi_hal_version_get_name_ptr());
rps_broadcast(game_context, data->buffer); rps_broadcast(game_context, data->buffer);
} }
@ -873,7 +888,7 @@ static void rps_state_machine_update(GameContext* game_context) {
// Update the state machine to reflect that a remote user joined the game. // Update the state machine to reflect that a remote user joined the game.
// @param game_context pointer to a GameContext. // @param game_context pointer to a GameContext.
static void rps_state_machine_remote_joined(GameContext* game_context) { static bool rps_state_machine_remote_joined(GameContext* game_context) {
if(StateHostingLookingForPlayer == game_context->data->local_player) { if(StateHostingLookingForPlayer == game_context->data->local_player) {
FURI_LOG_I(TAG, "Remote player joined our game!"); FURI_LOG_I(TAG, "Remote player joined our game!");
game_context->data->remote_player = StateReady; game_context->data->remote_player = StateReady;
@ -881,9 +896,11 @@ static void rps_state_machine_remote_joined(GameContext* game_context) {
game_context->data->local_player = StateReady; game_context->data->local_player = StateReady;
game_context->data->local_move_tick = furi_get_tick(); game_context->data->local_move_tick = furi_get_tick();
game_context->data->screen_state = ScreenPlayingGame; game_context->data->screen_state = ScreenPlayingGame;
return true;
} else { } else {
FURI_LOG_I( FURI_LOG_I(
TAG, "Remote requested join, but we are state %c!", game_context->data->local_player); TAG, "Remote requested join, but we are state %c!", game_context->data->local_player);
return false;
} }
} }
@ -1182,6 +1199,75 @@ static void remote_games_add(GameContext* game_context, GameEvent* game_event) {
furi_mutex_release(game_context->mutex); furi_mutex_release(game_context->mutex);
} }
// Saves a game result to the file system.
// @param game_context pointer to a GameContext.
static void save_result(GameContext* game_context) {
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) != FuriStatusOk) {
return;
}
FuriHalRtcDateTime datetime;
furi_hal_rtc_get_datetime(&datetime);
furi_string_printf(
game_context->data->buffer,
"%c%c\t%04d-%02d-%02dT%02d:%02d:%02d\t%s\t%s",
game_context->data->local_player,
game_context->data->remote_player,
datetime.year,
datetime.month,
datetime.day,
datetime.hour,
datetime.minute,
datetime.second,
(game_context->data->remote_name) ? furi_string_get_cstr(game_context->data->remote_name) :
"Unknown",
(game_context->data->remote_contact) ?
furi_string_get_cstr(game_context->data->remote_contact) :
CONTACT_INFO_NONE);
FURI_LOG_I(TAG, "Saving result: %s", furi_string_get_cstr(game_context->data->buffer));
Storage* storage = furi_record_open(RECORD_STORAGE);
File* games_file = storage_file_alloc(storage);
// If apps_data directory doesn't exist, create it.
if(!storage_dir_exists(storage, RPS_APPS_DATA_FOLDER)) {
FURI_LOG_I(TAG, "Creating directory: %s", RPS_APPS_DATA_FOLDER);
storage_simply_mkdir(storage, RPS_APPS_DATA_FOLDER);
} else {
FURI_LOG_I(TAG, "Directory exists: %s", RPS_APPS_DATA_FOLDER);
}
// If rock_paper_scissors directory doesn't exist, create it.
if(!storage_dir_exists(storage, RPS_GAME_FOLDER)) {
FURI_LOG_I(TAG, "Creating directory: %s", RPS_GAME_FOLDER);
storage_simply_mkdir(storage, RPS_GAME_FOLDER);
} else {
FURI_LOG_I(TAG, "Directory exists: %s", RPS_GAME_FOLDER);
}
// Append contents to ending of games.txt (create if doesn't exist)
if(storage_file_open(games_file, RPS_GAME_PATH, FSAM_WRITE, FSOM_OPEN_APPEND)) {
FURI_LOG_E(TAG, "Opened file: %s", RPS_GAME_PATH);
if(!storage_file_write(
games_file,
furi_string_get_cstr(game_context->data->buffer),
furi_string_size(game_context->data->buffer))) {
FURI_LOG_E(TAG, "Failed to write to file.");
}
storage_file_write(games_file, "\n", 1);
} else {
FURI_LOG_E(TAG, "Failed to open file: %s", RPS_GAME_PATH);
}
storage_file_close(games_file);
storage_file_free(games_file);
furi_record_close(RECORD_STORAGE);
furi_mutex_release(game_context->mutex);
}
// This is the entry point for our application, which should match the application.fam file. // This is the entry point for our application, which should match the application.fam file.
int32_t rock_paper_scissors_app(void* p) { int32_t rock_paper_scissors_app(void* p) {
UNUSED(p); UNUSED(p);
@ -1496,6 +1582,7 @@ int32_t rock_paper_scissors_app(void* p) {
break; break;
case GameEventPlaySong: case GameEventPlaySong:
play_song(game_context->data->local_player); play_song(game_context->data->local_player);
save_result(game_context);
break; break;
case GameEventDataDetected: case GameEventDataDetected:
rps_receive_data(game_context, event.tick); rps_receive_data(game_context, event.tick);
@ -1520,8 +1607,20 @@ int32_t rock_paper_scissors_app(void* p) {
case GameEventRemoteJoined: case GameEventRemoteJoined:
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) { if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
if(event.game_number == game_context->data->game_number) { if(event.game_number == game_context->data->game_number) {
rps_state_machine_remote_joined(game_context); if(rps_state_machine_remote_joined(game_context)) {
rps_broadcast_join_acknowledge(game_context); rps_broadcast_join_acknowledge(game_context);
if(game_context->data->remote_name) {
furi_string_free(game_context->data->remote_name);
}
game_context->data->remote_name = event.sender_name;
if(game_context->data->remote_contact) {
furi_string_free(game_context->data->remote_contact);
}
game_context->data->remote_contact = event.sender_contact;
// Take ownership of the name and contact
event.sender_name = NULL;
event.sender_contact = NULL;
}
} else { } else {
FURI_LOG_T( FURI_LOG_T(
TAG, TAG,
@ -1537,11 +1636,22 @@ int32_t rock_paper_scissors_app(void* p) {
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) { if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
if(event.game_number == game_context->data->game_number) { if(event.game_number == game_context->data->game_number) {
FURI_LOG_I(TAG, "Remote join acknowledged."); FURI_LOG_I(TAG, "Remote join acknowledged.");
if(game_context->data->remote_name) {
furi_string_free(game_context->data->remote_name);
}
game_context->data->remote_name = event.sender_name;
if(game_context->data->remote_contact) {
furi_string_free(game_context->data->remote_contact);
}
game_context->data->remote_contact = event.sender_contact;
game_context->data->remote_player = StateReady; game_context->data->remote_player = StateReady;
game_context->data->remote_move_tick = furi_get_tick(); game_context->data->remote_move_tick = furi_get_tick();
game_context->data->local_player = StateReady; game_context->data->local_player = StateReady;
game_context->data->local_move_tick = furi_get_tick(); game_context->data->local_move_tick = furi_get_tick();
game_context->data->screen_state = ScreenPlayingGame; game_context->data->screen_state = ScreenPlayingGame;
// Take ownership of the name and contact
event.sender_name = NULL;
event.sender_contact = NULL;
} else { } else {
FURI_LOG_T( FURI_LOG_T(
TAG, TAG,
@ -1601,6 +1711,13 @@ int32_t rock_paper_scissors_app(void* p) {
furi_message_queue_free(game_context->queue); furi_message_queue_free(game_context->queue);
furi_mutex_free(game_context->mutex); furi_mutex_free(game_context->mutex);
furi_string_free(game_context->data->buffer); furi_string_free(game_context->data->buffer);
if(game_context->data->remote_name) {
furi_string_free(game_context->data->remote_name);
}
if(game_context->data->remote_contact) {
furi_string_free(game_context->data->remote_contact);
}
remote_games_clear(game_context);
free(game_context->data); free(game_context->data);
free(game_context); free(game_context);

View File

@ -10,15 +10,24 @@
#include <notification/notification.h> #include <notification/notification.h>
#include <notification/notification_messages.h> #include <notification/notification_messages.h>
#include <storage/storage.h>
#include <lib/subghz/subghz_tx_rx_worker.h> #include <lib/subghz/subghz_tx_rx_worker.h>
#define RPS_APPS_DATA_FOLDER EXT_PATH("apps_data")
#define RPS_GAME_FOLDER \
RPS_APPS_DATA_FOLDER "/" \
"rock_paper_scissors"
#define RPS_GAME_FILE_NAME "games.txt"
#define RPS_GAME_PATH RPS_GAME_FOLDER "/" RPS_GAME_FILE_NAME
// This is sent at the beginning of all RF messages. NOTE: It must end with the ':' character. // This is sent at the beginning of all RF messages. NOTE: It must end with the ':' character.
#define RPS_GAME_NAME "RPS:" #define RPS_GAME_NAME "RPS:"
#define TAG "rock_paper_scissors_app" #define TAG "rock_paper_scissors_app"
// Name for "N", followed by your name without any spaces. // Name for "N", followed by your name without any spaces.
#define CONTACT_INFO "NYourNameHere" #define CONTACT_INFO "NYourNameHere"
#define CONTACT_INFO_NONE "NNone"
// The message max length should be no larger than a value around 60 to 64. // The message max length should be no larger than a value around 60 to 64.
#define MESSAGE_MAX_LEN 60 #define MESSAGE_MAX_LEN 60
@ -181,6 +190,7 @@ typedef struct {
uint32_t tick; // The time the event originated (furi_get_tick()). uint32_t tick; // The time the event originated (furi_get_tick()).
uint16_t game_number; // The game number for the message. uint16_t game_number; // The game number for the message.
FuriString* sender_name; // If not null, be sure to release this string. FuriString* sender_name; // If not null, be sure to release this string.
FuriString* sender_contact; // If not null, be sure to release this string.
} GameEvent; } GameEvent;
typedef struct GameInfo { typedef struct GameInfo {
@ -201,6 +211,8 @@ typedef struct {
uint32_t remote_move_tick; uint32_t remote_move_tick;
struct GameInfo* remote_games; struct GameInfo* remote_games;
struct GameInfo* remote_selected_game; struct GameInfo* remote_selected_game;
FuriString* remote_name;
FuriString* remote_contact;
} GameData; } GameData;
// This is our application context. // This is our application context.
@ -343,7 +355,7 @@ static void rps_state_machine_update(GameContext* game_context);
// Update the state machine to reflect that a remote user joined the game. // Update the state machine to reflect that a remote user joined the game.
// @param game_context pointer to a GameContext. // @param game_context pointer to a GameContext.
static void rps_state_machine_remote_joined(GameContext* game_context); static bool rps_state_machine_remote_joined(GameContext* game_context);
// Update the state machine to reflect the local user's move. // Update the state machine to reflect the local user's move.
// @param game_context pointer to a GameContext. // @param game_context pointer to a GameContext.
@ -366,5 +378,7 @@ static void remote_games_next(GameContext* game_context);
static void remote_games_previous(GameContext* game_context); static void remote_games_previous(GameContext* game_context);
static void remote_games_add(GameContext* game_context, GameEvent* game_event); static void remote_games_add(GameContext* game_context, GameEvent* game_event);
static void save_result(GameContext* game_context);
// This is the entry point for our application, which should match the application.fam file. // This is the entry point for our application, which should match the application.fam file.
int32_t rock_paper_scissors_app(void* p); int32_t rock_paper_scissors_app(void* p);