Files
g2/g2core/persistence.cpp
2020-01-10 19:25:10 -06:00

377 lines
14 KiB
C++

/*
* persistence.cpp - persistence functions
* This file is part of the g2core project
*
* Copyright (c) 2013 - 2018 Alden S. Hart Jr.
*
* This file ("the software") is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2 as published by the
* Free Software Foundation. You should have received a copy of the GNU General Public
* License, version 2 along with the software. If not, see <http://www.gnu.org/licenses/>.
*
* As a special exception, you may use this file as part of a software library without
* restriction. Specifically, if other files instantiate templates or use macros or
* inline functions from this file, or you compile this file and link it with other
* files to produce an executable, this file does not by itself cause the resulting
* executable to be covered by the GNU General Public License. This exception does not
* however invalidate any other reasons why the executable file might be covered by the
* GNU General Public License.
*
* THE SOFTWARE IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, BUT WITHOUT ANY
* WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
* SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "g2core.h"
#include "persistence.h"
#include "canonical_machine.h"
#include "report.h"
#include "util.h"
#include "board_spi.h"
#include "ff.h"
/***********************************************************************************
**** STRUCTURE ALLOCATIONS ********************************************************
***********************************************************************************/
//**** persistence singleton ****
struct nvmSingleton_t {
float tmp_value;
FATFS fat_fs;
FIL file;
uint8_t file_index;
alignas(4) uint8_t io_buffer[IO_BUFFER_SIZE];
uint16_t changed_nvs;
uint32_t last_write_systick;
uint8_t write_failures;
} nvm;
/***********************************************************************************
**** GENERIC STATIC FUNCTIONS AND VARIABLES ***************************************
***********************************************************************************/
stat_t prepare_persistence_file();
stat_t write_persistent_values();
stat_t validate_persistence_file();
uint8_t active_file_index();
// Leaving this in for now in case bugs come up; we can remove it when we're confident
// it's stable
#if 0
# define DEBUG_PRINT(...) printf(__VA_ARGS__);
#else
# define DEBUG_PRINT(...) do {} while (0)
#endif
// Analogous to ritorno(a), but for the FatFS return codes and with optional debug output
#define fs_ritorno(a, msg) if((status_code=a) != FR_OK) \
{ DEBUG_PRINT("%s res: %i\n", msg, status_code); return(STAT_PERSISTENCE_ERROR); }
/*
We cycle between three different files, indexed by a suffix. Each time we need to write
new values, we copy data from the current file to a new file with NEXT_FILE_INDEX, then
delete the current file once the write is complete. This ensures that at least one recent
copy of the file will be preserved if power is lost in the middle of a write.
*/
#define PERSISTENCE_DIR "persist"
#define PERSISTENCE_FILENAME(num) PERSISTENCE_DIR"/persist"#num".bin"
#define PERSISTENCE_FILENAME_CNT 3
#define NEXT_FILE_INDEX (nvm.file_index+1) % PERSISTENCE_FILENAME_CNT
#define PREV_FILE_INDEX (nvm.file_index+PERSISTENCE_FILENAME_CNT-1) % PERSISTENCE_FILENAME_CNT
const char* filenames[PERSISTENCE_FILENAME_CNT] = {
PERSISTENCE_FILENAME(0), PERSISTENCE_FILENAME(1), PERSISTENCE_FILENAME(2) };
#define CRC_LEN 4
/***********************************************************************************
**** CODE *************************************************************************
***********************************************************************************/
void persistence_init()
{
nvm.file_index = 0;
nvm.last_write_systick = Motate::SysTickTimer.getValue();
nvm.write_failures = 0;
nvm.changed_nvs = 0;
return;
}
/*
* read_persistent_value() - return value (as float) by index
*
* It's the responsibility of the caller to make sure the index does not exceed range
*/
stat_t read_persistent_value(nvObj_t *nv)
{
ritorno(prepare_persistence_file());
DEBUG_PRINT("file opened for reading\n");
fs_ritorno(f_lseek(&nvm.file, nv->index * NVM_VALUE_LEN), "f_lseek during read");
UINT br;
fs_ritorno(f_read(&nvm.file, &nvm.io_buffer, NVM_VALUE_LEN, &br), "read value");
if (br != NVM_VALUE_LEN) {
return (STAT_PERSISTENCE_ERROR);
}
if (cfgArray[nv->index].flags & TYPE_INTEGER) {
nv->valuetype = TYPE_INTEGER;
nv->value_int = *(int32_t *)nvm.io_buffer;
DEBUG_PRINT("value (i) copied from address %l in file: %l\n", nv->index * NVM_VALUE_LEN, nv->value_int);
} else if (cfgArray[nv->index].flags & TYPE_BOOLEAN) {
nv->valuetype = TYPE_BOOLEAN;
nv->value_int = *(int32_t *)nvm.io_buffer;
DEBUG_PRINT("value (b) copied from address %l in file: %l\n", nv->index * NVM_VALUE_LEN, nv->value_int);
} else {
nv->valuetype = TYPE_FLOAT;
nv->value_flt = *(float *)nvm.io_buffer;
DEBUG_PRINT("value (f) copied from address %l in file: %f\n", nv->index * NVM_VALUE_LEN, nv->value_flt);
}
return (STAT_OK);
}
/*
* write_persistent_value() - write to NVM by index, but only if the value has changed
*
* It's the responsibility of the caller to make sure the index does not exceed range
* Note: Removed NAN and INF checks on floats - not needed
*/
stat_t write_persistent_value(nvObj_t *nv)
{
nvm.changed_nvs++;
return (STAT_OK);
}
/*
* write_persistent_values_callback()
*
* On ARM, write cached values to a file. No-op on AVR.
*/
stat_t write_persistent_values_callback()
{
// Check the disk status to ensure we catch card-detect pin changes.
// FIXME: it would be much better to do this with an interrupt!
f_polldisk();
if (nvm.changed_nvs > 0) {
if (Motate::SysTickTimer.getValue() - nvm.last_write_systick < MIN_WRITE_INTERVAL) {
return (STAT_NOOP);
}
// this check may not be necessary on ARM, but just in case...
if (cm->cycle_type != CYCLE_NONE) {
return(STAT_NOOP); // can't write when machine is moving
}
if(write_persistent_values() == STAT_OK) {
nvm.changed_nvs = 0;
nvm.write_failures = 0;
} else {
// if the write failed, make sure no half-written output file exists
f_unlink(filenames[NEXT_FILE_INDEX]);
if (++nvm.write_failures >= MAX_WRITE_FAILURES) {
nvm.changed_nvs = 0; // give up on these values
nvm.write_failures = 0; // but try again if we get more values later
return(rpt_exception(STAT_PERSISTENCE_ERROR, NULL));
}
}
nvm.last_write_systick = Motate::SysTickTimer.getValue();
return STAT_OK;
}
return STAT_NOOP;
}
/*
* active_file_index()
*
* Determines which of the existing files is most current and should be used for
* value reads. If no files exist, return 0. This assumes that no more than two
* files exist at any one time, which should always be the case under our updating
* scheme (described in a comment near the PERSISTENCE_FILENAME define).
*/
uint8_t active_file_index()
{
uint8_t i = 0;
for (; i < PERSISTENCE_FILENAME_CNT; ++i) {
if (f_stat(filenames[i], nullptr) == FR_OK) {
uint8_t next = (i+1)%PERSISTENCE_FILENAME_CNT;
if (next > i && f_stat(filenames[next], nullptr) == FR_OK) {
i = next;
}
break;
}
}
return i;
}
/*
* prepare_persistence_file()
*
* ARM only. Ensures that the file is open and has a valid CRC. This should be called
* prior to using the file in any other function.
*/
stat_t prepare_persistence_file()
{
// if the file is already open and valid, no further prep is necessary.
// NOTE: we don't close the file after every use because the higher latency
// would slow down consecutive reads. However, we still need to re-validate before
// every use to ensure that the card status hasn't changed.
if (f_is_open(&nvm.file) && validate(&nvm.file) == FR_OK) return STAT_OK;
// mount volume if necessary
if (!nvm.fat_fs.fs_type) {
fs_ritorno(f_mount(&nvm.fat_fs, "", 1), "mount"); /* Give a work area to the default drive */
}
f_mkdir(PERSISTENCE_DIR);
uint8_t index = active_file_index();
fs_ritorno(f_open(&nvm.file, filenames[index], FA_READ | FA_OPEN_EXISTING), "open input");
nvm.file_index = index;
// if CRC doesn't match, delete file and return error
if (validate_persistence_file() != STAT_OK) {
f_close(&nvm.file);
f_unlink(filenames[nvm.file_index]);
nvm.file_index = 0;
return STAT_PERSISTENCE_ERROR;
}
// OK to delete old file now (if it still exists), since we know the current one is good
f_unlink(filenames[PREV_FILE_INDEX]);
return STAT_OK;
}
/*
* validate_persistence_file()
*
* ARM only. Helper function that checks the CRC and byte count of the
* persistence file. Assumes the file is already open.
*/
stat_t validate_persistence_file()
{
uint32_t crc = 0;
uint32_t filecrc = -1;
UINT br, br_sum = 0;
fs_ritorno(f_lseek(&nvm.file, 0), "crc check seek");
while (!f_eof(&nvm.file)) {
fs_ritorno(f_read(&nvm.file, &nvm.io_buffer, IO_BUFFER_SIZE, &br), "file read during CRC check");
if (f_eof(&nvm.file)) {
br -= std::min((UINT)CRC_LEN, br); // don't include old CRC in current CRC calculation
memcpy(&filecrc, nvm.io_buffer+br, CRC_LEN); // copy old CRC out of read buffer
}
// update calculated CRC
crc = crc32(crc, nvm.io_buffer, br);
br_sum += br;
}
// how did we do?
if (br_sum != nv_index_max() * NVM_VALUE_LEN) {
DEBUG_PRINT("bad byte count in file: %i\n", br_sum);
return STAT_PERSISTENCE_ERROR;
}
DEBUG_PRINT("crc: %lu from file, %lu calculated\n", filecrc, crc);
return crc == filecrc ? STAT_OK : STAT_PERSISTENCE_ERROR;
}
/*
* write_persistent_values()
*
* ARM only. Writes all the values from the write cache to the SD card. Since we can't
* rewrite individual pieces of data in the middle of an existing file, this requires
* rewriting all the data into a new file.
*/
stat_t write_persistent_values()
{
DEBUG_PRINT("writing new version\n");
FIL f_out;
UINT bw;
nvObj_t *nv = nv_reset_nv_list(); // sets *nv to the start of the body
cmDistanceMode saved_distance_mode;
// Save the current units mode
saved_distance_mode = (cmDistanceMode)cm_get_distance_mode(ACTIVE_MODEL);
// attempt to open file with previously persisted values
if (prepare_persistence_file() == STAT_OK) {
fs_ritorno(f_lseek(&nvm.file, 0), "f_lseek to input file start");
}
// open new file for writing updated values
fs_ritorno(f_open(&f_out, filenames[NEXT_FILE_INDEX], FA_WRITE | FA_OPEN_ALWAYS), "open output");
fs_ritorno(f_sync(&f_out), "sync output file");
DEBUG_PRINT("opened %s for writing\n", filenames[NEXT_FILE_INDEX]);
uint32_t crc = 0;
uint16_t step = IO_BUFFER_SIZE/NVM_VALUE_LEN;
for (index_t cnt = 0; cnt < nv_index_max(); cnt += step) {
// try to read old values from existing file
uint16_t io_byte_count = std::min((float)IO_BUFFER_SIZE, (float)((nv_index_max()-cnt) * NVM_VALUE_LEN));
UINT br = 0;
f_read(&nvm.file, &nvm.io_buffer, io_byte_count, &br);
DEBUG_PRINT("read %i bytes from old file\n", br);
// if we didn't get enough bytes from the old file, pad the buffer with defaults
// to keep the length correct
// FIXME: integrate this with default-setting code in config.cpp
for (; br<io_byte_count; br += NVM_VALUE_LEN) {
index_t index = cnt + br/NVM_VALUE_LEN;
memcpy(nvm.io_buffer+br, &cfgArray[index].def_value, NVM_VALUE_LEN);
}
DEBUG_PRINT("io_buffer populated with %i bytes total\n", br);
// update the values in the buffer from the changes indexes in the current range
for (nv->index = cnt; nv->index < (cnt + step); nv->index++) {
// Found an entry that needs to be written
if (nv->index == 0 || cfgArray[nv->index].flags & F_PERSIST) {
nv_get_nvObj(nv);
// Get the index for the current NVM value
index_t index = (nv->index - cnt) * NVM_VALUE_LEN;
// Write out based on the value type
if (nv->valuetype == TYPE_INTEGER || nv->valuetype == TYPE_BOOLEAN) {
memcpy(nvm.io_buffer + index, &nv->value_int, NVM_VALUE_LEN);
DEBUG_PRINT("item index: %l , write index: %l (cnt: %l), value: %i\n", nv->index, index, cnt, nv->value_int);
} else if (nv->valuetype == TYPE_FLOAT) {
memcpy(nvm.io_buffer + index, &nv->value_flt, NVM_VALUE_LEN);
DEBUG_PRINT("item index: %l , write index: %l (cnt: %l), value: %f\n", nv->index, index, cnt, nv->value_flt);
} else {
// next - ignore strings and other stuff which shouldn't be set to persist anyway
}
}
}
// write updated values to output file and sync
fs_ritorno(f_write(&f_out, &nvm.io_buffer, io_byte_count, &bw), "new file write");
if (bw != io_byte_count) return (STAT_PERSISTENCE_ERROR);
fs_ritorno(f_sync(&f_out), "out sync");
// update CRC
crc = crc32(crc, nvm.io_buffer, io_byte_count);
}
// write CRC in final 4 bytes
fs_ritorno(f_write(&f_out, &crc, CRC_LEN, &bw), "write crc");
DEBUG_PRINT("wrote crc: %lu\n", crc);
// close both old and new files
fs_ritorno(f_close(&f_out), "close output");
if (f_is_open(&nvm.file)) {
fs_ritorno(f_close(&nvm.file), "close input");
// if we made it here, it's now safe to delete the older file
fs_ritorno(f_unlink(filenames[nvm.file_index]), "old file delete");
DEBUG_PRINT("deleted obsolete file %s\n", filenames[nvm.file_index]);
nvm.file_index = 0;
}
// Restore units mode
cm_set_distance_mode(saved_distance_mode);
return (STAT_OK);
}