Savefiles
Best practices
Sometimes you want an application to save its state. Following a few simple rules can make your user’s life easier:
Save to SD card: Saving to flash not only wears it down, but also produces nasty glitches in the audio because it blocks hardware bus access to the synth engine’s memory. This is not fixable. Because of this, we strongly discourage applications from ever save to flash by asking nicely: Don’t save to flash please. Unfortunately, many flow3rs have shipped with bad SD cards that may make your application crash, see the Troubleshooting section. Fortunately, we have a hunch that we can recognize them - the
st3m.utils.sd_reliable()
function returns our best guess.Use standard path for savefile directory: Your save directory should be located on the SD card in the
app_data
directory. The name of your save directory should be your app store account user name and the name of your app repo, simplified to slug representation and connected with a dash (-). For example, if your username is “Dashie” and your app repo “Cool App” is hosted athttps://git.flow3r.garden/dashie/cool-app/
, the standard directory path would beapp_data/Dashie-cool-app
(note how the user name didn’t get converted to lowercase). We will provide nicer API for this by the next release, bear with us for now and hardcode it manually please!Files in the application directory are deleted with the app: If your savefiles are not that big it’s okay to just keep them. However, if you keep large downloads or similar around they should be saved in the application directory provided by
app_ctx
inApplication.__init__
.Account for file corruption when parsing files: You never know when flow3r’s power switch is turned off. This may in rare cases lead to file corruption. Wrapping file opening and parsing in
try
/except
will do the trick with low effort.
Most stock apps we ship don’t follow these guidelines yet due to time constraints, but we will migrate them shortly. We’re leading with bad examples. Let’s show a good one instead:
Example
If you’re not familiar with python, this might seem like a lot to juggle, but it can be quite easy. Let’s expand the
CaptouchBlinky
example from the Blinky section to save its last state. The json
module together with python
standard dicts make it easy to create pretty config files:
import json
from st3m.utils import save_file_if_changed, mkdir_recursive
def check_color(col):
# length of col is guaranteed to be 3, no need to check
for val in col:
# type checking: raises ValueError if conversion doesn't work
val = float(val)
if val > 1 or val < 0:
# we could clamp them here instead too if we felt like it
raise ValueError
return col
class PersistentBlinky(CaptouchBlinky):
def __init__(self, app_ctx):
super().__init__(app_ctx)
# repo name: "Persistent Blinky", app store user name: "BlinkyFan"
# because we're saving on the SD card we must prefix it with "/sd/"
self.dirpath = "/sd/app_data/BlinkyFan-persistent-blinky"
self.savefile = "config.json"
self.savepath = self.dirpath + "/" + self.savefile
def on_exit(self):
super().on_exit()
# let's create a dict with rgb values!
save_data = {}
# (this could also just be a list tbh but bear with us)
save_data["red"] = self.active_color[0]
save_data["green"] = self.active_color[1]
save_data["blue"] = self.active_color[2]
try:
# if the save directory doesn't exist we must create it first.
# will raise OSError if SD card is not available.
mkdir_recursive(self.dirpath)
# this creates a nicely formatted string from our dict
save_file_content = json.dumps(save_data, indent = 4)
# it's good practice to not waste write cycles. this
# function checks first if the string is different from
# what is already written to the file and only writes
# if necessary.
save_file_if_changed(self.savepath, save_file_content)
except OSError:
print("Saving savefile failed")
def on_enter(self, vm):
super().on_enter(vm)
try:
# raises OSError if file doesn't exist or ValueError if
# decoding fails (desktop python raises a different one)
save_data = json.load(self.savepath)
# save_data access will raise KeyError if the dict contains
# no such field
col = (
save_data["red"],
save_data["green"],
save_data["blue"],
)
# check if field value is ok, else raise ValueError
# if things won't crash from corrupted values or maybe
# even types you can simplify this, don't worry :D
self.active_color = check_color(col)
except (OSError, ValueError, KeyError):
# it's nice to see on the REPL if something didn't work
print("Loading savefile failed")