Chapter 17: Pebble Smartwatch File I/O
In the last chapter, we overviewed the kind of file I/O that was part of standard C. We also noted that, because of the nature of Pebble smartwatches, very little of that standard file I/O is available on a Pebble smartwatch platform.
In this chapter, we will examine what file I/O is possible on a Pebble smartwatch. Many of the same file concepts will apply here. However, the source and location of the files was well as the methods we use to access those files will be different.
We should note that, just like the last chapter, file I/O on a smartwatch is not part of C syntax or semantics. It is all based on function calls provided by the Pebble SDK libraries. Like in the last chapter, our discussion here will focus on which functions we need and how to use them.
An Introduction
There are two sources for files on a Pebble smartwatch. Resources are files that are included with the smartwatch app install package, the PBW file, that includes the smartwatch executable application. Like files that were accessed with standard C file I/O, these files must be opened before they are used and accessed with read operations. Unlike files accessed with standard C file I/O, these files are read-only, that is, no writing is possible, and they do not need to be closed.
The second file source is persistent storage that resides on a smartwatch itself. This storage can be compared to a single file where data is stored in a key/value pair format. There is only one source of persistent data; it does not need to be opened or closed; and it cannot be accessed as a raw set of bytes. Rather, it is accessed solely as keyed data, where data is retrieved as a data object associated with a specific integer key.
File I/O with Resources
File I/O with install package resources starts with the creation of the install package. When the install package is built, external files are included in the build process. How to include files is outside of our scope here, but you can read instructions, as well as the many types of files you can include, in the Pebble documentation at this link.
We will concentrate our file I/O discussion on raw data files. This will most closely match the discussion from the last chapter, because it focuses on files as sequences of bytes.
In order to read from a resource file, you must have given that file a file ID when you built the install package. This ID will be used to open the file.
Opening a Resource File
To open a resource file, you must use the resource_get_handle()
function. A resource handle is analogous to a file descriptor; it is a system structure that is used to access a resource file. The data type of a resource handle is ResHandle
and prototype of the opener function is
ResHandle resource_get_handle(uint32_t resource_id);
The resource_id
is a 32-bit unsigned integer that is the ID of the file you need from the install package. You don't need you know the actual value of this resource ID; the name of this ID in the program is specified by a constant with the prefix "RESOURCE_ID_" combined with the ID name you gave the file when the install package was built.
Let's take an example. Back in chapter 9, Project 9.3 introduced the game of "madlibs", where you would fill in blanks in a sentence with random words. These random words were chosen from files that were included with the code. There are four files that were included with this project:
- "madlibs.txt" is a file of madlibs, with the ID "MADLIBS".
- "nouns.txt" is a file of nouns, one per line, with the ID "NOUNS".
- "verbs.txt" is a file of verbs, one per line, with the ID "VERBS".
- "adjectives.txt" is a file of adjectives, one per line, with the ID "ADJECTIVES".
The code for using the data in these files declared four handles, one per file:
static ResHandle madlib_handle;
static ResHandle noun_handle;
static ResHandle verb_handle;
static ResHandle adjective_handle;
Each file was opened, and its handle initialized, by a call to resource_get_handle
:
madlib_handle = resource_get_handle(RESOURCE_ID_MADLIBS);
noun_handle = resource_get_handle(RESOURCE_ID_NOUNS);
verb_handle = resource_get_handle(RESOURCE_ID_VERBS);
adjective_handle = resource_get_handle(RESOURCE_ID_ADJECTIVES);
At this point, each file was opened and was accessible using its respective handle.
Resource File Size
The number of bytes in a resource file can be derived using a call to the function resource_size()
. The prototype for this function is given here:
size_t resource_size(ResHandle h);
Given a valid resource handle, this function will return the number of bytes in the file.
For the madlibs application, this information was helpful. For example, to generate a random noun from the noun file, we picked a random file position between 0 and the number of bytes in the file. We got the number of bytes in the file this way:
static size_t madlibsize;
static size_t nounsize;
static size_t verbsize;
static size_t adjectivesize;
...
madlibsize = resource_size(madlib_handle);
nounsize = resource_size(noun_handle);
verbsize = resource_size(verb_handle);
adjectivesize = resource_size(adjective_handle);
size_t
is a data type that depicts the idea of "size"; on Pebble smartwatches, this is an unsigned 32-bit integer.
In the above example, we derive the size of each file, that is, the number of bytes, by using the resource handle for the file in the function call.
Reading from Resource Files
To read from resource files, we use the resource handle in functions that implement two types of read operations.
To read all the data in a resource file in one operation, we can use the resource_load()
function. The prototype for this function is as follows:
size_t resource_load(ResHandle h, uint8_t *buffer, size_t max_length);
We need a resource handle for the file we are reading from (h
), a buffer allocated to have the correct number of bytes we expect (buffer
), and a maximum number of bytes to read from the file (max_length
).
We did not use this type of read operation for the madlibs application. However, if we did, we would use the file size data. In the code above, we derived the size of each file after we opened it, so we can use this size to allocation a buffer and to cap the loading of data from the resource file.
For example, if we were read all the nouns from the "nouns.txt" file, we might do it this way:
uint8_t *noun_buffer = (uint8_t *) malloc(nounsize * sizeof(uint8_t));
size_t numbytes = resource_load(noun_handle, noun_buffer, nounsize);
Note that the number of bytes actually copied from the resource file is returned by calling this function. This example would fill the noun_buffer
with all the bytes/characters from the noun file.
The second way to read data from a resource file is to read a range of bytes, rather than all the bytes, from a file. We can use the resource_load_byte_range()
function for this, who prototype is below:
size_t resource_load_byte_range(ResHandle h, uint32_t start_offset, uint8_t * buffer, size_t num_bytes);
Here, we need the resource handle of the file we want to read from (h
), the byte at which to start reading (start_offset
), the buffer to copy the byte range to (buffer
), and the number of bytes to copy (num_bytes
).
For our madlibs example, we used this method of reading the resource files in a number of ways. Let's say we need to find a random noun. Here's a description of the steps we use to find the noun:
- Generate a random number between 0 and the number of bytes in a file. We use this as the "current" position in the file.
- Since the nouns are in the file one per line, we look backwards for a line feed character that ends the previous line, then forwards to find the line feed character for the current line. The chosen noun lies between the two line feeds.
- Read the file starting at the previous line feed to the next line feed for the noun.
We first generate our random file position:
position = rand()%nounsize;
Then we find the noun with steps 2 and 3 from above:
int start = findlinefeed(noun_handle, position, noun_size, BACKWARD);
if (start != 0) start++;
int end = findlinefeed(noun_handle, position, noun_size, FORWARD);
if (end <= start) end = findlinefeed(noun_handle, position+1, noun_size, FORWARD);
int length = end-start;
uint8_t ubuffer = (uint8_t *)malloc((length+1)*sizeof(uint8_t));
resource_load_byte_range(noun_handle, start, ubuffer, length);
ubuffer[length] = '\0';
In this code, BACKWARD
has the value -1
and FORWARD
has the value 1
. When we first use the function findlinefeed()
, we move backward to find the line feed. Then we call the function again and we move characters forward to find it. After we figure out where the previous and next line feeds are, we call resource_load_byte_range()
to extract the character sequence that represents the noun from the file.
Note here that the function returns a byte sequence, not a string. The code above inserts a NULL character to treat the byte sequence like an actual string.
As one more example, let's look at the code for findlinefeed()
. This code is interesting because it walks through the reasource file a single character/byte at a time, looking for the line feed character.
int findlinefeed(ResHandle handle, int startpos, int filesize, int direction) {
int position = startpos;
char c = 0;
uint8_t character[1];
while (position >= 0 && position < filesize) {
resource_load_byte_range(handle, position, character, 1);
c = character[0];
if (c == '\n') break;
position += direction;
}
return position;
}
The code above uses a 1-character buffer in the call to resource_load_byte_range()
. Note that the buffer here is not dynamically allocated; it is a statically declared array of size 1.
File I/O Using Persistent Storage
Resource files accompany the executable file in the installation package transferred to the smartwatch. There is also accessible storage that exists on a smartwatch itself, regardless of what applications are installed. This storage is represented as a single file, always open, structured as a set of key/value pairs. This osmartwatch storage is persistent; it is present on a smartwatch between power cycles and no matter which applications are installed.
Persistent storage is always open. There is no function needed to open persistent storage. Storage is always associated with specific applications; each application is prevented from accessing another application's storage. The maximum size that each application's storage space can take up is (currently) 4,096 bytes.
Persistent storage is based on key/value pairs. A "key" is a 32-bit unsigned integer that is tied to a specific value. If a value is connected to a specific key and then written, that value can be retrieved by using that same key. For example, if we wanted to store the number of milliseconds for a timer, we might use the key "1234" and store the number of milliseconds (an integer) this way:
status_t status = persist_write_int(1234, milliseconds);
We could then retrieve the number of milliseconds later using a read function:
int millis = persist_read_int(1234);
This function would look for an integer value that was previous written using the key "1234" and would return it.
Access to persistent storage is through a set of functions provided by the Pebble operating system. In general, we would use functions that adhere to the pattern below to write data:
status_t persist_write_TYPE(const uint32_t key, const TYPE value);
TYPE can be one of "bool" for booleans, "int" for integers, "string" for strings, and "data" for arrays of 8-bit bytes. Each function returns a status_t
type, which is a 32-bit integer. This return type represents either the number of bytes written, if the number is positive, or an error code, if the number if negative.
Let's look at a madlib example. Let's say that we want to record the noun that we use, because we don't want to use the same nouns the next time we run the madlib application. We will get nouns for NOUN and NOUN2 designations. We will generate nouns and save them. Using our definitions in the code, we process NOUN and NOUN2 designations like this:
thing = random_thing(NOUN);
replace_string(madlibstr, "<NOUN>", thing);
free(thing);
thing = random_thing(NOUN);
replace_string(madlibstr, "<NOUN2>", thing);
free(thing);
Now, we need to add definitions of the integer keys:
#define SAVE_NOUN 1000
#define SAVE_NOUN2 1001
And we can save our nouns to persistent storage this way:
thing = random_thing(NOUN);
replace_string(madlibstr, "<NOUN>", thing);
persistent_write_string(SAVE_NOUN, thing);
free(thing);
thing = random_thing(NOUN);
replace_string(madlibstr, "<NOUN2>", thing);
persistent_write_string(SAVE_NOUN2, thing);
free(thing);
Like writing, reading persistent storage depends on functions whose names adhere to the following pattern:
TYPE persist_read_TYPE(const uint32_t key);
Again, the TYPE is to be one of "bool" for booleans, "int" for integers, "string" for strings, and "data" for arrays of 8-bit bytes. Each function returns the type it is reading.
If we were to add reading of persistent data to our example, we would want to read the nouns, then compare them to the random nouns read from the file. For the first noun, we could use this changed code:
char saved[30];
thing = random_thing(NOUN);
int numbytes = persist_read_string(SAVE_NOUN, saved, 30);
do {thing = random_thing(NOUN);} while (strcmp(saved, thing));
replace_string(madlibstr, "<NOUN>", thing);
persist_write_string(SAVE_NOUN, thing);
free(thing);
We would use similar code for NOUN2.
Three more functions help us to work with persistent data.
persist_exists()
returns a boolean value that tells us if a value has been set for a certain key. For example, we could test if a noun has been saved to persistent storage by callingpersist_exists(SAVE_NOUN)
.persist_delete()
will delete the value connected with the key parameter. If we wanted to make sure that our saved nouns were deleted, we would callpersist_delete(SAVE_NOUN)
andpersist_delete(SAVE_NOUN2)
.persist_get_size()
will get the number of bytes in storage for the key given as the parameter. If we wanted to see how many bytes were stored for the nouns we saved, we would callpersist_get_size(SAVE_NOUN)
andpersist_get_size(SAVE_NOUN2)
. This function returns an integer size or an error code (E_DOES_NOT_EXIST
, a negative number) if there is no storage for the key given.
Project Exercises
Project 17.1
Let's work with the madlib example again. Start by loading the Project Exercise 9.3 into CloudPebble.
You are to add adverbs to the madlib sentence. Adverbs are words that modify a verb, often ending in "ly". So "stiffly" is an adverb in this sentence: "The injured person walked stiffly to the car." You can find a file of adverbs here. You can find a small set of new madlibs with "
Get an adverb from the file in the same way we got nouns, verbs, and adjectives. Get a new sentence from the madlib file and replace the "
You can find an answer to this project here.
Project 17.2
Let's modify the adverb project to use persistent data. Starting with the Project 17.1 answer, use persistent storage to store the adverb that is used and to prevent the same adverb from being used twice in a row. You can use the same pattern that we used for the NOUN example in the chapter.
How should you handle the case where no adverb had been written? How do you detect this?
You can find an example answer here.
Project 17.3
Let's make some changes again to Project 17.1. Using persistent storage, add the necessary statements to keep track of the number of nouns, verbs, adjectives, and adverbs that have been read from the resource files. Display these on the screen when the "up" or "down" buttons are pressed.