Skip to content

Commit

Permalink
Autosave (#374)
Browse files Browse the repository at this point in the history
* auto save functionality, autosave interval is 60sec
* last explicit save state can still be loaded by user from project screen `Load`
* update user manual to explain new autosave function
  • Loading branch information
maks authored Feb 15, 2025
1 parent 4162008 commit 76dedf0
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 12 deletions.
15 changes: 14 additions & 1 deletion docs/DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,17 @@ A helpful tool for debugging MIDI output is [ShowMIDI](https://github.com/gbevin

### USB MIDI output

TODO
TODO

## Notes on code structure

### Key classes

Some classes are worth documenting specifically here as they contain key pieces of functionality.

`sources/Adapters/picoTracker/gui/picoTrackerEventManager.cpp`

`picoTrackerEventManager` is important because it's where `MainLoop()` is and hence it's where "events" are dispatched.
It is also here that the 1ms tick timer callback is set up and the callback function for it is found: `timerHandler()`. The `timerHandler()` function is then the source of the `PICO_CLOCK` events, which are then dispatched to the `picoTrackerEventQueue` at the rate of 1Hz.

`AppWindow::AnimationUpdate()` is the handy spot where we handle calling autosave just because of the convienence of the `AppWindow` class having easy access to the player, the current project name and the persistance service.
7 changes: 4 additions & 3 deletions sources/Adapters/picoTracker/gui/picoTrackerEventManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ picoTrackerEventQueue *queue;
char inBuffer[INPUT_BUFFER_SIZE];
#endif

// timer callback at a rate of 1kHz (from a 1ms hardware interrupt timer)
bool timerHandler(repeating_timer_t *rt) {
queue = picoTrackerEventQueue::GetInstance();
gTime_++;

// send a clock (PICO_CLOCK) event once every second
if (gTime_ % 1000 == 0) {
queue->push(picoTrackerEvent(PICO_CLOCK));
}
Expand All @@ -58,9 +61,7 @@ bool picoTrackerEventManager::Init() {

keyboardCS_ = new KeyboardControllerSource("keyboard");

// TODO: fix this, there is a timer service that should be used. Also all of
// this keyRepeat logic is already implemented in the eventdispatcher
// Application/Commands/EventDispatcher.cpp
// setup a repeating timer for 1ms ticks
add_repeating_timer_ms(1, timerHandler, NULL, &timer_);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "Adapters/picoTracker/utils/utils.h"
#include "Application/Model/Config.h"
#include "Application/Utils/char.h"
#include "Player/Player.h"
#include "System/Console/Trace.h"
#include "System/System/System.h"
#include "UIFramework/BasicDatas/GUIEvent.h"
Expand Down
27 changes: 27 additions & 0 deletions sources/Application/AppWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#include <nanoprintf.h>
#include <string.h>

const uint16_t AUTOSAVE_INTERVAL_IN_SECONDS = 1 * 60;

AppWindow *instance = 0;

unsigned char AppWindow::_charScreen[SCREEN_CHARS];
Expand Down Expand Up @@ -495,6 +497,18 @@ void AppWindow::AnimationUpdate() {
loadProject_ = false;
}
_currentView->AnimationUpdate();

// *attempt* to auto save every AUTOSAVE_INTERVAL_IN_MILLIS
// will return false if auto save was unsuccessful because eg. the sequencer
// is running
// we do this here because for sheer convenience because this
// callback is called every second and we have easy access in this class to
// the player, projectname and persistence service
if (++lastAutoSave > AUTOSAVE_INTERVAL_IN_SECONDS) {
if (autoSave()) {
lastAutoSave = 0;
}
}
}

void AppWindow::LayoutChildren(){};
Expand Down Expand Up @@ -616,3 +630,16 @@ void AppWindow::Print(char *line) {
};

void AppWindow::SetColor(ColorDefinition cd) { colorIndex_ = cd; };

bool AppWindow::autoSave() {
Player *player = Player::GetInstance();
// only auto save when sequencer is not running
if (!player->IsRunning()) {
Trace::Log("APPWINDOW", "AutoSaving Project Data");
// get persistence service and call autosave
PersistencyService *ps = PersistencyService::GetInstance();
ps->AutoSaveProjectData(projectName_);
return true;
}
return false;
}
4 changes: 4 additions & 0 deletions sources/Application/AppWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class AppWindow : public GUIWindow, I_Observer, Status {
void onQuitApp();

private:
bool autoSave();

View *_currentView;
ViewData *_viewData;
SongView *_songView;
Expand Down Expand Up @@ -120,6 +122,8 @@ class AppWindow : public GUIWindow, I_Observer, Status {
SysMutex drawMutex_;

bool loadProject_ = false;

uint32_t lastAutoSave = 0;
};

#endif
54 changes: 51 additions & 3 deletions sources/Application/Persistency/PersistencyService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,23 @@ PersistencyResult PersistencyService::Save(const char *projectName,
picoFS->CopyFile(pathBufferA.c_str(), pathBufferB.c_str());
};
}
return SaveProjectData(projectName, false);
};

PersistencyResult
PersistencyService::AutoSaveProjectData(const char *projectName) {
return SaveProjectData(projectName, true);
};

PersistencyResult PersistencyService::SaveProjectData(const char *projectName,
bool autosave) {

etl::vector segments = {PROJECTS_DIR, projectName, PROJECT_DATA_FILE};
const char *filename = autosave ? AUTO_SAVE_FILENAME : PROJECT_DATA_FILE;

etl::vector segments = {PROJECTS_DIR, projectName, filename};
CreatePath(pathBufferA, segments);

auto picoFS = PicoFileSystem::GetInstance();
PI_File *fp = picoFS->Open(pathBufferA.c_str(), "w");
if (!fp) {
Trace::Error("PERSISTENCYSERVICE: Could not open file for writing: %s",
Expand All @@ -119,6 +132,18 @@ PersistencyResult PersistencyService::Save(const char *projectName,

fp->Close();
delete (fp);

// if we are doing an explicit save (ie nto a autosave), then we need to
// delete the existing autosave file so that this explicit save will be loaded
// in case subsequent autosave has changes the user doesn't want to keep
if (!autosave) {
if (!ClearAutosave(projectName)) {
return PERSIST_ERROR;
}
Trace::Log("PERSISTENCYSERVICE", "Deleted Autosave File: %s\n",
pathBufferA.c_str());
}

return PERSIST_SAVED;
};

Expand All @@ -133,11 +158,25 @@ bool PersistencyService::Exists(const char *projectName) {
}

PersistencyResult PersistencyService::Load(const char *projectName) {
// first check if autosave exists
etl::string<128> autoSavePath(PROJECTS_DIR);
autoSavePath.append("/");
autoSavePath.append(projectName);
autoSavePath.append("/");
autoSavePath.append(AUTO_SAVE_FILENAME);

auto picoFS = PicoFileSystem::GetInstance();
bool useAutosave = (picoFS->exists(autoSavePath.c_str()));

Trace::Log("PERSISTENCYSERVICE", "using autosave: %b\n", useAutosave);
// if autosave exists, then we load it instead of the normal project file
const char *filename = useAutosave ? AUTO_SAVE_FILENAME : PROJECT_DATA_FILE;

etl::string<128> projectFilePath(PROJECTS_DIR);
projectFilePath.append("/");
projectFilePath.append(projectName);
projectFilePath.append("/");
projectFilePath.append(PROJECT_DATA_FILE);
projectFilePath.append(filename);

PersistencyDocument doc;
if (!doc.Load(projectFilePath.c_str()))
Expand Down Expand Up @@ -193,7 +232,16 @@ void PersistencyService::CreatePath(
path.clear();
// iterate over segments and concatenate using iterator
for (auto it = segments.begin(); it != segments.end(); ++it) {
path.append("/");
path.append(*it);
if (it != segments.end() - 1) {
path.append("/");
}
}
}

bool PersistencyService::ClearAutosave(const char *projectName) {
auto picoFS = PicoFileSystem::GetInstance();
etl::vector segments = {PROJECTS_DIR, projectName, AUTO_SAVE_FILENAME};
CreatePath(pathBufferA, segments);
return picoFS->DeleteFile(pathBufferA.c_str());
}
5 changes: 5 additions & 0 deletions sources/Application/Persistency/PersistencyService.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum PersistencyResult {

#define UNNAMED_PROJECT_NAME ".untitled"
#define PROJECT_DATA_FILE "lgptsav.dat"
#define AUTO_SAVE_FILENAME "autosave.dat"

class PersistencyService : public Service,
public T_Singleton<PersistencyService> {
Expand All @@ -37,11 +38,15 @@ class PersistencyService : public Service,
PersistencyResult CreateProject();
bool Exists(const char *projectName);
void PurgeUnnamedProject();
PersistencyResult AutoSaveProjectData(const char *projectName);
bool ClearAutosave(const char *projectName);

private:
PersistencyResult CreateProjectDirs_(const char *projectName);
void CreatePath(etl::istring &path,
const etl::ivector<const char *> &segments);
PersistencyResult SaveProjectData(const char *projectName, bool autosave);

// need these as statically allocated buffers as too big for stack
etl::vector<int, MAX_FILE_INDEX_SIZE> fileIndexes_;
etl::string<MAX_PROJECT_SAMPLE_PATH_LENGTH> pathBufferA;
Expand Down
5 changes: 3 additions & 2 deletions sources/Application/Views/ProjectView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ static void SaveAsOverwriteCallback(View &v, ModalView &dialog) {
const char *oldProjName = ((ProjectView &)v).getOldProjectName().c_str();

if (persist->Save(projName, oldProjName, true) != PERSIST_SAVED) {
Trace::Error("failed to save project");
Trace::Error("failed to save renamed project %s [old: %s]", projName,
oldProjName);
MessageBox *mb = new MessageBox(
((ProjectView &)v), "failed to save project", MBBF_OK | MBBF_CANCEL);
((ProjectView &)v), "Failed to save project", MBBF_OK | MBBF_CANCEL);
((ProjectView &)v).DoModal(mb, SaveAsOverwriteCallback);
return;
}
Expand Down
6 changes: 5 additions & 1 deletion sources/Application/Views/SelectProjectView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ void SelectProjectView::ProcessButtonMask(unsigned short mask, bool pressed) {
Trace::Log("SELECTPROJECTVIEW", "Select Project:%s \n", selection_);
// save newly opened projectname, it will be used to load the project file
// on device boots following the reboot below
PersistencyService::GetInstance()->SaveProjectState(selection_);
auto ps = PersistencyService::GetInstance();
ps->SaveProjectState(selection_);

// now need to delete autosave file so its not loaded when we reboot
ps->ClearAutosave(selection_);

// now reboot!
watchdog_reboot(0, 0, 0);
Expand Down
10 changes: 8 additions & 2 deletions usermanual/content/pages/projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ template: page

On the project screen you change various settings of the current project, save the current project, rename it (including giving it a random new name) create a new blank project or go to the project browser screen to **load** an another existing project.

Your current project settings are saved automatically every minute except when the sequencer is running, ie. when the current project is playing. This means that should you restart the picoTracker or accidently power off or a crash occurs, your current project state within the last minute will be restored when you restart the picoTracker.

You can ***explicitly*** save the current project by pressing [SAVE] on the project screen. By doing this you can then later on revert to the state that you just saved by reloading the current project using the [Load] button on screen button on the project screen.

## Current Project settings

- **Tempo:**: Can be set between 60bpm [0x3c] and 400bpm [0x190]. Resolution aligned to LSDJ.
Expand All @@ -21,9 +25,11 @@ On the project screen you change various settings of the current project, save t
## Project Management

- **project:** Displays the current name of the project and allows you to edit it
- **Load** Go to the project file browser to load a different project
- **Load** Go to the project file browser to load a different project or reload the last explicitly saved version of the current project
- **Save** Save the current project **NOTE:** *saving currently cannot be done during playback.*
- **New** *REPLACE* the current project with a new, *Blank* project.
- **Random** *RENAME* the current project with a new, *Randomly generated* name.

The project name is limited to 12 characters. The project name field allows deleting the currently selected character using the [EDIT] key.
The project name is **limited to 12 characters**.

You edit to project name by moving onto the name field and then holding the `ENTER` key while using the `UP` and `DOWN` keys to change the selected character and `LEFT` and `RIGHT` keys to move the cursor to the left or right of the current character. When on the last character, you can add chararacter to the end of the project name by using the `RIGHT` key.

0 comments on commit 76dedf0

Please sign in to comment.