/* * 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 #include #include 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 && tokens.count() !=7) return QDateTime(); // is the date in UTC Format ? // XXX we have no way of converting from UTC // XXX at present since we don't know what TZ // XXX the ride was recorded in ... bool UTCdate = tokens.count() == 7; Q_UNUSED(UTCdate); // 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 &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 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< 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(command), len, err); if (n != len) { if (n < 0) err = QString(tr("failed to write '%1' to device: %2")).arg(const_cast(command)).arg(err); else err = QString(tr("timeout writing '%1' to device")).arg(const_cast(command)); return false; } return true; }