Files
GoldenCheetah/src/MoxyDevice.cpp
Mark Liversedge fb994fa5e7 Rename Ride to Activity
.. across the code, except where it clearly is a ride
   e.g. importing PowerTap or SRM
2015-01-30 10:59:56 +00:00

552 lines
15 KiB
C++

/*
* Copyright (c) 2014 Mark Liversedge (liversedge@gmail.com)
*
* This program 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 2 of the License, or (at your option)
* any later version.
*
* This program 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, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "MoxyDevice.h"
#include "DownloadRideDialog.h"
#include "PowerTapUtil.h"
#include "Device.h"
#include "RideFile.h"
#include <cmath>
#include <errno.h>
#include <string.h>
static bool moxyRegistered =
Devices::addType("Moxy Muscle Oxygen Monitor", DevicesPtr(new MoxyDevices()) );
QString
MoxyDevices::downloadInstructions() const
{
return (tr("Make sure the Moxy is connected via USB"));
}
DevicePtr
MoxyDevices::newDevice( CommPortPtr dev )
{
return DevicePtr( new MoxyDevice( dev ));
}
static QDateTime dateTimeForRow(QString line)
{
// first row has data and time ?
QStringList tokens = line.split(",");
if (tokens.count() != 6) return QDateTime();
// parse date and time
QStringList mmdd = tokens[0].split("-");
int month = mmdd[0].toInt();
int day = mmdd[1].toInt();
int year = QDate::currentDate().year();
QStringList hhmmss = tokens[1].split(":");
int hh = hhmmss[0].toInt();
int mm = hhmmss[1].toInt();
int ss = hhmmss[2].toInt();
return QDateTime(QDate(year,month,day), QTime(hh,mm,ss));
}
// we need to sort the rows by the timestamp since it is downloaded
// cycled round and we need to unpick that !
class MoxyData {
public:
QDateTime timestamp;
QString line;
bool operator<(MoxyData right) const { return timestamp < right.timestamp; }
};
bool
MoxyDevice::download( const QDir &tmpdir,
QList<DeviceDownloadFile> &files,
QString &err)
{ Q_UNUSED(tmpdir);
Q_UNUSED(files);
int bytes=0;
char vbuf[256];
QString verString;
QString deviceInfo;
emit updateProgress(QString(tr("Connecting ... \n")));
if (!dev->open(err)) {
err = tr("ERROR: open failed: ") + err;
return false;
}
dev->setBaudRate(115200, err);
// get into engineering mode
if (writeCommand(dev, "\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
}
if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) <= 0) {
dev->close();
return false;
}
if (writeCommand(dev, "\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
}
if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) <= 0) {
dev->close();
return false;
}
// now get a prompt
if (writeCommand(dev, "\r", err) == false) {
dev->close();
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
return false;
}
// get a prompt back ?
if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) <= 0) {
dev->close();
return false;
}
// now get version
if (writeCommand(dev, "ver\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
} else if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) > 0) {
vbuf[bytes-1] = '\0';
verString = vbuf;
deviceInfo += vbuf;
} else {
dev->close();
return false;
}
// now get time on unit
if (writeCommand(dev, "time\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
} else if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) > 0) {
vbuf[bytes-1] = '\0';
deviceInfo += vbuf;
} else {
dev->close();
return false;
}
// now get battery status
if (writeCommand(dev, "batt\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
} else if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) > 0) {
vbuf[bytes-1] = '\0';
deviceInfo += vbuf;
} else {
dev->close();
return false;
}
// now get update mode
if (writeCommand(dev, "um\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
return false;
} else if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) > 0) {
vbuf[bytes-1] = '\0';
deviceInfo += vbuf;
} else {
dev->close();
return false;
}
// now lets get the data
if (writeCommand(dev, "gd\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
}
emit updateProgress(QString(tr("Downloading ... \n")));
QList<MoxyData> data;
long count = 0;
do {
if ((bytes=readData(dev, vbuf, 256, err)) > 0) {
count += bytes;
updateProgress(QString(tr("Downloading ... [%1 bytes]")).arg(count));
vbuf[bytes] = '\0';
// ignore 'delimiters' (they're just noise)
if (QString(vbuf) == " ,,,,," || QString(vbuf) == ",,,,,") continue;
// get data
QDateTime line = dateTimeForRow(vbuf);
MoxyData row;
row.line = vbuf;
row.timestamp = line;
data << row;
} else break;
// don't hang gui !
QApplication::processEvents();
} while(vbuf[0] != '>');
emit updateProgress(QString(tr("Parsing ... ")));
emit updateStatus(QString(tr("\nParsing ... ")));
// we need to sort the data by timestamp since the moxy uses a circular
// buffer which mean new lines arrive before old ones.
qSort(data);
// for deciding when to split rides
QDateTime last = QDateTime();
int rows=0;
int nfiles=0;
QTextStream os;
QTemporaryFile *tmpFile = NULL; // the file being output
// the download dialog structure
DeviceDownloadFile file;
file.extension = "csv";
// run through the data creating rides
foreach(MoxyData line, data) {
// ignore badly formatted lines
if (line.timestamp == QDateTime() || !line.timestamp.isValid()) continue;
if (line.timestamp.toMSecsSinceEpoch() > (last.toMSecsSinceEpoch() + 1800000)) {
// close last file
if (tmpFile) {
// does it have more than 30 rows?
if (rows > 30) {
// we have one file anyway
emit updateStatus(QString(tr("File #%1: %2")).arg(++nfiles).arg(file.startTime.toString("d-MMM-yy hh:mm")));
tmpFile->close();
files << file;
// free memory, but leave on disk
tmpFile->setAutoRemove(false);
delete tmpFile;
tmpFile = NULL;
} else {
// not enough data in this one
tmpFile->close();
tmpFile->setAutoRemove(true);
delete tmpFile;
tmpFile = NULL;
}
rows = 0;
}
QString tmpl = tmpdir.absoluteFilePath(".mxdl.XXXXXX");
tmpFile = new QTemporaryFile(tmpl);
if (!tmpFile->open()) {
err = tr("Failed to create temporary file ") + tmpl + ": " + tmpFile->error();
return false;
}
// QTemporaryFile initially has permissions set to 0600.
// Make it readable by everyone.
tmpFile->setPermissions(tmpFile->permissions() | QFile::ReadOwner | QFile::ReadUser
| QFile::ReadGroup | QFile::ReadOther);
file.name = tmpFile->fileName();
file.startTime = line.timestamp;
os.setDevice(tmpFile);
os<<verString;
os<<"mm-dd,hh:mm:ss,SmO2 Live,SmO2 Averaged,THb,Lap\n";
}
// ok, so moxy csv has version string, header then line data
os<<line.line;
rows++;
last = line.timestamp;
}
// close last file
if (tmpFile) {
// does it have more than 30 rows?
if (rows > 30) {
// we have one file anyway
emit updateStatus(QString(tr("File #%1: %2")).arg(++nfiles).arg(file.startTime.toString("d-MMM-yy hh:mm")));
tmpFile->close();
files << file;
// free memory, but leave on disk
tmpFile->setAutoRemove(false);
delete tmpFile;
tmpFile = NULL;
} else {
// not enough data in this one
tmpFile->close();
tmpFile->setAutoRemove(true);
delete tmpFile;
tmpFile = NULL;
}
}
// exit
if (writeCommand(dev, "exit\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
}
// close device
dev->close();
if (files.count() > 0) {
emit updateProgress("Importing ...");
emit updateStatus(QString(tr("\nImporting %1 Ride(s)... \n")).arg(files.count()));
} else emit updateStatus(QString(tr("\nNo rides found to import. \n")));
// success !
return true;
}
bool
MoxyDevice::cleanup( QString &err )
{
int bytes=0;
char vbuf[256];
emit updateStatus(tr("Erase all records on Moxy"));
if (!dev->open(err)) {
err = tr("ERROR: open failed: ") + err;
return false;
}
dev->setBaudRate(115200, err);
// get into engineering mode
if (writeCommand(dev, "\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
}
if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) <= 0) {
dev->close();
return false;
}
if (writeCommand(dev, "\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
}
if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) <= 0) {
dev->close();
return false;
}
// now get a prompt
if (writeCommand(dev, "\r", err) == false) {
dev->close();
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
return false;
}
// get a prompt back ?
if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) <= 0) {
dev->close();
return false;
}
// now clear the entries
if (writeCommand(dev, "cd\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
} else if ((bytes=readUntilPrompt(dev, vbuf, 256, err)) > 0) {
vbuf[bytes-1] = '\0';
emit updateStatus(vbuf);
} else {
dev->close();
return false;
}
// give it some time to get the work done !
sleep(3);
// exit engineering mode
if (writeCommand(dev, "exit\r", err) == false) {
emit updateStatus(QString(tr("Write error: %1\n")).arg(err));
dev->close();
return false;
}
// close device
dev->close();
// success !
return true;
}
static bool
hasNewline(const char *buf, int len)
{
static char newline[] = { 0x0d, 0x0a };
if (len < 2)
return false;
for (int i = 0; i < len; ++i) {
bool success = true;
for (int j = 0; j < 2; ++j) {
if (buf[i+j] != newline[j]) {
success = false;
break;
}
}
if (success)
return true;
}
return false;
}
static QString
cEscape(const char *buf, int len)
{
char *result = new char[4 * len + 1];
char *tmp = result;
for (int i = 0; i < len; ++i) {
if (buf[i] == '"')
tmp += sprintf(tmp, "\\\"");
else if (isprint(buf[i]))
*(tmp++) = buf[i];
else
tmp += sprintf(tmp, "\\x%02x", 0xff & (unsigned) buf[i]);
}
return result;
}
// read the entire response after a command issued it ends with
// a command prompt
int
MoxyDevice::readUntilPrompt(CommPortPtr dev, char *buf, int len, QString &err)
{
int sofar = 0;
while (sofar < len) {
// Read one byte at a time to avoid waiting for timeout.
int n = dev->read(buf + sofar, 1, err);
if (n <= 0) {
err = (n < 0) ? (tr("read error: ") + err) : tr("read timeout");
err += QString(tr(", read %1 bytes so far: \"%2\""))
.arg(sofar).arg(sofar > 0 ? cEscape(buf, sofar) : "");
return -1;
}
sofar += n;
// we got our prompt
if (*(buf+sofar-1) == '>') break;
}
return sofar;
}
// just read whatever we get before the new line
int
MoxyDevice::readUntilNewline(CommPortPtr dev, char *buf, int len, QString &err)
{
int sofar = 0;
while (!hasNewline(buf, sofar) && sofar < len) {
// Read one byte at a time to avoid waiting for timeout.
int n = dev->read(buf + sofar, 1, err);
if (n <= 0) {
err = (n < 0) ? (tr("read error: ") + err) : tr("read timeout");
err += QString(tr(", read %1 bytes so far: \"%2\""))
.arg(sofar).arg(cEscape(buf, sofar));
return -1;
}
sofar += n;
}
return sofar;
}
// read a line of data, if it starts with a prompt ">" then its done
int
MoxyDevice::readData(CommPortPtr dev, char *buf, int len, QString &err)
{
int sofar = 0;
while (sofar < len) {
// Read one byte at a time to avoid waiting for timeout.
int n = dev->read(buf + sofar, 1, err);
if (n <= 0) {
err = (n < 0) ? (tr("read error: ") + err) : tr("read timeout");
err += QString(tr(", read %1 bytes so far: \"%2\""))
.arg(sofar).arg(cEscape(buf, sofar));
return -1;
}
sofar += n;
if (hasNewline(buf, sofar) || *buf == '>') {
break;
}
}
return sofar;
}
bool
MoxyDevice::writeCommand(CommPortPtr dev, const char *command, QString &err)
{
// on qt4 we need to waste some cycles
msleep(100);
int len = strlen(command);
int n = dev->write(const_cast<char*>(command), len, err);
if (n != len) {
if (n < 0)
err = QString(tr("failed to write '%1' to device: %2")).arg(const_cast<char*>(command)).arg(err);
else
err = QString(tr("timeout writing '%1' to device")).arg(const_cast<char*>(command));
return false;
}
return true;
}