sd-card: Only add header to the results file if it’s empty
[brewing-logger:firmware.git] / sd-card.c
1 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
2 /*
3  * Brewing Logger.
4  * Copyright (C) Philip Withnall 2012 <philip@tecnocode.co.uk>
5  *
6  * Brewing Logger is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Brewing Logger is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Brewing Logger.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 #include "config.h"
21
22 #include <string.h>
23
24 #include <avr/io.h>
25 #include <util/crc16.h>
26
27 #include "lib/diskio.h"
28 #include "lib/ff.h"
29
30 #include "sd-card.h"
31
32 /**
33  * \file
34  * \brief SD card control.
35  *
36  * Interaction with the SD card, if one is inserted in the SD card slot. This supports a single partition FAT file system on the SD, containing a brew
37  * configuration file and a brew results. The brew configuration file can be loaded (or overwritten if a default configuration is generated). The
38  * brew results file can be created and appended to.
39  *
40  * The code also supports generating a partition table and FAT file system if the card previously didn't have one.
41  *
42  * All fields in data structures stored on the SD card are encoded in little-endian format. e.g. A 16-bit field with value \c 0x1234 is encoded as
43  * bytes \c 0x12 and \c 0x34, with \c 0x12 being placed at the _higher_ address.
44  *
45  * \todo What happens if a non-FAT card is inserted? Does its file system get destroyed? That would be bad.
46  */
47
48 /**
49  * Notes:
50  *  - Chip selects are performed automatically by diskio.c.
51  */
52
53 /**
54  * \brief Checksum for a brew configuration.
55  *
56  * The checksum used for a brew configuration when stored on an SD card. It is calculated over all the fields of the BrewConfig except the version.
57  * A CRC-8 checksum is used, so all burst errors of 8 bits (or shorter) will be detected, as will longer burst errors with probability
58  * \f$1 - 2^{-8}\f$.
59  */
60 typedef uint8_t BrewConfigChecksum;
61
62 /**
63  * \brief Checksum for a brew results log entry.
64  *
65  * The checksum used for a log entry in a brew results file on an SD card. It is calculated over all the fields of the LogEntry. A CRC-8 checksum is
66  * used, so all burst errors of 8 bits (or shorter) will be detected, as will longer burst errors with probability \f$1 - 2^{-8}\f$.
67  */
68 typedef uint8_t BrewResultsChecksum;
69
70 /**
71  * \brief Brew configuration header.
72  *
73  * The storage layout of the header for a brew configuration file. This is used to read/write the first few bytes of a brew configuration file on the
74  * SD card. Once the header has been read, the version can be checked and then the correct storage format will be used to load/store the BrewConfig
75  * itself.
76  */
77 typedef struct {
78         uint8_t version;                /**< Brew configuration format version number. */
79         BrewConfigChecksum checksum;    /**< Brew configuration data checksum over all fields in the BrewConfig. */
80 } SdCardBrewConfigHeader;
81
82 /**
83  * \brief Brew results header.
84  *
85  * The storage layout of the header for a brew results file. This is used to write the first few bytes of a brew results file on the SD card. This
86  * allows the client-side software to parse the rest of the file as appropriate.
87  */
88 typedef struct {
89         uint8_t version;                /**< Brew results format version number. */
90         BrewConfigId brew_id;           /**< ID of the brew configuration for these results. */
91 } SdCardBrewResultsHeader;
92
93 /**
94  * \brief Current brew results format version.
95  *
96  * The version number of the current brew results format.
97  */
98 #define BREW_RESULTS_VERSION 1
99
100 /* We only support a single disk drive (the only drive; the SD card on the SPI bus). */
101 #define DISK_DRIVE_NUMBER 0
102
103 /* FAT filesystem on the SD card. */
104 static FATFS fat_fs;
105
106 SdCardInitResponse sd_card_init (void)
107 {
108         FRESULT fat_op_status;
109         SdCardInitResponse retval = SD_CARD_INIT_SUCCESS;
110
111         /* Mount the FAT filesystem. */
112         fat_op_status = f_mount (DISK_DRIVE_NUMBER, &fat_fs);
113
114         switch (fat_op_status) {
115                 case FR_OK:
116                         /* Success! */
117                         break;
118                 case FR_DISK_ERR:
119                 case FR_INT_ERR:
120                 case FR_NOT_READY:
121                         /* Low-level I/O error. */
122                         retval = SD_CARD_INIT_IO_ERROR;
123                         goto done;
124                 case FR_WRITE_PROTECTED:
125                         /* SD card is write protected. */
126                         retval = SD_CARD_INIT_CARD_WRITE_PROTECTED;
127                         goto done;
128                 case FR_INVALID_DRIVE:
129                         /* Logical drive number is invalid. */
130                         retval = SD_CARD_INIT_MISC_ERROR;
131                         goto done;
132                 case FR_NOT_ENABLED:
133                 case FR_NO_FILESYSTEM:
134                 case FR_NO_FILE:
135                 case FR_NO_PATH:
136                 case FR_INVALID_NAME:
137                 case FR_DENIED:
138                 case FR_EXIST:
139                 case FR_INVALID_OBJECT:
140                 case FR_MKFS_ABORTED:
141                 case FR_TIMEOUT:
142                 case FR_LOCKED:
143                 case FR_NOT_ENOUGH_CORE:
144                 case FR_TOO_MANY_OPEN_FILES:
145                 case FR_INVALID_PARAMETER:
146                 default:
147                         /* These shouldn't ever be encountered when calling f_mount(). */
148                         retval = SD_CARD_INIT_MISC_ERROR;
149                         goto done;
150         }
151
152 done:
153         /* Success! */
154         return retval;
155 }
156
157 static SdCardCommonResponse _sd_card_convert_f_open_response (FRESULT f_open_response)
158 {
159         switch (f_open_response) {
160                 case FR_OK:
161                         /* Success! */
162                         return SD_CARD_RESPONSE_SUCCESS;
163                 case FR_DISK_ERR:
164                 case FR_INT_ERR:
165                 case FR_NOT_READY:
166                         /* Low-level I/O error. */
167                         return SD_CARD_RESPONSE_IO_ERROR;
168                 case FR_INVALID_DRIVE:
169                         /* Logical drive number is invalid. */
170                         return SD_CARD_RESPONSE_MISC_ERROR;
171                 case FR_NOT_ENABLED:
172                 case FR_NO_FILESYSTEM:
173                         /* No filesystem exists. Bail. */
174                         return SD_CARD_RESPONSE_NOT_EXIST;
175                 case FR_NO_FILE:
176                 case FR_NO_PATH:
177                 case FR_INVALID_NAME:
178                 case FR_EXIST:
179                 case FR_INVALID_OBJECT:
180                         /* File doesn't exist on the filesystem. */
181                         return SD_CARD_RESPONSE_NOT_EXIST;
182                 case FR_DENIED:
183                         /* Access denied. */
184                         return SD_CARD_RESPONSE_INVALID_PERMISSIONS;
185                 case FR_WRITE_PROTECTED:
186                 case FR_MKFS_ABORTED:
187                 case FR_TIMEOUT:
188                 case FR_LOCKED:
189                 case FR_NOT_ENOUGH_CORE:
190                 case FR_TOO_MANY_OPEN_FILES:
191                 case FR_INVALID_PARAMETER:
192                 default:
193                         /* These shouldn't ever be encountered when calling f_open(). */
194                         return SD_CARD_RESPONSE_MISC_ERROR;
195         }
196 }
197
198 static SdCardCommonResponse _sd_card_convert_f_read_response (FRESULT f_read_response)
199 {
200         switch (f_read_response) {
201                 case FR_OK:
202                         return SD_CARD_RESPONSE_SUCCESS;
203                 case FR_DISK_ERR:
204                 case FR_INT_ERR:
205                 case FR_NOT_READY:
206                         /* Low-level I/O error. */
207                         return SD_CARD_RESPONSE_IO_ERROR;
208                 case FR_INVALID_DRIVE:
209                         /* Logical drive number is invalid. */
210                         return SD_CARD_RESPONSE_MISC_ERROR;
211                 case FR_NOT_ENABLED:
212                 case FR_NO_FILESYSTEM:
213                         /* No filesystem exists. Bail. */
214                         return SD_CARD_RESPONSE_NOT_EXIST;
215                 case FR_NO_FILE:
216                 case FR_NO_PATH:
217                 case FR_INVALID_NAME:
218                 case FR_EXIST:
219                 case FR_INVALID_OBJECT:
220                         /* File doesn't exist on the filesystem. */
221                         return SD_CARD_RESPONSE_NOT_EXIST;
222                 case FR_DENIED:
223                         /* Access denied. */
224                         return SD_CARD_RESPONSE_INVALID_PERMISSIONS;
225                 case FR_WRITE_PROTECTED:
226                 case FR_MKFS_ABORTED:
227                 case FR_TIMEOUT:
228                 case FR_LOCKED:
229                 case FR_NOT_ENOUGH_CORE:
230                 case FR_TOO_MANY_OPEN_FILES:
231                 case FR_INVALID_PARAMETER:
232                 default:
233                         /* These shouldn't ever be encountered when calling f_read(). */
234                         return SD_CARD_RESPONSE_MISC_ERROR;
235         }
236 }
237
238 static SdCardCommonResponse _sd_card_convert_f_write_response (FRESULT f_write_response)
239 {
240         switch (f_write_response) {
241                 case FR_OK:
242                         return SD_CARD_RESPONSE_SUCCESS;
243                 case FR_DISK_ERR:
244                 case FR_INT_ERR:
245                 case FR_NOT_READY:
246                         /* Low-level I/O error. */
247                         return SD_CARD_RESPONSE_IO_ERROR;
248                 case FR_INVALID_DRIVE:
249                         /* Logical drive number is invalid. */
250                         return SD_CARD_RESPONSE_MISC_ERROR;
251                 case FR_NOT_ENABLED:
252                 case FR_NO_FILESYSTEM:
253                         /* No filesystem exists. Bail. */
254                         return SD_CARD_RESPONSE_MISC_ERROR;
255                 case FR_NO_FILE:
256                 case FR_NO_PATH:
257                 case FR_INVALID_NAME:
258                 case FR_EXIST:
259                 case FR_INVALID_OBJECT:
260                         /* File doesn't exist on the filesystem. */
261                         return SD_CARD_RESPONSE_NOT_EXIST;
262                 case FR_DENIED:
263                         /* Access denied. */
264                         return SD_CARD_RESPONSE_INVALID_PERMISSIONS;
265                 case FR_WRITE_PROTECTED:
266                 case FR_MKFS_ABORTED:
267                 case FR_TIMEOUT:
268                 case FR_LOCKED:
269                 case FR_NOT_ENOUGH_CORE:
270                 case FR_TOO_MANY_OPEN_FILES:
271                 case FR_INVALID_PARAMETER:
272                 default:
273                         /* These shouldn't ever be encountered when calling f_write(). */
274                         return SD_CARD_RESPONSE_MISC_ERROR;
275         }
276 }
277
278 /* Calculate a checksum for the brew config. The Dallas/Maxim CRC-8 from avr-libc is used.
279  * It uses polynomial: x^8 + x^5 + x^4 + 1 with initial value 0x00.
280  * See: http://www.nongnu.org/avr-libc/user-manual/group__util__crc.html#ga37b2f691ebbd917e36e40b096f78d996 */
281 static BrewConfigChecksum _brew_config_calculate_checksum (const BrewConfig *brew_config, uint8_t version) NONNULL (1);
282
283 static BrewConfigChecksum _brew_config_calculate_checksum (const BrewConfig *brew_config, uint8_t version)
284 {
285         uint8_t crc = 0, i;
286
287         crc = _crc_ibutton_update (crc, version);
288         for (i = 0; i < sizeof (BrewConfig); i++) {
289                 crc = _crc_ibutton_update (crc, ((const uint8_t *) brew_config)[i]);
290         }
291
292         return crc;
293 }
294
295 SdCardLoadFileResponse sd_card_load_brew_config (BrewConfig *brew_config)
296 {
297         /* TODO: preconditions and postconditions. */
298
299         FRESULT fat_op_status;
300         FIL brew_config_file;
301         UINT bytes_read;
302         SdCardBrewConfigHeader header;
303         SdCardLoadFileResponse retval = SD_CARD_LOAD_FILE_SUCCESS;
304
305         /* Open the config file. */
306         fat_op_status = f_open (&brew_config_file, BREW_CONFIG_FILE_NAME, FA_READ);
307         retval = _sd_card_convert_f_open_response (fat_op_status);
308
309         if (retval != SD_CARD_LOAD_FILE_SUCCESS) {
310                 /* Failure. */
311                 goto done;
312         }
313
314         /* Read the file contents. Read the header first so we know how big the config file should be. */
315         bytes_read = 0;
316         fat_op_status = f_read (&brew_config_file, &header, sizeof (SdCardBrewConfigHeader), &bytes_read);
317         retval = _sd_card_convert_f_read_response (fat_op_status);
318
319         if (retval == SD_CARD_LOAD_FILE_SUCCESS) {
320                 /* Success! Check the number of bytes read. */
321                 if (bytes_read != sizeof (SdCardBrewConfigHeader)) {
322                         /* Somehow not even the version and checksum could be read. Bail. */
323                         retval = SD_CARD_LOAD_FILE_IO_ERROR;
324                         goto close_file;
325                 }
326
327                 /* Header was read successfully. Do we support this version? */
328                 if (header.version != BREW_CONFIG_VERSION) {
329                         retval = SD_CARD_LOAD_FILE_UNSUPPORTED_VERSION;
330                         goto close_file;
331                 }
332         } else {
333                 /* Failure. */
334                 goto close_file;
335         }
336
337         /* Read the rest of the file contents into the config data structure. */
338         bytes_read = 0;
339         fat_op_status = f_read (&brew_config_file, brew_config, sizeof (BrewConfig), &bytes_read);
340         retval = _sd_card_convert_f_read_response (fat_op_status);
341
342         if (retval == SD_CARD_LOAD_FILE_SUCCESS) {
343                 /* Success! Check the number of bytes read. */
344                 if (bytes_read != sizeof (BrewConfig)) {
345                         /* The data structure was only partially filled. The config file on the SD card must be corrupt or in the wrong
346                          * format. */
347                         retval = SD_CARD_LOAD_FILE_IO_ERROR;
348                         goto close_file;
349                 }
350
351                 /* Otherwise: file was read successfully and in full. */
352         } else {
353                 /* Failure. */
354                 goto close_file;
355         }
356
357         /* Check the file's checksum. */
358         if (header.checksum != _brew_config_calculate_checksum (brew_config, header.version)) {
359                 retval = SD_CARD_LOAD_FILE_CHECKSUM_FAILURE;
360                 goto close_file;
361         }
362
363 close_file:
364         /* Close the file. Ignore any errors. */
365         f_close (&brew_config_file);
366
367 done:
368         return retval;
369 }
370
371 SdCardWriteFileResponse sd_card_create_brew_config (const BrewConfig *brew_config)
372 {
373         SdCardCommonResponse _retval;
374         SdCardWriteFileResponse retval;
375         FRESULT fat_op_status;
376         FIL brew_config_file;
377         UINT bytes_written = 0;
378         SdCardBrewConfigHeader header;
379
380         /* Create the file header. */
381         header.version = BREW_CONFIG_VERSION;
382         header.checksum = _brew_config_calculate_checksum (brew_config, BREW_CONFIG_VERSION);
383
384         /* Try opening the file for writing. */
385         fat_op_status = f_open (&brew_config_file, BREW_CONFIG_FILE_NAME, FA_WRITE | FA_OPEN_ALWAYS);
386
387         if (fat_op_status == FR_NO_FILESYSTEM) {
388                 /* Create a file system and try again. Ignore failures in creating the file system; we'll catch then when trying to open the file.*/
389                 #define PARTITION_TABLE_FDISK 0 /* fdisk-style partition table */
390                 #define PARTITION_TABLE_SFD 1 /* no partition table */
391                 #define ALLOCATION_UNIT_AUTO 0 /* automatically select FAT allocation unit size (bytes) */
392
393                 f_mkfs (DISK_DRIVE_NUMBER, PARTITION_TABLE_SFD, ALLOCATION_UNIT_AUTO);
394                 fat_op_status = f_open (&brew_config_file, BREW_CONFIG_FILE_NAME, FA_WRITE | FA_CREATE_ALWAYS);
395         }
396
397         _retval = _sd_card_convert_f_open_response (fat_op_status);
398         if (_retval == SD_CARD_RESPONSE_NOT_EXIST) {
399                 /* There is no equivalent to SD_CARD_RESPONSE_NOT_EXIST in SdCardWriteFileResponse, so filter that out. */
400                 retval = SD_CARD_RESPONSE_MISC_ERROR;
401         } else {
402                 retval = (SdCardWriteFileResponse) _retval;
403         }
404
405         if (retval != SD_CARD_WRITE_FILE_SUCCESS) {
406                 /* Failure. */
407                 goto done;
408         }
409
410         /* Write out the header. */
411         bytes_written = 0;
412         fat_op_status = f_write (&brew_config_file, &header, sizeof (SdCardBrewConfigHeader), &bytes_written);
413         _retval = _sd_card_convert_f_write_response (fat_op_status);
414         if (_retval == SD_CARD_RESPONSE_NOT_EXIST) {
415                 /* There is no equivalent to SD_CARD_RESPONSE_NOT_EXIST in SdCardWriteFileResponse, so filter that out. */
416                 retval = SD_CARD_RESPONSE_MISC_ERROR;
417         } else {
418                 retval = (SdCardWriteFileResponse) _retval;
419         }
420
421         if (retval != SD_CARD_WRITE_FILE_SUCCESS || bytes_written != sizeof (SdCardBrewConfigHeader)) {
422                 /* Failure. Ensure retval has a non-success value if we failed due to writing too little out. */
423                 if (retval == SD_CARD_WRITE_FILE_SUCCESS && bytes_written != sizeof (SdCardBrewConfigHeader)) {
424                         retval = SD_CARD_WRITE_FILE_IO_ERROR;
425                 }
426
427                 goto close_file;
428         }
429
430         /* Write out the config proper. */
431         bytes_written = 0;
432         fat_op_status = f_write (&brew_config_file, brew_config, sizeof (BrewConfig), &bytes_written);
433         _retval = _sd_card_convert_f_write_response (fat_op_status);
434         if (_retval == SD_CARD_RESPONSE_NOT_EXIST) {
435                 /* There is no equivalent to SD_CARD_RESPONSE_NOT_EXIST in SdCardWriteFileResponse, so filter that out. */
436                 retval = SD_CARD_RESPONSE_MISC_ERROR;
437         } else {
438                 retval = (SdCardWriteFileResponse) _retval;
439         }
440
441         if (retval != SD_CARD_WRITE_FILE_SUCCESS || bytes_written != sizeof (BrewConfig)) {
442                 /* Failure. Ensure retval has a non-success value if we failed due to writing too little out. */
443                 if (retval == SD_CARD_WRITE_FILE_SUCCESS && bytes_written != sizeof (BrewConfig)) {
444                         retval = SD_CARD_WRITE_FILE_IO_ERROR;
445                 }
446
447                 goto close_file;
448         }
449
450 close_file:
451         f_close (&brew_config_file);
452
453 done:
454         return retval;
455 }
456
457 static SdCardCommonResponse _sd_card_convert_f_lseek_response (FRESULT f_lseek_response)
458 {
459         switch (f_lseek_response) {
460                 case FR_OK:
461                         return SD_CARD_RESPONSE_SUCCESS;
462                 case FR_DISK_ERR:
463                 case FR_INT_ERR:
464                 case FR_NOT_READY:
465                         /* Low-level I/O error. */
466                         return SD_CARD_RESPONSE_IO_ERROR;
467                 case FR_INVALID_DRIVE:
468                         /* Logical drive number is invalid. */
469                         return SD_CARD_RESPONSE_MISC_ERROR;
470                 case FR_NOT_ENABLED:
471                 case FR_NO_FILESYSTEM:
472                         /* No filesystem exists. Bail. */
473                         return SD_CARD_RESPONSE_MISC_ERROR;
474                 case FR_NO_FILE:
475                 case FR_NO_PATH:
476                 case FR_INVALID_NAME:
477                 case FR_EXIST:
478                 case FR_INVALID_OBJECT:
479                         /* File doesn't exist on the filesystem. */
480                         return SD_CARD_RESPONSE_NOT_EXIST;
481                 case FR_DENIED:
482                         /* Access denied. */
483                         return SD_CARD_RESPONSE_INVALID_PERMISSIONS;
484                 case FR_WRITE_PROTECTED:
485                 case FR_MKFS_ABORTED:
486                 case FR_TIMEOUT:
487                 case FR_LOCKED:
488                 case FR_NOT_ENOUGH_CORE:
489                 case FR_TOO_MANY_OPEN_FILES:
490                 case FR_INVALID_PARAMETER:
491                 default:
492                         /* These shouldn't ever be encountered when calling f_lseek(). */
493                         return SD_CARD_RESPONSE_MISC_ERROR;
494         }
495 }
496
497 /* Calculate a checksum for the brew config. The Dallas/Maxim CRC-8 from avr-libc is used.
498  * It uses polynomial: x^8 + x^5 + x^4 + 1 with initial value 0x00.
499  * See: http://www.nongnu.org/avr-libc/user-manual/group__util__crc.html#ga37b2f691ebbd917e36e40b096f78d996 */
500 static BrewResultsChecksum _brew_results_calculate_checksum (const LogEntry *log_entry) NONNULL (1);
501
502 static BrewConfigChecksum _brew_results_calculate_checksum (const LogEntry *log_entry)
503 {
504         uint8_t crc = 0, i;
505
506         for (i = 0; i < sizeof (LogEntry); i++) {
507                 crc = _crc_ibutton_update (crc, ((const uint8_t *) log_entry)[i]);
508         }
509
510         return crc;
511 }
512
513 /**
514  * \brief Append log entries to results file.
515  *
516  * Append the given log entries to the results file on the SD card. If power is lost part-way through a call to this function, the results file is
517  * left in an indeterminate state (between 0 and \a num_entries entries could have been partially or wholly appended). However, once this function
518  * returns, the entries are guaranteed to have been entirely written to the SD card and any buffers flushed.
519  */
520 SdCardWriteFileResponse sd_card_append_log_entries (LogEntry *entries, unsigned int num_entries, unsigned int *num_entries_appended)
521 {
522         FRESULT fat_op_status;
523         FIL brew_results_file;
524         SdCardCommonResponse _retval;
525         SdCardWriteFileResponse retval = SD_CARD_WRITE_FILE_SUCCESS;
526         SdCardBrewResultsHeader header;
527         unsigned int i = 0;
528         UINT bytes_written = 0;
529
530         /* Set up the header for the brew results file. */
531         header.version = BREW_RESULTS_VERSION;
532         header.brew_id = global_brew_config.brew_id;
533
534         /* Open the results file. */
535         fat_op_status = f_open (&brew_results_file, BREW_RESULTS_FILE_NAME, FA_WRITE | FA_OPEN_ALWAYS);
536         _retval = _sd_card_convert_f_open_response (fat_op_status);
537         if (_retval == SD_CARD_RESPONSE_NOT_EXIST) {
538                 /* There is no equivalent to SD_CARD_RESPONSE_NOT_EXIST in SdCardWriteFileResponse, so filter that out. */
539                 retval = SD_CARD_RESPONSE_MISC_ERROR;
540         } else {
541                 retval = (SdCardWriteFileResponse) _retval;
542         }
543
544         if (retval != SD_CARD_WRITE_FILE_SUCCESS) {
545                 /* Failure. */
546                 goto done;
547         }
548
549         /* Seek to the end of the file. */
550         fat_op_status = f_lseek (&brew_results_file, brew_results_file.fsize);
551         _retval = _sd_card_convert_f_lseek_response (fat_op_status);
552         if (_retval == SD_CARD_RESPONSE_NOT_EXIST) {
553                 /* There is no equivalent to SD_CARD_RESPONSE_NOT_EXIST in SdCardWriteFileResponse, so filter that out. */
554                 retval = SD_CARD_RESPONSE_MISC_ERROR;
555         } else {
556                 retval = (SdCardWriteFileResponse) _retval;
557         }
558
559         if (retval != SD_CARD_WRITE_FILE_SUCCESS) {
560                 /* Failure. */
561                 goto close_file;
562         }
563
564         /* If the file is zero-sized (i.e. we've just created it), write out a header containing the brew ID. */
565         if (brew_results_file.fsize == 0) {
566                 bytes_written = 0;
567                 fat_op_status = f_write (&brew_results_file, &header, sizeof (SdCardBrewResultsHeader), &bytes_written);
568                 _retval = _sd_card_convert_f_write_response (fat_op_status);
569                 if (_retval == SD_CARD_RESPONSE_NOT_EXIST) {
570                         /* There is no equivalent to SD_CARD_RESPONSE_NOT_EXIST in SdCardWriteFileResponse, so filter that out. */
571                         retval = SD_CARD_RESPONSE_MISC_ERROR;
572                 } else {
573                         retval = (SdCardWriteFileResponse) _retval;
574                 }
575
576                 if (retval != SD_CARD_WRITE_FILE_SUCCESS || bytes_written != sizeof (SdCardBrewResultsHeader)) {
577                         /* Failure. Ensure retval has a non-success value if we failed due to writing too little out. */
578                         if (retval == SD_CARD_WRITE_FILE_SUCCESS && bytes_written != sizeof (SdCardBrewResultsHeader)) {
579                                 retval = SD_CARD_WRITE_FILE_IO_ERROR;
580                         }
581
582                         goto close_file;
583                 }
584         }
585
586         /* Append each entry to the results file. */
587         for (i = 0; i < num_entries; i++) {
588                 union {
589                         struct {
590                                 LogEntry log_entry;
591                                 BrewResultsChecksum checksum;
592                         };
593                         uint8_t data[sizeof (LogEntry) + sizeof (BrewResultsChecksum)];
594                 } output;
595
596                 /* Prepare the entry. */
597                 memcpy (&(output.log_entry), &(entries[i]), sizeof (LogEntry));
598                 output.checksum = _brew_results_calculate_checksum (&(output.log_entry));
599
600                 /* Write the entry and checksum out. */
601                 fat_op_status = f_write (&brew_results_file, output.data, sizeof (output.data), &bytes_written);
602                 _retval = _sd_card_convert_f_write_response (fat_op_status);
603                 if (_retval == SD_CARD_RESPONSE_NOT_EXIST) {
604                         /* There is no equivalent to SD_CARD_RESPONSE_NOT_EXIST in SdCardWriteFileResponse, so filter that out. */
605                         retval = SD_CARD_RESPONSE_MISC_ERROR;
606                 } else {
607                         retval = (SdCardWriteFileResponse) _retval;
608                 }
609
610                 if (retval != SD_CARD_WRITE_FILE_SUCCESS || bytes_written != sizeof (output.data)) {
611                         /* Failure. Ensure retval has a non-success value if we failed due to writing too little out. */
612                         if (retval == SD_CARD_WRITE_FILE_SUCCESS && bytes_written != sizeof (output.data)) {
613                                 retval = SD_CARD_WRITE_FILE_IO_ERROR;
614                         }
615
616                         goto close_file;
617                 }
618         }
619
620 close_file:
621         f_close (&brew_results_file);
622
623 done:
624         *num_entries_appended = i;
625
626         return retval;
627 }