mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
552 lines
15 KiB
C++
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;
|
|
}
|