Shared Data Between Apps
SharedData/ is a directory that every app can read and write. An app reaches it from its
own root as ../SharedData/.
It exists because some data belongs to the user, not to an app. Stride calibration is the case the SDK already ships: the Running app measures it outdoors with GNSS, and the Treadmill app needs it indoors where there is no GNSS.
On the watch the directory sits beside the app directories rather than inside any of them —
Apps/SharedData/, a sibling of Apps/Running/ and the rest — and it is visible over USB
mass storage, so you can read a shared file from a desktop or seed one by hand.
For app-private files, see the Files tutorial.
The path
static constexpr const char *kPath = "../SharedData/stride.json";
Paths are relative to the app’s own root. The whole path, filename included, must fit
IFileSystem::skMaxPathLen (256 bytes).
../SharedData/ is the only writable path that may leave your app’s root, and it is a
whitelist rather than general parent traversal. The kernel resolves ../SharedData and
../SharedData/<name> to the shared directory, and rejects every other .. segment
outright — leading, embedded or trailing. The name is reserved by that rule, so an app
cannot create a sibling directory that collides with it.
../SomethingElse/file is rejected during path resolution, before any filesystem call is
made. Note where that leaves you: fs.file() still hands back a non-null object, and it is
open() that returns false — the same false you would get for a file that simply is not
there. A null check will not catch a bad path, and nothing distinguishes “you left the
sandbox” from “no such file”.
Nested paths inside it do work. ../SharedData/maps/uk.map resolves, and
mkdir("../SharedData/maps") creates both levels.
Read-only folders at the volume root
A path starting :/ names a folder at the root of the volume the apps live on, beside
Apps/ rather than inside it. The kernel offers one: :/Workouts, which holds the
structured workout files that SDK::Workout::Library reads
(WorkoutLibrary.hpp:38-40). It resolves
:/Workouts and :/Workouts/<name>, rejects a .. segment inside them, and rejects any
other :/ name.
These folders are read-only to apps. Opening a file in one for writing returns false, and
so do write(), truncate(), rename(), remove() and mkdir(). Data an app writes for
other apps to read belongs in ../SharedData/.
Who uses it today
Path |
Written by |
Read by |
|---|---|---|
|
|
|
|
|
recovery only |
|
Running app, when tracing is on ( |
diagnostic only |
|
Treadmill app, backing up the LUT before a user-initiated clear ( |
recovery only |
The last three are conditional — a watch that has never hit a corrupt store, never enabled
tracing and never cleared its calibration holds only stride.json. Expect the others to
appear, but do not require them.
Create the directory before opening a file in it
IFileSystem::mkdir() creates missing parent directories. Opening a file does not. A writer
that skips the mkdir works on your watch and fails on one that has never run a calibrating
app:
// Ensure the SharedData directory exists (FatFs f_open does not create
// missing parents). "Already exists" counts as success.
const char *slash = std::strrchr(mPath, '/');
if (slash != nullptr) {
char dir[SDK::Interface::IFileSystem::skMaxPathLen] {};
std::snprintf(dir, sizeof(dir), "%.*s", static_cast<int>(slash - mPath), mPath);
if (!mFs.mkdir(dir)) {
return false;
}
}
Every read is optional
Your reader has to work on a watch where nothing has written the file yet. That is the normal state of a new device, not an edge case.
StrideLut::loadFromFile() handles it by clearing itself and returning false when the
file is absent or will not open
(StrideLut.cpp:163-175). The caller gets an
all-zero LUT and falls back to a default model.
One caveat, and it bites precisely where this rule tells you to stop looking. IFile::open()
returns a plain bool. File absent, file locked by another app’s write, timed out waiting
for the volume, and too many files already open all arrive as the same false. A reader
that treats every failure as “nothing has written this yet” will quietly mistake another
app is writing it right now for no data exists, and fall back to defaults.
StrideLut::loadFromFile() does exactly that, and for stride calibration the cost is one
session on a default model — a fair trade. If your shared file is larger or more expensive
to regenerate, decide deliberately whether a failed read means “empty” or “try again later”.
The filesystem will not tell you which it was.
Share only what is shared
App-specific data stays in the app’s own root even when it is closely related to something shared. The Treadmill app keeps its delta-LUT out:
/// Delta-LUT filename in the Treadmill app's own root (NOT under SharedData).
CadenceStrideModelConfig.hpp:72
Ask whether another app would be right to read the file, not just curious about it. A user’s stride length is theirs and follows them between apps. A treadmill’s calibration offset only means anything inside the model that produced it.
Concurrent access
Two apps writing the same shared file cannot corrupt it, and cannot both hold it open.
Filesystem calls are serialised — every operation takes a mutex before touching the media,
so writes queue rather than interleave. On top of that the filesystem keeps a table of open
files: it refuses a second open of anything already open for writing, and refuses a
write-mode open of anything open at all. The loser simply gets false from open().
That table is small, and it is shared by the entire watch. The system log, the activity recorder, settings, and every process of every running app draw on the same budget of ten simultaneously open files. The eleventh open fails no matter who asks. An app that holds several shared files open at once is competing with everything else the watch is doing, and whether it fails depends on what that happens to be. Open what you need, use it, close it.
Expect interrupted writes
Apps are scheduled independently, and a watch can lose power mid-write.
The obvious defence is not available: rename() will not replace an existing file.
Renaming onto a name already in use fails, so the POSIX idiom of writing a temporary file
and renaming it over the original does not work here. You would have to remove the target
first, which reopens the very window the idiom exists to close.
What the SDK does instead is rotate and fall back. RecordingMarker writes a temporary
file, moves the current good file aside to .bak (clearing any stale .bak first, because
of the rename rule above), renames the temporary into place, and has its reader fall back to
the .bak when the primary will not parse
(RecordingMarker.cpp:82-140). A crash at any
single step leaves at least one intact copy. Copy that shape when the file matters.
Be clear-eyed about what the calibration store does not do. OutdoorStrideCalibrator
overwrites stride.json in place, truncating on open, so a power loss mid-write leaves a
torn file and no backup. The .bak in the table above is written later — on a subsequent
load, once the store has proved larger than the calibrator accepts or failed to parse.
Nothing checks that the copy succeeded, so even then it may be missing. That is recovery
evidence, not protection.
stride_deleted.json is not protection either: the Treadmill app writes it before an
explicit user-initiated clear calibration, never before a routine save.
The failure to design against is a reader that silently accepts half a record.
Lifetime
Shared files outlive the app that wrote them. SharedData/ is a sibling of the app
directories rather than a child of any one of them, so removing an app cannot take it along.
That is the point — a user’s stride calibration should survive reinstalling Running.
There is no automatic cleanup. No owner, no reference count, and no screen on the watch that
lists shared files. Deletion happens only when something explicitly asks for it — an app
acting on the user’s instruction, the way Treadmill’s clear-calibration action removes the
stride store, or a factory reset, which clears the directory along with everything else
under Apps/. Otherwise whatever you write stays.
A user with a USB cable can delete shared files by hand, since the directory is visible over mass storage. That is a reasonable escape hatch for a calibration gone bad. It is not a substitute for being able to do it on the watch.
For a few kilobytes of calibration none of this costs anything. For anything large — a downloaded map set, say — it means leaving data on the watch that nothing owns and no on-device screen can remove. If your app writes something big, give the user a way to remove it.
In the simulator
Two differences, and the second one is the one that will catch you.
The simulator’s filesystem root is Output/
(Kernel.cpp:17). It passes .. through to
the host filesystem instead of clamping it to that root, so ../SharedData/ lands beside
Output/ and not inside it. Apps behave correctly. The files are just not where you would
first look for them. A :/ path goes to Volume/ inside that root, so :/Workouts is
Output/Volume/Workouts/
(FileSystem.cpp:45-54).
More importantly, the simulator does not enforce the whitelist at all. It concatenates its
prefix with your path and hands the result to the host filesystem, so ../anything/at/all
works in the simulator and fails on the watch — and the simulator gives you no hint that you
have left the sandbox. Nor does it make :/ folders read-only: a write there succeeds in
the simulator and fails on the watch. If you invent a new shared path, check it against the rule in The
path above rather than against what the simulator accepts.