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 frequency.
- 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):
- Log game results & contact info onto SD card.
- Config - Allow changing hard-coded CONTACT_INFO message.
- Allow viewing past games/scores.
- 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;
unsigned int game_number;
char randomInfo[MESSAGE_MAX_LEN];
char sender_contact[MESSAGE_MAX_LEN];
char sender_name[9];
char tmp;
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,
"%03u%s :%8s",
&game_number,
randomInfo,
sender_contact,
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!
FuriString* name = furi_string_alloc();
furi_string_set(name, sender_name);
FuriString* contact = furi_string_alloc();
furi_string_set(contact, sender_contact);
GameEvent event = {
.type = GameEventRemoteJoined,
.sender_name = name,
.sender_contact = contact,
.game_number = game_number};
furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
} else {
@ -234,12 +237,23 @@ static void rps_receive_data(GameContext* game_context, uint32_t tick) {
case GameRfPurposeJoinAcknowledge:
if(sscanf(
(const char*)message + game_name_len + 2,
"%03u :%8s",
"%03u%s :%8s",
&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 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 = {
.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);
} else {
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.
// 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.
static void rps_broadcast_join_acknowledge(GameContext* game_context) {
GameData* data = game_context->data;
unsigned int gameNumber = data->game_number;
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(
data->buffer,
"%s%c%c%03u :%s\r\n",
"%s%c%c%03u%s :%s\r\n",
RPS_GAME_NAME,
GameRfPurposeJoinAcknowledge,
MAJOR_VERSION,
data->game_number,
CONTACT_INFO,
furi_hal_version_get_name_ptr());
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.
// @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) {
FURI_LOG_I(TAG, "Remote player joined our game!");
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_move_tick = furi_get_tick();
game_context->data->screen_state = ScreenPlayingGame;
return true;
} else {
FURI_LOG_I(
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);
}
// 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.
int32_t rock_paper_scissors_app(void* p) {
UNUSED(p);
@ -1496,6 +1582,7 @@ int32_t rock_paper_scissors_app(void* p) {
break;
case GameEventPlaySong:
play_song(game_context->data->local_player);
save_result(game_context);
break;
case GameEventDataDetected:
rps_receive_data(game_context, event.tick);
@ -1520,8 +1607,20 @@ int32_t rock_paper_scissors_app(void* p) {
case GameEventRemoteJoined:
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
if(event.game_number == game_context->data->game_number) {
rps_state_machine_remote_joined(game_context);
rps_broadcast_join_acknowledge(game_context);
if(rps_state_machine_remote_joined(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 {
FURI_LOG_T(
TAG,
@ -1537,11 +1636,22 @@ int32_t rock_paper_scissors_app(void* p) {
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
if(event.game_number == game_context->data->game_number) {
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_move_tick = furi_get_tick();
game_context->data->local_player = StateReady;
game_context->data->local_move_tick = furi_get_tick();
game_context->data->screen_state = ScreenPlayingGame;
// Take ownership of the name and contact
event.sender_name = NULL;
event.sender_contact = NULL;
} else {
FURI_LOG_T(
TAG,
@ -1601,6 +1711,13 @@ int32_t rock_paper_scissors_app(void* p) {
furi_message_queue_free(game_context->queue);
furi_mutex_free(game_context->mutex);
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);

View File

@ -10,15 +10,24 @@
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <storage/storage.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.
#define RPS_GAME_NAME "RPS:"
#define TAG "rock_paper_scissors_app"
// Name for "N", followed by your name without any spaces.
#define CONTACT_INFO "NYourNameHere"
#define CONTACT_INFO_NONE "NNone"
// The message max length should be no larger than a value around 60 to 64.
#define MESSAGE_MAX_LEN 60
@ -181,6 +190,7 @@ typedef struct {
uint32_t tick; // The time the event originated (furi_get_tick()).
uint16_t game_number; // The game number for the message.
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;
typedef struct GameInfo {
@ -201,6 +211,8 @@ typedef struct {
uint32_t remote_move_tick;
struct GameInfo* remote_games;
struct GameInfo* remote_selected_game;
FuriString* remote_name;
FuriString* remote_contact;
} GameData;
// 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.
// @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.
// @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_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.
int32_t rock_paper_scissors_app(void* p);