// ----------------------------------------------------------------------------
// Copyright (C) 2014
// David Freese, W1HKJ
//
// This file is part of fldigi
//
// fldigi is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// fldigi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
// ----------------------------------------------------------------------------
#include
#include
#include
#include
#include
#include
#include "fl_digi.h"
#include "signal.h"
#include "threads.h"
#include "adif_io.h"
#include "config.h"
#include "configuration.h"
#include "lgbook.h"
#include "icons.h"
#include "gettext.h"
#include "debug.h"
#include "util.h"
#include "date.h"
#include "logsupport.h"
#include "qrunner.h"
#include "timeops.h"
using namespace std;
static pthread_mutex_t logfile_mutex = PTHREAD_MUTEX_INITIALIZER;
size_t ptr, ptr2;
string sbuff;
#ifdef __WOE32__
static const char *szEOL = "\r\n";
#else
static const char *szEOL = "\n";
#endif
static const char *szEOR = "";
// These ADIF fields define the ADIF database
FIELD fields[] = {
// TYPE, FSIZE, NAME, WIDGET
{FREQ, 12, "FREQ", &btnSelectFreq}, // QSO frequency in Mhz
{CALL, 30, "CALL", &btnSelectCall}, // contacted stations CALLSIGN
{ADIF_MODE, 20, "MODE", &btnSelectMode}, // QSO mode
{SUBMODE, 20, "SUBMODE", NULL}, // QSO submode
{NAME, 80, "NAME", &btnSelectName}, // contacted operators NAME
{QSO_DATE, 8, "QSO_DATE", &btnSelectQSOdateOn}, // QSO data
{QSO_DATE_OFF, 8, "QSO_DATE_OFF", &btnSelectQSOdateOff},// QSO data OFF, according to ADIF 2.2.6
{TIME_OFF, 6, "TIME_OFF", &btnSelectTimeOFF}, // HHMM or HHMMSS in UTC
{TIME_ON, 6, "TIME_ON", &btnSelectTimeON}, // HHMM or HHMMSS in UTC
{QTH, 100, "QTH", &btnSelectQth}, // contacted stations city
{RST_RCVD, 3, "RST_RCVD", &btnSelectRSTrcvd}, // received signal report
{RST_SENT, 3, "RST_SENT", &btnSelectRSTsent}, // sent signal report
{STATE, 20, "STATE", &btnSelectState}, // contacted stations STATE
{VE_PROV, 20, "VE_PROV", &btnSelectProvince}, // 2 letter abbreviation for Canadian Province
{NOTES, 512, "NOTES", &btnSelectNotes}, // QSO notes
{QSLRDATE, 8, "QSLRDATE", &btnSelectQSLrcvd}, // QSL received date
{QSLSDATE, 8, "QSLSDATE", &btnSelectQSLsent}, // QSL sent date
{EQSLRDATE, 8, "EQSLRDATE", &btnSelecteQSLrcvd}, // EQSL received date
{EQSLSDATE, 8, "EQSLSDATE", &btnSelecteQSLsent}, // EQSL sent date
{LOTWRDATE, 8, "LOTWRDATE", &btnSelectLOTWrcvd}, // LOTW received date
{LOTWSDATE, 8, "LOTWSDATE", &btnSelectLOTWsent}, // LOTW sent date
{GRIDSQUARE, 8, "GRIDSQUARE", &btnSelectLOC}, // contacted stations Maidenhead Grid Square
{BAND, 8, "BAND", &btnSelectBand}, // QSO band
{CNTY, 60, "CNTY", &btnSelectCNTY}, // secondary political subdivision, ie: county
{COUNTRY, 60, "COUNTRY", &btnSelectCountry}, // contacted stations DXCC entity name
{CQZ, 8, "CQZ", &btnSelectCQZ}, // contacted stations CQ Zone
{DXCC, 8, "DXCC", &btnSelectDXCC}, // contacted stations Country Code
{QSL_VIA, 256, "QSL_VIA", &btnSelectQSL_VIA}, // contacted stations path
{IOTA, 20, "IOTA", &btnSelectIOTA}, // Islands on the air
{ITUZ, 20, "ITUZ", &btnSelectITUZ}, // ITU zone
{CONT, 60, "CONT", &btnSelectCONT}, // contacted stations continent
{SRX, 50, "SRX", &btnSelectSerialIN}, // received serial number for a contest QSO
{STX, 50, "STX", &btnSelectSerialOUT}, // QSO transmitted serial number
{XCHG1, 100, "SRX_STRING", &btnSelectXchgIn}, // contest exchange #1 / free1 in xlog
{MYXCHG, 100, "STX_STRING", &btnSelectMyXchg}, // contest exchange sent
{CLASS, 20, "CLASS", &btnSelectClass}, // Field Day / School RR class received
{ARRL_SECT, 20, "ARRL_SECT", &btnSelectSection}, // ARRL section received
{TX_PWR, 8, "TX_PWR", &btnSelectTX_pwr}, // power transmitted by this station
{OP_CALL, 30, "OPERATOR", &btnSelectOperator}, // Callsign of person logging the QSO
{STA_CALL, 30, "STATION_CALLSIGN", &btnSelectStaCall}, // Callsign of transmitting station
{MY_GRID, 8, "MY_GRIDSQUARE", &btnSelectStaGrid}, // Xmt station locator
{MY_CITY, 60, "MY_CITY", &btnSelectStaCity}, // Xmt station location
{SS_SEC, 20, "CWSS_SECTION", &btnSelect_cwss_section}, // CW sweepstakes
{SS_SERNO, 20, "CWSS_SERNO", &btnSelect_cwss_serno},
{SS_PREC, 20, "CWSS_PREC", &btnSelect_cwss_prec},
{SS_CHK, 20, "CWSS_CHK", &btnSelect_cwss_check},
{AGE, 2, "AGE", &btnSelectAge}, // contacted operators age in years
{TEN_TEN, 10, "TEN_TEN", &btnSelect_1010}, // ten ten # of other station
{CHECK, 10, "CHECK", &btnSelectCheck}, // contest identifier
{FD_CLASS, 20, "FD_CLASS", NULL}, // Field Day Rcvd
{FD_SECTION, 20, "FD_SECTION", NULL}, // FD section received
{TROOPS, 20, "TROOPS", NULL}, // JOTA troop number sent
{TROOPR, 20, "TROOPR", NULL}, // JOTA troop number received
{SCOUTS, 20, "SCOUTS", NULL},
{SCOUTR, 20, "SCOUTR", NULL},
{NUMFIELDS, 0, "", NULL}
};
// This ADIF fields is in the fldigi QSO database, but not saved in the ADIF file
/*
{EXPORT, 0, "EXPORT", NULL}, // used to indicate record is to be exported
*/
// These ADIF fields are not in the fldigi QSO database
/*
{COMMENT, 256, "COMMENT", NULL}, // comment field for QSO
{ADDRESS, 256, "ADDRESS", NULL}, // contacted stations mailing address
{PFX, 20, "PFX", NULL}, // WPA prefix
{PROP_MODE, 100, "PROP_MODE", NULL}, // propogation mode
{QSL_MSG, 256, "QSL_MSG", NULL}, // personal message to appear on qsl card
{QSL_RCVD, 4, "QSL_RCVD", NULL}, // QSL received status
{QSL_SENT, 4, "QSL_SENT", NULL}, // QSL sent status
{QSL_VIA, 20, "QSL_VIA", NULL}, // QSL via this person
{RX_PWR, 8, "RX_PWR", NULL}, // power of other station in watts
{SAT_MODE, 20, "SAT_MODE", NULL}, // satellite mode
{SAT_NAME, 20, "SAT_NAME", NULL}, // satellite name
};
*/
static string read_errors;
static int num_read_errors;
static void write_rxtext(const char *s)
{
ReceiveText->addstr(s);
}
static char *fastlookup = 0;
static unsigned int maxlen = 0;
static void initfields()
{
if (fastlookup) return; // may have multiple instances using common code
int i = 0;
while (fields[i].type != NUMFIELDS) {
if (strlen(fields[i].name) > maxlen) maxlen = strlen(fields[i].name);
i++;
}
maxlen++;
fastlookup = new char[maxlen * i + 1];
fastlookup[0] = 0;
i = 0;
while (fields[i].type != NUMFIELDS) {
strcat(fastlookup, fields[i].name);
unsigned int n = maxlen - strlen(fastlookup) % maxlen;
if (n > 0 && n < maxlen) for (unsigned int j = 0; j < n; j++) strcat(fastlookup, " ");
i++;
}
}
static inline int findfield( char *p )
{
if (strncasecmp (p, "EOR>", 4) == 0 || !maxlen)
return -1;
char *pos;
char *p1 = strchr(p, ':');
char *p2 = strchr(p, '>');
if (p1 && p2) {
if (p1 < p2) {
pos = p;
do { *pos = toupper(*pos); } while (++pos < p1);
*p1 = 0;
pos = strcasestr(fastlookup, p);
*p1 = ':';
if (pos) {
return fields[(pos - fastlookup) / maxlen].type;
}
}
}
return -2; //search key not found
}
int cAdifIO::instances = 0;
cAdifIO::cAdifIO ()
{
initfields();
instances++;
}
cAdifIO::~cAdifIO()
{
if (--instances == 0) {
delete [] fastlookup;
fastlookup = 0;
}
}
char * cAdifIO::fillfield (int recnbr, int fieldnum, char *buff)
{
char *p1 = strchr(buff, ':');
char *p2 = strchr(buff, '>');
if (!p1 || !p2 || p2 < p1) {
return 0; // bad ADIF specifier ---> no ':' after field name
}
p1++;
int fldsize = 0;
while (p1 != p2) {
if (*p1 >= '0' && *p1 <= '9') {
fldsize = fldsize * 10 + *p1 - '0';
}
p1++;
}
string tmp = "";
tmp.assign(p2+1, fldsize);
// added to disallow very large corrupted adif fields
if (fldsize > fields[fieldnum].fsize) {
string bfr = buff;
tmp.erase(fields[fieldnum].fsize);
static char szmsg[1000];
snprintf(szmsg, sizeof(szmsg),
"In record # %d, <%s, too large, saving first %d characters\n",
recnbr+1,
bfr.substr(0, (int)(p2+1 - buff)).c_str(),
fields[fieldnum].fsize );
read_errors.append(szmsg);
num_read_errors++;
}
if ((fieldnum == TIME_ON || fieldnum == TIME_OFF) && fldsize < 6)
while (tmp.length() < 6) tmp += '0';
adifqso->putField( fieldnum, tmp.c_str(), tmp.length() );
return p2 + fldsize + 1;
}
void cAdifIO::do_readfile(const char *fname, cQsoDb *db)
{
guard_lock lock(&logfile_mutex);
int found;
static char szmsg[500];
read_errors.clear();
num_read_errors = 0;
// open the adif file
FILE *adiFile = fl_fopen (fname, "rb");
if (adiFile == NULL) {
LOG_ERROR("Could not open %s", fname);
return;
}
/*
struct timespec t0, t1, t2;
#ifdef _POSIX_MONOTONIC_CLOCK
clock_gettime(CLOCK_MONOTONIC, &t0);
#else
clock_gettime(CLOCK_REALTIME, &t0);
#endif
*/
char buff[16384];
sbuff.clear();
memset(buff, 0, 16384);
int retnbr = fread(buff, 1, 16384, adiFile);
while (retnbr) {
sbuff.append(buff, retnbr);
retnbr = fread(buff, 1, 16384, adiFile);
}
fclose(adiFile);
size_t p;//, ptr, ptr2;
p = sbuff.find("");
if (p == std::string::npos) p = sbuff.find("");
if (p == std::string::npos) {
LOG_ERROR("Could not find in %s", fname);
return;
}
if ((sbuff.find("") == std::string::npos) &&
(sbuff.find("") == std::string::npos)) {
LOG_ERROR("Empty log file %s", fname);
return;
}
size_t recend;
int recnbr = 0;
p = sbuff.find('<', p + 1);
while (p != std::string::npos) {
recend = sbuff.find("", p);
if (recend == string::npos) recend = sbuff.find("", p);
if (recend == string::npos)
break;
ptr = p;
adifqso = 0;
while (ptr != std::string::npos) {
ptr2 = sbuff.find('<', ptr + 1);
if (ptr2 == string::npos)
break;
found = findfield( &sbuff[ptr + 1] );
if (found > -1) {
if (!adifqso) adifqso = db->newrec(); // need new record in db
fillfield (recnbr, found, &sbuff[ptr + 1]);
} else if (found == -1) { // reached;
break;
}
ptr = ptr2;
if (ptr == std::string::npos)
break; // corrupt record
}
recnbr++;
p = sbuff.find('<', recend + 1);
}
/*
#ifdef _POSIX_MONOTONIC_CLOCK
clock_gettime(CLOCK_MONOTONIC, &t2);
#else
clock_gettime(CLOCK_REALTIME, &t2);
#endif
float t = t1.tv_sec - t0.tv_sec + (t1.tv_nsec - t0.tv_nsec)/1e9;
float tp = t2.tv_sec - t1.tv_sec + (t2.tv_nsec - t1.tv_nsec)/1e9;
snprintf(szmsg, sizeof(szmsg), "\n\
================================================\n\
Read Logbook: %s\n\
read %d records in %4.1f seconds\n\
parsed in %4.1f seconds\n\
================================================\n",
fname, db->nbrRecs(), t, tp);
*/
snprintf(szmsg, sizeof(szmsg), "\n\
================================================\n\
Read Logbook: %s\n\
%d records\n\
================================================\n",
fname, db->nbrRecs());
if (progdefaults.DisplayLogbookRead && (db == &qsodb))
REQ(write_rxtext, szmsg);
LOG_INFO("%s", szmsg);
if (num_read_errors) {
if (!read_errors.empty()) {
read_errors.append("\n");
read_errors.append(szmsg);
} else
read_errors.assign(szmsg);
snprintf(szmsg, sizeof(szmsg),
"Corrected %d errors. Save logbook and then reload\n",
num_read_errors);
read_errors.append("\n\
================================================\n").append(szmsg);
read_errors.append("\
================================================\n");
REQ(write_rxtext, read_errors.c_str());
}
if (db == &qsodb)
REQ(adif_read_OK);
}
static const char *adifmt = "<%s:%d>";
// write ALL or SELECTED records to the designated file
int cAdifIO::writeFile (const char *fname, cQsoDb *db)
{
guard_lock lock(&logfile_mutex);
string ADIFHEADER;
ADIFHEADER = "File: %s";
ADIFHEADER.append(szEOL);
ADIFHEADER.append("%s");
ADIFHEADER.append(szEOL);
ADIFHEADER.append("%s");
ADIFHEADER.append(szEOL);
ADIFHEADER.append("%s");
ADIFHEADER.append(szEOL);
ADIFHEADER.append("");
ADIFHEADER.append(szEOL);
// open the adif file
cQsoRec *rec;
string sFld;
adiFile = fl_fopen (fname, "wb");
if (!adiFile)
return 1;
fprintf (adiFile, ADIFHEADER.c_str(),
fl_filename_name(fname),
strlen(ADIF_VERS), ADIF_VERS,
strlen(PACKAGE_NAME), PACKAGE_NAME,
strlen(PACKAGE_VERSION), PACKAGE_VERSION);
string sName;
int field_type;
for (int i = 0; i < db->nbrRecs(); i++) {
rec = db->getRec(i);
if (rec->getField(EXPORT)[0] == 'E') {
int j = 0;
while (fields[j].type != NUMFIELDS) {
if (strcmp(fields[j].name,"MYXCHG") == 0) { j++; continue; }
if (strcmp(fields[j].name,"XCHG1") == 0) { j++; continue; }
if (fields[j].btn != NULL) {
if ((*fields[j].btn)->value()) {
field_type = fields[j].type;
sFld = rec->getField(field_type);
sName = fields[j].name;
if (field_type == ADIF_MODE && !sFld.empty()) {
fprintf(adiFile, adifmt,
"MODE",
adif2export(sFld).length());
fprintf(adiFile, "%s", adif2export(sFld).c_str());
if (!adif2submode(sFld).empty()) {
fprintf(adiFile, adifmt,
"SUBMODE",
adif2submode(sFld).length());
fprintf(adiFile, "%s", adif2submode(sFld).c_str());
}
} else {
if (!sFld.empty()) {
fprintf(adiFile, adifmt,
sName.c_str(),
sFld.length());
//Exchange commas by dots in frequency for ADIF-conformity
if (strcmp(fields[j].name,"FREQ") == 0) {
char sfreq[20];
char* comma_position;
memset(sfreq, 0, 20);
strncpy (sfreq, sFld.c_str(), sizeof(sfreq) - 1);
comma_position = strchr(sfreq,',');
if (comma_position != NULL) {
*comma_position = '.';
}
fprintf(adiFile, "%s", sfreq);
} else {
fprintf(adiFile, "%s", sFld.c_str());
}
}
}
}
}
j++;
}
rec->putField(EXPORT,"");
db->qsoUpdRec(i, rec);
fprintf(adiFile, "%s", szEOR);
fprintf(adiFile, "%s", szEOL);
}
}
fclose (adiFile);
return 0;
}
// write ALL records to the common log
//======================================================================
// thread support writing database
//======================================================================
pthread_t* ADIF_RW_thread = 0;
pthread_mutex_t ADIF_RW_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t ADIF_RW_cond = PTHREAD_COND_INITIALIZER;
static void ADIF_RW_init();
static string adif_file_image;
static string adif_file_name;
static string records;
static string record;
static char recfield[200];
static bool ADIF_READ = false;
static bool ADIF_WRITE = false;
static cQsoDb *adif_db;
static cAdifIO *adifIO = 0;
void cAdifIO::readFile (const char *fname, cQsoDb *db)
{
ENSURE_THREAD(FLMAIN_TID);
if (!ADIF_RW_thread)
ADIF_RW_init();
pthread_mutex_lock(&ADIF_RW_mutex);
adif_file_name = fname;
adif_db = db;
adifIO = this;
ADIF_READ = true;
pthread_cond_signal(&ADIF_RW_cond);
pthread_mutex_unlock(&ADIF_RW_mutex);
}
static cQsoDb *adifdb = 0;
static cQsoDb *wrdb = 0;
static struct timespec t0, t1;
std::string cAdifIO::adif_record(cQsoRec *rec)
{
static std::string record;
static std::string sFld;
record.clear();
for (int j = 0; fields[j].type != NUMFIELDS; j++) {
if (strcmp(fields[j].name,"MYXCHG") == 0) continue;
if (strcmp(fields[j].name,"XCHG1") == 0) continue;
sFld = rec->getField(fields[j].type);
if (!sFld.empty()) {
snprintf(recfield, sizeof(recfield),
adifmt,
fields[j].name,
sFld.length());
record.append(recfield).append(sFld);
}
}
record.append(szEOR);
record.append(szEOL);
return record;
}
int cAdifIO::writeAdifRec (cQsoRec *rec, const char *fname)
{
std::string strRecord = adif_record(rec);
FILE *adiFile = fl_fopen (fname, "ab");
if (!adiFile) {
LOG_ERROR("Cannot write to %s", fname);
return 1;
}
LOG_INFO("Write record to %s", fname);
fprintf (adiFile, "%s", strRecord.c_str());
fclose (adiFile);
return 0;
}
int cAdifIO::writeLog (const char *fname, cQsoDb *db, bool immediate) {
ENSURE_THREAD(FLMAIN_TID);
if (!ADIF_RW_thread)
ADIF_RW_init();
#ifdef _POSIX_MONOTONIC_CLOCK
clock_gettime(CLOCK_MONOTONIC, &t0);
#else
clock_gettime(CLOCK_REALTIME, &t0);
#endif
if (!immediate) {
pthread_mutex_lock(&ADIF_RW_mutex);
adif_file_name = fname;
adifIO = this;
ADIF_WRITE = true;
if (wrdb) delete wrdb;
wrdb = new cQsoDb(db);
adifdb = wrdb;
pthread_cond_signal(&ADIF_RW_cond);
pthread_mutex_unlock(&ADIF_RW_mutex);
} else {
adif_file_name = fname;
adifdb = db;
do_writelog();
}
return 1;
}
void cAdifIO::do_writelog()
{
guard_lock lock(&logfile_mutex);
string ADIFHEADER;
ADIFHEADER = "File: %s";
ADIFHEADER.append(szEOL);
ADIFHEADER.append("%s");
ADIFHEADER.append(szEOL);
ADIFHEADER.append("%s");
ADIFHEADER.append(szEOL);
ADIFHEADER.append("%s");
ADIFHEADER.append(szEOL);
ADIFHEADER.append("");
ADIFHEADER.append(szEOL);
adiFile = fl_fopen (adif_file_name.c_str(), "wb");
if (!adiFile) {
LOG_ERROR("Cannot write to %s", adif_file_name.c_str());
if (wrdb) delete wrdb;
return;
}
LOG_INFO("Writing %s", adif_file_name.c_str());
cQsoRec *rec;
fprintf ( adiFile, ADIFHEADER.c_str(),
fl_filename_name(adif_file_name.c_str()),
strlen(ADIF_VERS), ADIF_VERS,
strlen(PACKAGE_NAME), PACKAGE_NAME,
strlen(PACKAGE_VERSION), PACKAGE_VERSION );
for (int i = 0; i < adifdb->nbrRecs(); i++) {
rec = adifdb->getRec(i);
fprintf (adiFile, "%s", adif_record(rec).c_str());
if (wrdb) adifdb->qsoUpdRec(i, rec);
}
fflush (adiFile);
fclose (adiFile);
if (wrdb) delete wrdb;
#ifdef _POSIX_MONOTONIC_CLOCK
clock_gettime(CLOCK_MONOTONIC, &t1);
#else
clock_gettime(CLOCK_REALTIME, &t1);
#endif
t0 = t1 - t0;
float t = (t0.tv_sec + t0.tv_nsec/1e9);
static char szmsg[50];
snprintf(szmsg, sizeof(szmsg), "%d records in %4.2f seconds", adifdb->nbrRecs(), t);
LOG_INFO("%s", szmsg);
snprintf(szmsg, sizeof(szmsg), "Wrote log %d recs", adifdb->nbrRecs());
put_status(szmsg, 5.0);
return;
}
//======================================================================
// thread to support writing database in a separate thread
//======================================================================
static void *ADIF_RW_loop(void *args);
static bool ADIF_RW_EXIT = false;
static void *ADIF_RW_loop(void *args)
{
SET_THREAD_ID(ADIF_RW_TID);
for (;;) {
pthread_mutex_lock(&ADIF_RW_mutex);
pthread_cond_wait(&ADIF_RW_cond, &ADIF_RW_mutex);
pthread_mutex_unlock(&ADIF_RW_mutex);
if (ADIF_RW_EXIT)
return NULL;
if (ADIF_WRITE && adifIO) {
LOG_INFO("ADIF_WRITE: adifIO->do_writelog()");
adifIO->do_writelog();
ADIF_WRITE = false;
} else if (ADIF_READ && adifIO) {
LOG_INFO("ADIF_READ: adifIO->do_readfile(%s)", adif_file_name.c_str());
adifIO->do_readfile(adif_file_name.c_str(), adif_db);
ADIF_READ = false;
}
}
return NULL;
}
void ADIF_RW_close(void)
{
ENSURE_THREAD(FLMAIN_TID);
if (!ADIF_RW_thread)
return;
pthread_mutex_lock(&ADIF_RW_mutex);
ADIF_RW_EXIT = true;
LOG_INFO("%s", "Exiting ADIF_RW_thread");
pthread_cond_signal(&ADIF_RW_cond);
pthread_mutex_unlock(&ADIF_RW_mutex);
pthread_join(*ADIF_RW_thread, NULL);
delete ADIF_RW_thread;
ADIF_RW_thread = 0;
LOG_INFO("%s", "ADIF_RW_thread closed");
}
static void ADIF_RW_init()
{
ENSURE_THREAD(FLMAIN_TID);
if (ADIF_RW_thread)
return;
ADIF_RW_thread = new pthread_t;
ADIF_RW_EXIT = false;
if (pthread_create(ADIF_RW_thread, NULL, ADIF_RW_loop, NULL) != 0) {
LOG_PERROR("pthread_create");
return;
}
#ifdef __WIN32__
MilliSleep(100);
#else
MilliSleep(10);
#endif
}