Files
controller-wisetopdataserver/python/biopro/ext/export_csv.h
T
2021-12-20 14:52:55 +08:00

863 lines
28 KiB
C

#include "channel.h"
#include "export.h"
#ifndef _BPS_EXPORT_CSV_H_
#define _BPS_EXPORT_CSV_H_
typedef struct {
int temp_channel;
double temp_time_step;
unsigned int temp_line_number;
FILE * temp_file;
} ExportCsvTempFileEntry;
/**
* used in ExportCsvOptions.device_identify
*/
#define DEVICE_ID_NORMAL 0
#define DEVICE_ID_ELITE 1
/**
* export_csv options
*/
typedef struct {
// export function options, exported fields
unsigned int resample_rate;
unsigned int resample_method;
unsigned int limit_column_time;
unsigned int at_least_row_number;
IntSet * channel_mask;
unsigned char include_header; // boolean
unsigned char shift_channel; // boolean
// below are export function internal fields
// export function use
FILE * export_file;
double time_stamp;
double sample_rate;
int valid_time_stamp; // workaround for duplicate data
// export function cache
/**
* identify which device it is. it should be one of the macro DEVICE_ID_*
*/
unsigned char device_identify;
/**
* device ID number.
*/
unsigned int device;
unsigned int entry_size;
ReSampleEntry **entries;
// export temp file
unsigned int temp_file_size;
ExportCsvTempFileEntry *temp_file_entry;
} ExportCsvOptions;
static int ExportCsvOptions_init(ExportCsvOptions *options) {
options->export_file = NULL;
options->device_identify = DEVICE_ID_NORMAL;
options->device = 0;
options->time_stamp = 0;
options->entry_size = 0;
options->entries = NULL;
options->temp_file_size = 0;
options->temp_file_entry = NULL;
// set mode
switch (options->resample_method) {
case SAMPLE_MODE_RAW:
options->resample_method = SAMPLE_MODE_RAW;
break;
case SAMPLE_MODE_FIRST:
default:
options->resample_method = SAMPLE_MODE_FIRST;
break;
case SAMPLE_MODE_AVG:
options->resample_method = SAMPLE_MODE_AVG;
break;
}
// resample entry
if (options->resample_method > SAMPLE_MODE_RAW) {
// check resample rate
if (options->resample_rate == 0) {
PyErr_SetString(PyExc_ValueError, "zero sample rate");
return 1;
}
options->entry_size = 16;
options->entries = (ReSampleEntry **)PyMem_Malloc(options->entry_size * sizeof(ReSampleEntry *));
if (options->entries == NULL) {
options->entry_size = 0;
PyErr_SetString(PyExc_RuntimeError, "PyMem_Malloc ReSampleEntry[] fail");
return 1;
}
for (unsigned int i = 0; i < options->entry_size; i++) {
options->entries[i] = NULL;
}
}
return 0;
}
static void ExportCsvOptions_free(ExportCsvOptions *options) {
if (options->entries != NULL) {
for (unsigned int i = 0; i < options->entry_size; i++) {
ReSampleEntry *entry = options->entries[i];
if (entry != NULL) {
PyMem_Free(entry);
options->entries[i] = NULL;
}
}
PyMem_Free(options->entries);
options->entries = NULL;
}
if (options->temp_file_size > 0) {
for (unsigned int i = 0; i < options->temp_file_size; i++) {
ExportCsvTempFileEntry *entry = options->temp_file_entry + i;
FILE * temp = entry->temp_file;
if (temp != NULL) {
fclose(temp);
}
entry->temp_file = NULL;
}
PyMem_Free(options->temp_file_entry);
options->temp_file_entry = NULL;
options->temp_file_size = 0;
}
}
static int ExportCsvOptions_extend(ExportCsvOptions *options, unsigned int at_least_size) {
unsigned int new_size = options->entry_size;
while (new_size < at_least_size) {
new_size += 4;
}
ReSampleEntry **new_entries = (ReSampleEntry **)PyMem_Realloc(options->entries, new_size * sizeof(ReSampleEntry *));
if (new_entries == NULL) {
PyErr_SetString(PyExc_RuntimeError, "PyMem_Malloc ReSampleEntry fail");
return 1;
}
options->entry_size = new_size;
options->entries = new_entries;
return 0;
}
/**
* @return: 1 on success, assign result to *index*.
*/
static int ExportCsvOptions_get_temp_index(ExportCsvOptions *options, unsigned int channel, int *index) {
if (options->temp_file_size == 0) return 0;
for (int i = options->temp_file_size - 1; i >= 0; i--) {
int temp_channel = (options->temp_file_entry + i)->temp_channel;
if (temp_channel >= 0 && (unsigned int)temp_channel == channel) {
*index = i;
return 1;
}
}
return 0;
}
/**
* @return: 1 on success, -1 on error
*/
static int ExportCsvOptions_get_empty_temp_index(ExportCsvOptions *options, unsigned int channel, int *index) {
unsigned int size = options->channel_mask->size;
int mod = int_set_indexof(options->channel_mask, channel);
if (mod < 0) {
PyErr_SetString(PyExc_RuntimeError, "channel not found");
return -1;
}
for (unsigned int i = mod; i < options->temp_file_size; i += size) {
int temp_channel = (options->temp_file_entry + i)->temp_channel;
if (temp_channel == -1) {
*index = i;
return 1;
}
}
return 0;
}
/**
* @return: 1 on success
*/
static int ExportCsvOptions_new_temp(ExportCsvOptions *options, unsigned int channel, int *index) {
int result = ExportCsvOptions_get_empty_temp_index(options, channel, index);
ExportCsvTempFileEntry *last_entry = NULL;
if (result == -1) {
// channel not found, with error
return 0;
} else if (result == 1) {
last_entry = options->temp_file_entry + *index;
} else {
// channel not found, extend array size
unsigned int size = options->channel_mask->size;
unsigned int new_size = options->temp_file_size + size;
ExportCsvTempFileEntry *new_entry = (ExportCsvTempFileEntry *)PyMem_Realloc(options->temp_file_entry, //
new_size * sizeof(ExportCsvTempFileEntry));
if (new_entry == NULL) {
ensure_error(PyErr_SetString(PyExc_RuntimeError, "ExportCsvOptions_new_temp PyMem_Realloc fail"));
return 0;
}
// init channel value
for (unsigned int i = options->temp_file_size; i < new_size; i++) {
ExportCsvTempFileEntry *tmp_entry = new_entry + i;
tmp_entry->temp_channel = -1;
tmp_entry->temp_time_step = 0;
tmp_entry->temp_line_number = 0;
tmp_entry->temp_file = NULL;
FILE *temp = tmpfile();
if (temp == NULL) {
ensure_error(PyErr_Format(PyExc_RuntimeError, "ExportCsvOptions_new_temp tmpfile() for column : %u", i));
return 0;
}
tmp_entry->temp_file = temp;
// add header
unsigned int tmp_channel = *(options->channel_mask->storage + i % size);
if (options->shift_channel) tmp_channel++;
if (options->device_identify == DEVICE_ID_ELITE) {
if (i == 0) {
fprintf(temp, "#channel,%d\n", tmp_channel);
fprintf(temp, "#time[ms],current[nA]\n");
} else if (i % 3 == 0) {
fprintf(temp, "channel,%d\n", tmp_channel);
fprintf(temp, "time[ms],current[nA]\n");
} else if (i % 3 == 1) {
fprintf(temp, "channel,%d\n", tmp_channel);
fprintf(temp, "time[ms],voltage[uV]\n");
} else {
fprintf(temp, "channel,%d\n", tmp_channel);
fprintf(temp, "time[ms],resister[kohm]\n");
}
} else {
fprintf(temp, "#channel,%d\n", tmp_channel);
fprintf(temp, "#time[ms],voltage[uV]\n");
}
}
options->temp_file_size = new_size;
options->temp_file_entry = new_entry;
// redo
result = ExportCsvOptions_get_empty_temp_index(options, channel, index);
if (result == -1) {
// channel not found, with error
return 0;
} else if (result == 1) {
last_entry = options->temp_file_entry + *index;
} else {
ensure_error(PyErr_SetString(PyExc_RuntimeError, "channel should be found"));
return 0;
}
}
// init field
last_entry->temp_channel = channel;
last_entry->temp_time_step = 0;
last_entry->temp_line_number = 0;
return 1;
}
/**
* RecordingDataInitFunction
* @return: 1 on fail
*/
static int _export_csv_decode_init(RecordingDataDecodeState *state, //
void * _data) {
if (_data == NULL) {
ensure_error(PyErr_SetString(PyExc_RuntimeError, "_export_csv_decode_init(_data=NULL)"));
return 1;
}
ExportCsvOptions *options = (ExportCsvOptions *)_data;
if (options->time_stamp > 0 && state->time_stamp <= options->time_stamp) {
options->valid_time_stamp = 0;
} else {
options->valid_time_stamp = 1;
options->time_stamp = state->time_stamp;
}
options->sample_rate = state->sample_rate;
return 0;
}
/**
* @return: 1 on fail
*/
static int _export_csv_append_line(ExportCsvOptions *options, //
unsigned int channel,
double time_stamp,
double value) {
// get last column
int index = 0;
double time_step = 0;
ExportCsvTempFileEntry *entry = NULL;
if (ExportCsvOptions_get_temp_index(options, channel, &index)) {
entry = options->temp_file_entry + index;
time_step = entry->temp_time_step;
}
// next column?
int new_column = 0;
if (options->limit_column_time > 0 && entry != NULL) {
double step = (time_stamp / options->limit_column_time / 1000.0);
if (step > time_step && entry->temp_line_number > options->at_least_row_number) {
new_column = 1;
time_step = step;
}
}
// new column
if (entry == NULL || new_column) {
if (ExportCsvOptions_new_temp(options, channel, &index)) {
entry = options->temp_file_entry + index;
entry->temp_time_step = time_step;
} else {
return 1;
}
}
FILE *export_file = entry->temp_file;
if (export_file == NULL) {
ensure_error(PyErr_SetString(PyExc_RuntimeError, "ExportCsvOptions->temp_file[channel] = NULL"));
return 1;
}
// data
if (options->device_identify == DEVICE_ID_ELITE && (channel % 3) == 1) {
fprintf(export_file, "%.4lf,%.3f\n", time_stamp, value);
} else if (options->device_identify == DEVICE_ID_ELITE && (channel % 3) == 2) {
fprintf(export_file, "%.4lf,%.3f\n", time_stamp, value / 1000);
} else {
fprintf(export_file, "%.4lf,%f\n", time_stamp, value);
}
entry->temp_line_number++;
return 0;
}
/**
* RecordingDataAppendFunction
* @return: 1 on fail
*/
static int _export_csv_append_raw(RecordingDataDecodeState *state, //
unsigned int data_index,
unsigned int device,
unsigned int channel,
int * value,
void * _data) {
if (_data == NULL) {
ensure_error(PyErr_SetString(PyExc_RuntimeError, "_export_csv_append_raw(_data=NULL)"));
return 1;
}
if (value == NULL) {
// ignore invalid data
return 0;
}
ExportCsvOptions *options = (ExportCsvOptions *)_data;
if (!options->valid_time_stamp) {
// ignore duplicate data section
return 0;
}
// filter channel
if (!int_set_contain(options->channel_mask, channel)) {
return 0;
}
// calculate time stamp
double time_stamp;
if (options->sample_rate == 0) {
time_stamp = options->time_stamp;
} else {
time_stamp = options->time_stamp + data_index * 1000.0 / options->sample_rate;
// printf("@@@@@ Time stamp = %lf\n", time_stamp);
// printf("@@@@@ data_index = %d\n", data_index);
// printf("@@@@@ options->sample_rate = %lf\n", options->sample_rate);
}
return _export_csv_append_line(options, channel, time_stamp, (double)*value);
}
/**
* RecordingDataAppendFunction
* @return: 1 on fail
*/
static int _export_csv_append_resample(RecordingDataDecodeState *state, //
unsigned int data_index,
unsigned int device,
unsigned int channel,
int * value,
void * _data) {
if (_data == NULL) {
ensure_error(PyErr_SetString(PyExc_RuntimeError, "_export_csv_append_resample(_data=NULL)"));
return 1;
}
if (value == NULL) {
// ignore invalid data
return 0;
}
ExportCsvOptions *options = (ExportCsvOptions *)_data;
if (!options->valid_time_stamp) {
// ignore duplicate data section
return 0;
}
// filter channel
if (!int_set_contain(options->channel_mask, channel)) {
return 0;
}
// calculate time stamp
double time_delta = 1000.0 / options->resample_rate;
double time_stamp;
if (options->sample_rate == 0) {
time_stamp = options->time_stamp;
} else {
time_stamp = options->time_stamp + data_index * 1000.0 / options->sample_rate;
// printf("##### Time stamp = %lf\n", time_stamp);
// printf("##### data_index = %d\n", data_index);
// printf("##### options->sample_rate = %lf\n", options->sample_rate);
}
// check entry size
if (options->entry_size <= channel) {
ExportCsvOptions_extend(options, channel);
}
ReSampleEntry *entry = options->entries[channel];
// check entry
if (entry == NULL) {
// new entry
entry = (ReSampleEntry *)PyMem_Malloc(sizeof(ReSampleEntry));
if (entry == NULL) return 1;
options->entries[channel] = entry;
entry->time_step = (time_stamp / time_delta);
entry->count = 0;
entry->value = 0;
}
// check time range
double base_time_stamp = options->time_stamp + entry->time_step * time_delta;
double double_value;
if (base_time_stamp + time_delta < time_stamp) {
if (ReSampleEntry_value(options->resample_method, entry, &double_value)) {
if (_export_csv_append_line(options, channel, base_time_stamp, double_value)) {
return 1;
}
}
entry->time_step = (time_stamp / time_delta);
entry->count = 0;
entry->value = 0;
}
// add value
ReSampleEntry_add_value(options->resample_method, entry, *value);
return 0;
}
/**
* RecordingMetaExportInit
* @return: 1 on fail
*/
static int export_csv_init(RecordingMeta *recording_meta, //
FILE * export_file,
void * _options) {
if (recording_meta == NULL) {
PyErr_SetString(PyExc_RuntimeError, "export_csv_init(recording_meta=NULL)");
return 1;
}
if (export_file == NULL) {
PyErr_SetString(PyExc_RuntimeError, "export_csv_init(export_file=NULL)");
return 1;
}
if (_options == NULL) {
PyErr_SetString(PyExc_RuntimeError, "export_csv_init(options=NULL)");
return 1;
}
#define SIZE 256
char buf[SIZE];
ExportCsvOptions *options = (ExportCsvOptions *)_options;
// data format
if (strncmp(DATA_FORMAT_REC, recording_meta->data_format, DATA_FORMAT_SIZE) != 0) {
PyErr_SetString(PyExc_RuntimeError, "illegal recording meta encode data format");
return 1;
}
options->export_file = export_file;
// detect which device it is
#define serial_eq(P, p, V, v, S, s) \
({ \
char * __s = recording_meta->device_serial; \
__s[0] == P &&__s[1] == p &&__s[2] == V &&__s[3] == v &&__s[4] == S &&__s[5] == s; \
})
if(*(recording_meta->device_name) == 'E'){
options->device_identify = DEVICE_ID_ELITE;
}
if (serial_eq(0, 2, 1, 2, 0, 0) // EliteZM
|| serial_eq(0, 2, 1, 3, 0, 0) // EliteZMS
|| serial_eq(0, 2, 1, 4, 0, 0) // EliteZML
|| serial_eq(0, 2, 1, 5, 0, 0) // EliteZML
) {
// is Elite device
options->device_identify = DEVICE_ID_ELITE;
}
#undef serial_eq
// print header
if (options->include_header) {
#ifndef _RASPBERRY_PI3_
{
unsigned long create_time = recording_meta->create_time;
time_t create_time_t = (time_t)(create_time / 1000);
int read = strftime(buf, SIZE, "%F %T", localtime(&create_time_t));
if (read == 0) return 1;
fprintf(export_file, "#create_time,%s\n", buf);
}
#endif
// device name
fprintf(export_file, "#device,%s,", recording_meta->device_name);
// device mac address
fprintf(export_file, //
"%02hhX:%02hhX:%02hhX:%02hhX:%02hhX:%02hhX\n",
recording_meta->device_address[5],
recording_meta->device_address[4],
recording_meta->device_address[3],
recording_meta->device_address[2],
recording_meta->device_address[1],
recording_meta->device_address[0]);
// recording parameter
fprintf(export_file, "#parameter\n");
// recording parameter
for (unsigned int parameter_index = 0; parameter_index < recording_meta->parameter_count; parameter_index++) {
RecordingParameter *parameter = recording_meta->parameter + parameter_index;
// parameter name
if (!file_seek_abs(recording_meta->meta_file, parameter->name_seek_pos, "parameter name")) {
return 1;
}
if (!file_read_string(recording_meta->meta_file, buf, "parameter name")) {
return 1;
}
fprintf(export_file, "#%s,", buf);
// parameter value
if (parameter->parameter_type == 0 || parameter->data_length == 0) {
fprintf(export_file, "\n");
} else {
if (!file_seek_abs(recording_meta->meta_file, parameter->value_seek_pos, "parameter value")) {
return 1;
}
int shift_channel = (options->shift_channel && (strcmp("CHANNEL", buf) == 0)) ? 1 : 0;
for (unsigned int i = 0; i < parameter->data_length; i++) {
if (i > 0) fprintf(export_file, ",");
switch (parameter->parameter_type) {
case '?': // bool
if (fgetc(recording_meta->meta_file)) {
fprintf(export_file, "True");
} else {
fprintf(export_file, "False");
}
break;
case 'p': // str
file_read_string(recording_meta->meta_file, buf, "parameter value");
fprintf(export_file, "%s", buf);
break;
case 'b': // i8
case 'B': // u8
file_read(recording_meta->meta_file, buf, 1, "parameter value");
if (parameter->parameter_type == 'B') {
fprintf(export_file, "%u", buf_u8(buf) + shift_channel);
} else {
fprintf(export_file, "%d", buf_i8(buf) + shift_channel);
}
break;
case 'h': // i16
case 'H': // u16
file_read(recording_meta->meta_file, buf, 2, "parameter value");
if (parameter->parameter_type == 'H') {
fprintf(export_file, "%u", buf_u16B(buf) + shift_channel);
} else {
fprintf(export_file, "%d", buf_i16B(buf) + shift_channel);
}
break;
case 'i': // i32
case 'I': // u32
file_read(recording_meta->meta_file, buf, 4, "parameter value");
if (parameter->parameter_type == 'I') {
fprintf(export_file, "%u", buf_u32B(buf) + shift_channel);
} else {
fprintf(export_file, "%d", buf_i32B(buf) + shift_channel);
}
break;
case 'f': // float
file_read(recording_meta->meta_file, buf, 4, "parameter value");
fprintf(export_file, "%f", buf_f32B(buf));
break;
case 'd': // XXX double support
default:
return 1;
}
}
fprintf(export_file, "\n");
}
}
}
return 0;
#undef SIZE
}
/**
* RecordingMetaExportFunc
* @return: 1 on fail
*/
static int export_csv_func(RecordingMeta *recording_meta, //
FILE * export_file,
int file_index,
FILE * recording_file,
void * _options) {
if (recording_meta == NULL) {
PyErr_SetString(PyExc_RuntimeError, "export_csv_func(recording_meta=NULL)");
return 1;
}
if (export_file == NULL) {
PyErr_SetString(PyExc_RuntimeError, "export_csv_func(export_file=NULL)");
return 1;
}
if (_options == NULL) {
PyErr_SetString(PyExc_RuntimeError, "export_csv_func(options=NULL)");
return 1;
}
#define _file_seek(_offset, _message) \
do { \
if (!file_seek_rel(recording_file, _offset, _message)) { \
return 1; \
} \
} while (0)
#define SIZE 256
char buf[SIZE];
ExportCsvOptions *options = (ExportCsvOptions *)_options;
if (file_index >= 0) {
// move to beginning
if (!file_seek_abs(recording_file, 0, "recording file")) {
return 1;
}
_file_seek(4, "magic number");
_file_seek(4, "version number");
_file_seek(8, "create time");
_file_seek(16, "file uuid");
_file_seek(1, "file serial number");
// data_format
file_read(recording_file, buf, DATA_FORMAT_SIZE, "data format");
if (strncmp(DATA_FORMAT_REC, buf, DATA_FORMAT_SIZE) != 0) {
PyErr_SetString(PyExc_RuntimeError, "illegal recording file encode data format");
return 1;
}
RecordingDataInitFunction * init_func = &_export_csv_decode_init;
RecordingDataAppendFunction *append_func;
if (options->resample_rate == SAMPLE_MODE_RAW) {
append_func = _export_csv_append_raw;
} else {
append_func = _export_csv_append_resample;
}
if (decode_recording_file_ALL(recording_file, //
init_func,
append_func,
options)) {
return 1;
}
} else {
// end of recording file
if (options->entries != NULL) {
double time_delta = 1000.0 / options->resample_rate;
for (unsigned int channel = 0; channel < options->entry_size; channel++) {
// get entry
ReSampleEntry *entry = options->entries[channel];
if (entry != NULL) {
// get value
double double_value;
double base_time_stamp = options->time_stamp + entry->time_step * time_delta;
if (ReSampleEntry_value(options->resample_method, entry, &double_value)) {
_export_csv_append_line(options, channel, base_time_stamp, double_value);
}
}
}
}
// merge temp file
for (unsigned int i = 0; i < options->temp_file_size; i++) {
FILE *temp_file = (options->temp_file_entry + i)->temp_file;
if (temp_file != NULL) {
rewind(temp_file);
}
}
#define _file_read_line(_file) \
({ \
int __c; \
int read = 0; \
for (;;) { \
__c = fgetc(_file); \
if (__c == EOF || __c == '\n') { \
buf[read] = '\0'; \
break; \
} else { \
buf[read++] = (char)__c; \
} \
} \
__c; \
})
//
unsigned int eof_file;
do {
eof_file = 0;
for (unsigned int i = 0; i < options->temp_file_size; i++) {
FILE *temp_file = (options->temp_file_entry + i)->temp_file;
if (temp_file == NULL) {
fprintf(export_file, (i == 0) ? "," : ",,");
eof_file++;
} else if (_file_read_line(temp_file) == EOF) {
fprintf(export_file, (i == 0) ? "," : ",,");
eof_file++;
} else {
fprintf(export_file, "%s%s", (i == 0) ? "" : ",", buf);
}
}
fprintf(export_file, "\n");
// all file EOF?
} while (eof_file < options->temp_file_size);
// close
for (unsigned int i = 0; i < options->temp_file_size; i++) {
FILE *temp_file = (options->temp_file_entry + i)->temp_file;
if (temp_file != NULL) {
fclose(temp_file);
}
(options->temp_file_entry + i)->temp_file = NULL;
}
}
return 0;
#undef SIZE
#undef _file_seek
#undef _file_read_line
}
static PyObject *export_csv(const char * meta_filepath, //
const char * export_path,
unsigned int resample_rate,
unsigned int resample_method,
unsigned int limit_column_time,
IntSet * channel_mask,
unsigned char include_header /*boolean*/,
unsigned char shift_channel /*boolean*/) {
if (meta_filepath == NULL) return NULL;
if (export_path == NULL) return NULL;
ExportCsvOptions options = {
// clang-format off
.limit_column_time = limit_column_time,
.at_least_row_number = 100000,
.channel_mask = channel_mask,
.resample_rate = resample_rate,
.resample_method = resample_method,
.include_header = include_header,
.shift_channel = shift_channel
// clang-format on
};
ExportCsvOptions_init(&options);
PyObject *ret = export_recording_meta(meta_filepath, //
export_path,
&export_csv_init,
&export_csv_func,
&options);
ExportCsvOptions_free(&options);
return ret;
}
#endif