mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-17 01:49:55 +00:00
.. works for download and clear .. is pretty basic as the downloaded data is treated as a single ride .. need to think about how we can split into rides and refactor the tool to create multiple rides with the user selecting which to download.
452 lines
12 KiB
C++
452 lines
12 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 <math.h>
|
|
#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 ));
|
|
}
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|
|
|
|
emit updateStatus(deviceInfo);
|
|
|
|
// 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 updateStatus(QString(tr("Downloading ... \n")));
|
|
|
|
QStringList data;
|
|
do {
|
|
if ((bytes=readData(dev, vbuf, 256, err)) > 0) {
|
|
vbuf[bytes] = '\0';
|
|
data<<vbuf;
|
|
} else break;
|
|
|
|
// don't hang gui !
|
|
QApplication::processEvents();
|
|
|
|
} while(vbuf[0] != '>');
|
|
|
|
// did we get anything of any value ?
|
|
if (data.count() < 30) {
|
|
|
|
emit updateStatus(QString("Less than a minutes worth of data, skipping."));
|
|
|
|
} else {
|
|
|
|
// lets create a csv file for import then
|
|
|
|
QString tmpl = tmpdir.absoluteFilePath(".mxdl.XXXXXX");
|
|
QTemporaryFile tmp(tmpl);
|
|
tmp.setAutoRemove(false);
|
|
if (!tmp.open()) {
|
|
err = tr("Failed to create temporary file ") + tmpl + ": " + tmp.error();
|
|
return false;
|
|
}
|
|
|
|
// QTemporaryFile initially has permissions set to 0600.
|
|
// Make it readable by everyone.
|
|
tmp.setPermissions(tmp.permissions() | QFile::ReadOwner | QFile::ReadUser
|
|
| QFile::ReadGroup | QFile::ReadOther);
|
|
|
|
DeviceDownloadFile file;
|
|
file.extension = "csv";
|
|
file.name = tmp.fileName();
|
|
|
|
// first row has data and time ?
|
|
QStringList tokens = data[2].split(",");
|
|
|
|
// we got something ?
|
|
if (tokens.count() != 6) {
|
|
|
|
// fall back to "now"
|
|
file.startTime = QDateTime::currentDateTime();
|
|
|
|
} else {
|
|
|
|
// 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();
|
|
|
|
file.startTime = QDateTime(QDate(year,month,day), QTime(hh,mm,ss));
|
|
}
|
|
|
|
QTextStream os(&tmp);
|
|
|
|
// ok, so moxy csv has version string, header then line data
|
|
os<<verString;
|
|
os<<"mm-dd,hh:mm:ss,SmO2 Live,SmO2 Averaged,THb,Lap";
|
|
foreach(QString line, data) os<<line;
|
|
|
|
// add to list
|
|
files << file;
|
|
|
|
}
|
|
|
|
// 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();
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
// 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;
|
|
}
|