From 10f9f29a147a99410160d0e86e2d39e5e3b48e90 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 11 Dec 2019 19:35:01 -0500 Subject: [PATCH 01/58] Fix an off by one error with signal sizes. --- dbc/dbcsignaleditor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dbc/dbcsignaleditor.cpp b/dbc/dbcsignaleditor.cpp index d2b592d..acaa55c 100644 --- a/dbc/dbcsignaleditor.cpp +++ b/dbc/dbcsignaleditor.cpp @@ -147,8 +147,8 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : if (currentSignal == nullptr) return; int temp; temp = Utility::ParseStringToNum(ui->txtBitLength->text()); - if (temp < 0) return; - if (temp > 63) return; + if (temp < 1) return; + if (temp > 64) return; if (currentSignal->valType != SP_FLOAT && currentSignal->valType != DP_FLOAT) currentSignal->signalSize = temp; fillSignalForm(currentSignal); From 0baffa300f6263e7129411a9c9b3b7b39c3a671c Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 11 Dec 2019 21:17:33 -0500 Subject: [PATCH 02/58] A partial commit that changes CANFrame over to be based on QCanBusFrame so that things are more compatible with other Qt based CAN bus code. Not complete yet but getting there. --- bisectwindow.cpp | 4 +- bus_protocols/isotp_handler.cpp | 115 ++++++++++++++-------------- bus_protocols/isotp_handler.h | 5 +- bus_protocols/isotp_message.h | 3 + can_structs.h | 23 +++--- connections/canconmanager.cpp | 4 +- connections/gvretserial.cpp | 81 ++++++++++---------- connections/gvretserial.h | 3 + connections/serialbusconnection.cpp | 37 ++++----- dbc/dbc_classes.cpp | 26 +++---- dbc/dbcmaineditor.cpp | 4 +- re/discretestatewindow.cpp | 12 +-- re/filecomparatorwindow.cpp | 48 ++++++------ re/flowviewwindow.cpp | 55 ++++++------- re/frameinfowindow.cpp | 26 +++---- re/fuzzingwindow.cpp | 18 ++--- re/graphingwindow.cpp | 24 +++--- re/rangestatewindow.cpp | 24 +++--- re/sniffer/snifferitem.cpp | 12 +-- re/sniffer/sniffermodel.cpp | 12 +-- re/temporalgraphwindow.cpp | 12 +-- utility.h | 2 +- 22 files changed, 272 insertions(+), 278 deletions(-) diff --git a/bisectwindow.cpp b/bisectwindow.cpp index 22d3e78..99a27ca 100644 --- a/bisectwindow.cpp +++ b/bisectwindow.cpp @@ -66,7 +66,7 @@ void BisectWindow::refreshIDList() int id; for (int i = 0; i < modelFrames->count(); i++) { - id = modelFrames->at(i).ID; + id = modelFrames->at(i).frameId(); if (!foundID.contains(id)) { foundID.append(id); @@ -129,7 +129,7 @@ void BisectWindow::handleCalculateButton() uint32_t upperID = Utility::ParseStringToNum2(ui->cbIDUpper->currentText()); for (int i = 0; i < modelFrames->count(); i++) { - if (modelFrames->at(i).ID >= lowerID && modelFrames->at(i).ID <= upperID) splitFrames.append(modelFrames->at(i)); + if (modelFrames->at(i).frameId() >= lowerID && modelFrames->at(i).frameId() <= upperID) splitFrames.append(modelFrames->at(i)); } } refreshFrameNumbers(); diff --git a/bus_protocols/isotp_handler.cpp b/bus_protocols/isotp_handler.cpp index db05824..9534b26 100644 --- a/bus_protocols/isotp_handler.cpp +++ b/bus_protocols/isotp_handler.cpp @@ -50,7 +50,7 @@ void ISOTP_HANDLER::setReception(bool mode) void ISOTP_HANDLER::sendISOTPFrame(int bus, int ID, QVector data) { CANFrame frame; - frame.remote = false; + frame.setFrameType(QCanBusFrame::DataFrame); int currByte = 0; int index = 0; if (bus < 0) return; @@ -61,27 +61,28 @@ void ISOTP_HANDLER::sendISOTPFrame(int bus, int ID, QVector data) if (data.length() < 8) { - frame.bus = bus; - frame.ID = ID; - if (ID > 0x7FF) frame.extended = true; - else frame.extended = false; - frame.len = 8; - for (int b = 0; b < 8; b++) frame.data[b] = 0x00; - frame.data[0] = data.length(); - for (int i = 0; i < frame.data[0]; i++) frame.data[i + 1] = data[i]; + frame.bus = bus; + frame.setFrameId(ID); + if (ID > 0x7FF) frame.setExtendedFrameFormat(true); + else frame.setExtendedFrameFormat(false); + QByteArray bytes(8,0); + bytes.resize(8); + bytes[0] = data.length(); + for (int i = 0; i < data.length(); i++) bytes[i + 1] = data[i]; + frame.setPayload(bytes); CANConManager::getInstance()->sendFrame(frame); } else //need to send a multi-part ISO_TP message - Respects timing and frame number based flow control { frame.bus = bus; - frame.ID = ID; - if (ID > 0x7FF) frame.extended = true; - else frame.extended = false; - frame.len = 8; - for (int b = 0; b < 8; b++) frame.data[b] = 0x00; - frame.data[0] = 0x10 + (data.length() / 256); - frame.data[1] = data.length() & 0xFF; - for (int i = 0; i < 6; i++) frame.data[2 + i] = data[currByte++]; + frame.setFrameId(ID); + if (ID > 0x7FF) frame.setExtendedFrameFormat(true); + else frame.setExtendedFrameFormat(false); + QByteArray bytes(8, 0); + bytes[0] = 0x10 + (data.length() / 256); + bytes[1] = data.length() & 0xFF; + for (int i = 0; i < 6; i++) bytes[2 + i] = data[currByte++]; + frame.setPayload(bytes); CANConManager::getInstance()->sendFrame(frame); //Queue up the rest of the frames waitingForFlow = true; @@ -90,13 +91,13 @@ void ISOTP_HANDLER::sendISOTPFrame(int bus, int ID, QVector data) frameTimer.start(); while (currByte < data.length()) { - for (int b = 0; b < 8; b++) frame.data[b] = 0x00; - frame.data[0] = 0x20 + index; + for (int b = 0; b < 8; b++) bytes[b] = 0x00; + bytes[0] = 0x20 + index; index = (index + 1) & 0xF; int bytesToGo = data.length() - currByte; if (bytesToGo > 7) bytesToGo = 7; - for (int i = 0; i < bytesToGo; i++) frame.data[1 + i] = data[currByte++]; - frame.len = 8; + for (int i = 0; i < bytesToGo; i++) bytes[1 + i] = data[currByte++]; + frame.setPayload(bytes); sendingFrames.append(frame); //CANConManager::getInstance()->sendFrame(frame); } @@ -138,7 +139,7 @@ void ISOTP_HANDLER::rapidFrames(const CANConnection* conn, const QVector> 4; - frameLen = frame.data[1] & 0xF; + ID += frame.payload()[0]; + frameType = frame.payload()[1] >> 4; + frameLen = frame.payload()[1] & 0xF; } else { - frameType = frame.data[0] >> 4; - frameLen = frame.data[0] & 0xF; + frameType = frame.payload()[0] >> 4; + frameLen = frame.payload()[0] & 0xF; } switch(frameType) @@ -185,42 +186,42 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) if (frameLen > 7) return; msg.bus = frame.bus; - msg.extended = frame.extended; + msg.extended = frame.hasExtendedFrameFormat(); msg.ID = ID; msg.isReceived = frame.isReceived; msg.len = frameLen; msg.data.reserve(frameLen); - msg.timestamp = frame.timestamp; + msg.timestamp = frame.timeStamp().microSeconds(); msg.isMultiframe = false; - if (useExtendedAddressing) for (int j = 0; j < frameLen; j++) msg.data.append(frame.data[j+2]); - else for (int j = 0; j < frameLen; j++) msg.data.append(frame.data[j+1]); + if (useExtendedAddressing) for (int j = 0; j < frameLen; j++) msg.data.append(frame.payload()[j+2]); + else for (int j = 0; j < frameLen; j++) msg.data.append(frame.payload()[j+1]); //qDebug() << "Emitting single frame ISOTP message"; emit newISOMessage(msg); break; case 1: //first frame of a multi-frame message checkNeedFlush(ID); msg.bus = frame.bus; - msg.extended = frame.extended; + msg.extended = frame.hasExtendedFrameFormat(); msg.ID = ID; - msg.timestamp = frame.timestamp; + msg.timestamp = frame.timeStamp().microSeconds(); msg.isReceived = frame.isReceived; msg.isMultiframe = true; frameLen = frameLen << 8; if (useExtendedAddressing) { - frameLen += frame.data[2]; + frameLen += frame.payload()[2]; frameLen = frameLen & 0xFFF; msg.len = frameLen; msg.data.reserve(frameLen); - for (int j = 0; j < 5; j++) msg.data.append(frame.data[3 + j]); + for (int j = 0; j < 5; j++) msg.data.append(frame.payload()[3 + j]); } else { - frameLen += frame.data[1]; + frameLen += frame.payload()[1]; frameLen = frameLen & 0xFFF; msg.len = frameLen; msg.data.reserve(frameLen); - for (int j = 0; j < 6; j++) msg.data.append(frame.data[2 + j]); + for (int j = 0; j < 6; j++) msg.data.append(frame.payload()[2 + j]); } msg.lastSequence = -1; messageBuffer.append(msg); @@ -230,13 +231,13 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) { CANFrame outFrame; outFrame.bus = lastSenderBus; - outFrame.extended = false; - outFrame.ID = lastSenderID; - outFrame.len = 8; - for (int b = 0; b < 8; b++) outFrame.data[b] = 0x00; - outFrame.data[0] = 0x30; //flow control, go ahead and send - outFrame.data[1] = 0; //dont ask again about flow control - outFrame.data[2] = 3; //separation time in milliseconds between messages. + outFrame.setExtendedFrameFormat(false); + outFrame.setFrameId(lastSenderID); + QByteArray bytes(8, 0); + bytes[0] = 0x30; //flow control, go ahead and send + bytes[1] = 0; //dont ask again about flow control + bytes[2] = 3; //separation time in milliseconds between messages. + outFrame.setPayload(bytes); CANConManager::getInstance()->sendFrame(outFrame); } break; @@ -257,12 +258,12 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) if (useExtendedAddressing) { if (ln > 6) ln = 6; - for (int j = 0; j < ln; j++) pMsg->data.append(frame.data[j+2]); + for (int j = 0; j < ln; j++) pMsg->data.append(frame.payload()[j+2]); } else { if (ln > 7) ln = 7; - for (int j = 0; j < ln; j++) pMsg->data.append(frame.data[j+1]); + for (int j = 0; j < ln; j++) pMsg->data.append(frame.payload()[j+1]); } if (pMsg->len <= pMsg->data.count()) { @@ -276,10 +277,10 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) case 0: //continue to send frames but maybe change inter-frame delay waitingForFlow = false; //data[1] contains number of frames to send before waiting for next flow control - framesUntilFlow = frame.data[1]; + framesUntilFlow = frame.payload()[1]; if (framesUntilFlow == 0) framesUntilFlow = -1; //-1 means don't count frames and just keep going //data[2] contains the interframe delay to use (0xF1 through 0xF9 are special through) - if (frame.data[2] < 0xF1) frameTimer.start(frame.data[2]); //set proper delay between frames + if (frame.payload()[2] < (char)0xF1) frameTimer.start(frame.payload()[2]); //set proper delay between frames else frameTimer.start(1); //can't do sub-millisecond sending with this code so just use 1ms timing break; case 1: //wait - do not send any more frames until other side says so @@ -307,14 +308,14 @@ void ISOTP_HANDLER::checkNeedFlush(uint64_t ID) //used to pass by reference but now newISOMessage should pass by value which makes it easier to use cross thread if (messageBuffer[i].ID > 0x600 && messageBuffer[i].ID < 0x630) { - if (messageBuffer[i].len <= messageBuffer[i].data.count()) - { - qDebug() << "Flushing full frame" << QString::number(messageBuffer[i].ID, 16) << " " << messageBuffer[i].len << " " << messageBuffer[i].data.count(); - } - else - { - qDebug() << "Flushing a partial frame " << QString::number(messageBuffer[i].ID, 16) << " " << messageBuffer[i].len << " " << messageBuffer[i].data.count(); - } + if (messageBuffer[i].len <= messageBuffer[i].data.count()) + { + qDebug() << "Flushing full frame" << QString::number(messageBuffer[i].ID, 16) << " " << messageBuffer[i].len << " " << messageBuffer[i].data.count(); + } + else + { + qDebug() << "Flushing a partial frame " << QString::number(messageBuffer[i].ID, 16) << " " << messageBuffer[i].len << " " << messageBuffer[i].data.count(); + } } emit newISOMessage(messageBuffer[i]); messageBuffer.removeAt(i); diff --git a/bus_protocols/isotp_handler.h b/bus_protocols/isotp_handler.h index 611f63a..7cdc16d 100644 --- a/bus_protocols/isotp_handler.h +++ b/bus_protocols/isotp_handler.h @@ -1,5 +1,4 @@ -#ifndef ISOTP_HANDLER_H -#define ISOTP_HANDLER_H +#pragma once #include #include @@ -53,5 +52,3 @@ private: void processFrame(const CANFrame &frame); void checkNeedFlush(uint64_t ID); }; - -#endif // ISOTP_HANDLER_H diff --git a/bus_protocols/isotp_message.h b/bus_protocols/isotp_message.h index f2d4c85..5e59580 100644 --- a/bus_protocols/isotp_message.h +++ b/bus_protocols/isotp_message.h @@ -1,6 +1,9 @@ #ifndef ISOTP_MESSAGE_H #define ISOTP_MESSAGE_H +#include +#include + //the same as the CANFrame struct but with arbitrary data size. class ISOTP_MESSAGE { diff --git a/can_structs.h b/can_structs.h index d0b3063..6b8cdee 100644 --- a/can_structs.h +++ b/can_structs.h @@ -4,35 +4,32 @@ #include #include #include +#include -struct CANFrame +//Now inherits from the built-in CAN frame class from Qt. This should be more future proof and easier to integrate with other code + +struct CANFrame : public QCanBusFrame { public: - uint32_t ID; int bus; - bool extended; - bool remote; bool isReceived; //did we receive this or send it? - int len; - unsigned char data[8]; - uint64_t timestamp; uint64_t timedelta; uint32_t frameCount; //used in overwrite mode friend bool operator<(const CANFrame& l, const CANFrame& r) { - return l.timestamp < r.timestamp; + qint64 lStamp = l.timeStamp().seconds() * 1000000 + l.timeStamp().microSeconds(); + qint64 rStamp = r.timeStamp().seconds() * 1000000 + r.timeStamp().microSeconds(); + return lStamp < rStamp; } CANFrame() { - ID = 0; + setFrameId(0); bus = 0; - extended = false; - remote = false; + setExtendedFrameFormat(false); + setFrameType(QCanBusFrame::DataFrame); isReceived = true; - len = 0; - timestamp = 0; timedelta = 0; frameCount = 1; } diff --git a/connections/canconmanager.cpp b/connections/canconmanager.cpp index 0154c03..9ae1f11 100644 --- a/connections/canconmanager.cpp +++ b/connections/canconmanager.cpp @@ -220,11 +220,11 @@ bool CANConManager::sendFrame(const CANFrame& pFrame) workingFrame.isReceived = false; if (useSystemTime) { - workingFrame.timestamp = (QDateTime::currentMSecsSinceEpoch() * 1000); + workingFrame.setTimeStamp(QCanBusFrame::TimeStamp(0,QDateTime::currentMSecsSinceEpoch() * 1000)); } else { - workingFrame.timestamp = mElapsedTimer.nsecsElapsed() / 1000; + workingFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, mElapsedTimer.nsecsElapsed() / 1000)); //workingFrame.timestamp -= mTimestampBasis; } txFrame = conn->getQueue().get(); diff --git a/connections/gvretserial.cpp b/connections/gvretserial.cpp index 523c3a5..11708a2 100644 --- a/connections/gvretserial.cpp +++ b/connections/gvretserial.cpp @@ -246,11 +246,11 @@ bool GVRetSerial::piSendFrame(const CANFrame& frame) // Doesn't make sense to send an error frame // to an adapter - if (frame.ID & 0x20000000) { + if (frame.frameId() & 0x20000000) { return true; } - ID = frame.ID; - if (frame.extended) ID |= 1 << 31; + ID = frame.frameId(); + if (frame.hasExtendedFrameFormat()) ID |= 1 << 31; buffer[0] = (unsigned char)0xF1; //start of a command over serial buffer[1] = 0; //command ID for sending a CANBUS frame @@ -259,12 +259,12 @@ bool GVRetSerial::piSendFrame(const CANFrame& frame) buffer[4] = (unsigned char)(ID >> 16); buffer[5] = (unsigned char)(ID >> 24); buffer[6] = (unsigned char)((frame.bus) & 3); - buffer[7] = (unsigned char)frame.len; - for (c = 0; c < frame.len; c++) + buffer[7] = (unsigned char)frame.payload().length(); + for (c = 0; c < frame.payload().length(); c++) { - buffer[8 + c] = frame.data[c]; + buffer[8 + c] = frame.payload()[c]; } - buffer[8 + frame.len] = 0; + buffer[8 + frame.payload().length()] = 0; sendToSerial(buffer); @@ -375,18 +375,18 @@ void GVRetSerial::deviceConnected() { sendDebug("Connecting to GVRET Device!"); QByteArray output; - output.append((unsigned char)0xE7); //this puts the device into binary comm mode - output.append((unsigned char)0xE7); + output.append((char)0xE7); //this puts the device into binary comm mode + output.append((char)0xE7); - output.append((unsigned char)0xF1); - output.append((unsigned char)0x0C); //get number of actually implemented buses. Not implemented except on M2RET + output.append((char)0xF1); + output.append((char)0x0C); //get number of actually implemented buses. Not implemented except on M2RET mNumBuses = 2; //the proper number if C/12 is not implemented - output.append((unsigned char)0xF1); //signal we want to issue a command - output.append((unsigned char)0x06); //request canbus stats from the board + output.append((char)0xF1); //signal we want to issue a command + output.append((char)0x06); //request canbus stats from the board - output.append((unsigned char)0xF1); //another command to the GVRET - output.append((unsigned char)0x07); //request device information + output.append((char)0xF1); //another command to the GVRET + output.append((char)0x07); //request device information /*output.append((char)0xF1); output.append((char)0x08); //setting singlewire mode @@ -399,11 +399,11 @@ void GVRetSerial::deviceConnected() output.append((char)0xFF); //signal we don't want single wire mode }*/ - output.append((unsigned char)0xF1); //and another command - output.append((unsigned char)0x01); //Time Sync - Not implemented until 333 but we can try + output.append((char)0xF1); //and another command + output.append((char)0x01); //Time Sync - Not implemented until 333 but we can try - output.append((unsigned char)0xF1); //yet another command - output.append((unsigned char)0x09); //comm validation command + output.append((char)0xF1); //yet another command + output.append((char)0x09); //comm validation command continuousTimeSync = true; @@ -659,57 +659,59 @@ void GVRetSerial::procRXChar(unsigned char c) switch (rx_step) { case 0: - buildFrame.timestamp = c; + buildTimestamp = c; break; case 1: - buildFrame.timestamp |= (uint)(c << 8); + buildTimestamp |= (uint)(c << 8); break; case 2: - buildFrame.timestamp |= (uint)c << 16; + buildTimestamp |= (uint)c << 16; break; case 3: - buildFrame.timestamp |= (uint)c << 24; + buildTimestamp |= (uint)c << 24; - buildFrame.timestamp += timeBasis; + buildTimestamp += timeBasis; if (useSystemTime) { - buildFrame.timestamp = QDateTime::currentMSecsSinceEpoch() * 1000l; + buildTimestamp = QDateTime::currentMSecsSinceEpoch() * 1000l; } + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); break; case 4: - buildFrame.ID = c; + buildId = c; break; case 5: - buildFrame.ID |= c << 8; + buildId |= c << 8; break; case 6: - buildFrame.ID |= c << 16; + buildId |= c << 16; break; case 7: - buildFrame.ID |= c << 24; - if ((buildFrame.ID & 1 << 31) == 1u << 31) + buildId |= c << 24; + if ((buildId & 1 << 31) == 1u << 31) { - buildFrame.ID &= 0x7FFFFFFF; - buildFrame.extended = true; + buildId &= 0x7FFFFFFF; + buildFrame.setExtendedFrameFormat(true); } - else buildFrame.extended = false; + else buildFrame.setExtendedFrameFormat(false); + buildFrame.setFrameId(buildId); break; case 8: - buildFrame.len = c & 0xF; - if (buildFrame.len > 8) buildFrame.len = 8; + buildData.resize(c & 0xF); buildFrame.bus = (c & 0xF0) >> 4; break; default: - if (rx_step < buildFrame.len + 9) + if (rx_step < buildData.length() + 9) { - buildFrame.data[rx_step - 9] = c; + buildData[rx_step - 9] = c; } else { rx_state = IDLE; rx_step = 0; buildFrame.isReceived = true; - + buildFrame.setPayload(buildData); + buildFrame.setFrameType(QCanBusFrame::FrameType::DataFrame); if (!isCapSuspended()) { /* get frame from queue */ @@ -717,8 +719,7 @@ void GVRetSerial::procRXChar(unsigned char c) if(frame_p) { //qDebug() << "GVRET got frame on bus " << frame_p->bus; /* copy frame */ - *frame_p = buildFrame; - frame_p->remote = false; + *frame_p = buildFrame; checkTargettedFrame(buildFrame); /* enqueue frame */ getQueue().queue(); diff --git a/connections/gvretserial.h b/connections/gvretserial.h index dba664f..3df1350 100644 --- a/connections/gvretserial.h +++ b/connections/gvretserial.h @@ -93,6 +93,9 @@ protected: STATE rx_state; uint32_t rx_step; CANFrame buildFrame; + qint64 buildTimestamp; + quint32 buildId; + QByteArray buildData; int can0Baud, can1Baud, swcanBaud, lin1Baud, lin2Baud; bool can0Enabled, can1Enabled, swcanEnabled, lin1Enabled, lin2Enabled; bool can0ListenOnly, can1ListenOnly, swcanListenOnly; diff --git a/connections/serialbusconnection.cpp b/connections/serialbusconnection.cpp index 6fa35f2..72e1e90 100644 --- a/connections/serialbusconnection.cpp +++ b/connections/serialbusconnection.cpp @@ -115,22 +115,11 @@ void SerialBusConnection::piSetBusSettings(int pBusIdx, CANBus bus) bool SerialBusConnection::piSendFrame(const CANFrame& pFrame) { /* sanity checks */ - if(0 != pFrame.bus || pFrame.len>8) + if(0 != pFrame.bus /*|| pFrame.len>8*/) return false; if (!mDev_p) return false; - /* fill frame */ - QCanBusFrame frame; - frame.setExtendedFrameFormat(pFrame.extended); - frame.setFrameId(pFrame.ID); - if (pFrame.remote) { - frame.setFrameType(QCanBusFrame::FrameType::RemoteRequestFrame); - } else { - frame.setFrameType(QCanBusFrame::FrameType::DataFrame); - } - frame.setPayload(QByteArray(reinterpret_cast(pFrame.data), - static_cast(pFrame.len))); - return mDev_p->writeFrame(frame); + return mDev_p->writeFrame(pFrame); } @@ -193,14 +182,16 @@ void SerialBusConnection::framesReceived() if (recFrame.payload().length() <= 8) { CANFrame* frame_p = getQueue().get(); if(frame_p) { - frame_p->len = static_cast(recFrame.payload().length()); - frame_p->bus = 0; + frame_p->setPayload(recFrame.payload()); + frame_p->bus = 0; if (recFrame.frameType() == QCanBusFrame::ErrorFrame) { // Constants defined in include/uapi/linux/can/error.h + //since the whole program uses QCanBusFrame now we can just grab errors directly out of the proper place from now on + /* switch (recFrame.error()) { case QCanBusFrame::TransmissionTimeoutError: - frame_p->ID = 0x20000001; + frame_p->setFrameId(0x20000001); break; case QCanBusFrame::LostArbitrationError: frame_p->ID = 0x20000002; @@ -230,19 +221,21 @@ void SerialBusConnection::framesReceived() break; } frame_p->extended = true; + */ } else { - frame_p->extended = recFrame.hasExtendedFrameFormat(); - frame_p->ID = recFrame.frameId(); - frame_p->remote = (recFrame.frameType() == recFrame.RemoteRequestFrame); - memcpy(frame_p->data, recFrame.payload().data(), frame_p->len); + frame_p->setExtendedFrameFormat(recFrame.hasExtendedFrameFormat()); + frame_p->setFrameId(recFrame.frameId()); + frame_p->setTimeStamp(recFrame.timeStamp()); + frame_p->setFrameType(recFrame.frameType()); + frame_p->setError(recFrame.error()); frame_p->isReceived = true; } frame_p->isReceived = true; if (useSystemTime) { - frame_p->timestamp = QDateTime::currentMSecsSinceEpoch() * 1000ul; + frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, QDateTime::currentMSecsSinceEpoch() * 1000ul)); } - else frame_p->timestamp = (recFrame.timeStamp().seconds() * 1000000ul + recFrame.timeStamp().microSeconds()) - timeBasis; + else frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, (recFrame.timeStamp().seconds() * 1000000ul + recFrame.timeStamp().microSeconds()) - timeBasis)); checkTargettedFrame(*frame_p); diff --git a/dbc/dbc_classes.cpp b/dbc/dbc_classes.cpp index f692f7e..003ea52 100644 --- a/dbc/dbc_classes.cpp +++ b/dbc/dbc_classes.cpp @@ -47,7 +47,7 @@ bool DBC_SIGNAL::processAsText(const CANFrame &frame, QString &outString, bool o QString buildString; int startByte = startBit / 8; int bytes = signalSize / 8; - for (int x = 0; x < bytes; x++) buildString.append(frame.data[startByte + x]); + for (int x = 0; x < bytes; x++) buildString.append((char)frame.payload()[startByte + x]); outString = buildString; cachedValue = outString; return true; @@ -68,7 +68,7 @@ bool DBC_SIGNAL::processAsText(const CANFrame &frame, QString &outString, bool o if (valType == SIGNED_INT) isSigned = true; if (valType == SIGNED_INT || valType == UNSIGNED_INT) { - result = Utility::processIntegerSignal(frame.data, startBit, signalSize, intelByteOrder, isSigned); + result = Utility::processIntegerSignal(frame.payload(), startBit, signalSize, intelByteOrder, isSigned); endResult = ((double)result * factor) + bias; result = (int64_t)endResult; } @@ -79,19 +79,19 @@ bool DBC_SIGNAL::processAsText(const CANFrame &frame, QString &outString, bool o //that the bytes that make up the integer are instead treated as having made up //a 32 bit single precision float. That's evil incarnate but it is very fast and small //in terms of new code. - result = Utility::processIntegerSignal(frame.data, startBit, 32, intelByteOrder, false); + result = Utility::processIntegerSignal(frame.payload(), startBit, 32, intelByteOrder, false); endResult = (*((float *)(&result)) * factor) + bias; } else //double precision float { - if ( frame.len < 8 ) + if ( frame.payload().length() < 8 ) { result = 0; return false; } //like the above, this is rotten and evil and wrong in so many ways. Force //calculation of a 64 bit integer and then cast it into a double. - result = Utility::processIntegerSignal(frame.data, 0, 64, intelByteOrder, false); + result = Utility::processIntegerSignal(frame.payload(), 0, 64, intelByteOrder, false); endResult = (*((double *)(&result)) * factor) + bias; } @@ -155,13 +155,13 @@ bool DBC_SIGNAL::processAsInt(const CANFrame &frame, int32_t &outValue) } if (valType == SIGNED_INT) isSigned = true; - if ( static_cast(frame.len * 8) < (startBit + signalSize) ) + if ( static_cast(frame.payload().length() * 8) < (startBit + signalSize) ) { result = 0; return false; } - result = static_cast(Utility::processIntegerSignal(frame.data, startBit, signalSize, intelByteOrder, isSigned)); + result = static_cast(Utility::processIntegerSignal(frame.payload(), startBit, signalSize, intelByteOrder, isSigned)); double endResult = (result * factor) + bias; result = static_cast(endResult); @@ -200,19 +200,19 @@ bool DBC_SIGNAL::processAsDouble(const CANFrame &frame, double &outValue) if (valType == SIGNED_INT) isSigned = true; if (valType == SIGNED_INT || valType == UNSIGNED_INT) { - if ( frame.len*8 < (startBit+signalSize) ) + if ( frame.payload().length() * 8 < (startBit+signalSize) ) { result = 0; return false; } - result = Utility::processIntegerSignal(frame.data, startBit, signalSize, intelByteOrder, isSigned); + result = Utility::processIntegerSignal(frame.payload(), startBit, signalSize, intelByteOrder, isSigned); endResult = ((double)result * factor) + bias; result = (int64_t)endResult; } /*TODO: It should be noted that the below floating point has not even been tested. For shame! Test it!*/ else if (valType == SP_FLOAT) { - if ( frame.len*8 < (startBit+32) ) + if ( frame.payload().length() * 8 < (startBit + 32) ) { result = 0; return false; @@ -222,19 +222,19 @@ bool DBC_SIGNAL::processAsDouble(const CANFrame &frame, double &outValue) //that the bytes that make up the integer are instead treated as having made up //a 32 bit single precision float. That's evil incarnate but it is very fast and small //in terms of new code. - result = Utility::processIntegerSignal(frame.data, startBit, 32, false, false); + result = Utility::processIntegerSignal(frame.payload(), startBit, 32, false, false); endResult = (*((float *)(&result)) * factor) + bias; } else //double precision float { - if ( frame.len < 8 ) + if ( frame.payload().length() < 8 ) { result = 0; return false; } //like the above, this is rotten and evil and wrong in so many ways. Force //calculation of a 64 bit integer and then cast it into a double. - result = Utility::processIntegerSignal(frame.data, 0, 64, false, false); + result = Utility::processIntegerSignal(frame.payload(), 0, 64, false, false); endResult = (*((double *)(&result)) * factor) + bias; } cachedValue = endResult; diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index f84b36d..5ae5c1e 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -294,9 +294,9 @@ void DBCMainEditor::onCellChangedMessage(int row,int col) for (int i = 0; i < referenceFrames->length(); i++) { - if ((uint) referenceFrames->at(i).ID == msgID) + if ((uint) referenceFrames->at(i).frameId() == msgID) { - newMsg.len = referenceFrames->at(i).len; + newMsg.len = referenceFrames->at(i).payload().length(); break; } } diff --git a/re/discretestatewindow.cpp b/re/discretestatewindow.cpp index ddf5f1d..a4a0ba3 100644 --- a/re/discretestatewindow.cpp +++ b/re/discretestatewindow.cpp @@ -140,10 +140,10 @@ void DiscreteStateWindow::updatedFrames(int numFrames) { thisFrame = modelFrames->at(i); - if (!idFilters.contains(thisFrame.ID)) + if (!idFilters.contains(thisFrame.frameId())) { - idFilters.insert(thisFrame.ID, true); - QListWidgetItem* listItem = new QListWidgetItem(Utility::formatCANID(thisFrame.ID, thisFrame.extended), ui->listID); + idFilters.insert(thisFrame.frameId(), true); + QListWidgetItem* listItem = new QListWidgetItem(Utility::formatCANID(thisFrame.frameId(), thisFrame.hasExtendedFrameFormat()), ui->listID); listItem->setFlags(listItem->flags() | Qt::ItemIsUserCheckable); // set checkable flag listItem->setCheckState(Qt::Checked); //default all filters to be set active } @@ -164,11 +164,11 @@ void DiscreteStateWindow::refreshFilterList() for (int i = 0; i < modelFrames->length(); i++) { CANFrame thisFrame = modelFrames->at(i); - id = thisFrame.ID; + id = thisFrame.frameId(); if (!idFilters.contains(id)) { idFilters.insert(id, true); - QListWidgetItem* listItem = new QListWidgetItem(Utility::formatCANID(id, thisFrame.extended), ui->listID); + QListWidgetItem* listItem = new QListWidgetItem(Utility::formatCANID(id, thisFrame.hasExtendedFrameFormat()), ui->listID); listItem->setFlags(listItem->flags() | Qt::ItemIsUserCheckable); // set checkable flag listItem->setCheckState(Qt::Checked); //default all filters to be set active } @@ -359,7 +359,7 @@ void DiscreteStateWindow::calculateResults() frameCache.clear(); for (int i = 0; i < modelFrames->count(); i++) { - if (modelFrames->at(i).ID == (unsigned int)it.key()) frameCache.append(modelFrames->at(i)); + if (modelFrames->at(i).frameId() == (unsigned int)it.key()) frameCache.append(modelFrames->at(i)); } for (int bits = maxBits; bits >= minBits; bits--) { diff --git a/re/filecomparatorwindow.cpp b/re/filecomparatorwindow.cpp index 3efcfea..e565de7 100644 --- a/re/filecomparatorwindow.cpp +++ b/re/filecomparatorwindow.cpp @@ -152,22 +152,22 @@ void FileComparatorWindow::calculateDetails() for (int x = 0; x < interestedFrames.count(); x++) { CANFrame frame = interestedFrames.at(x); - if (interestedIDs.contains(frame.ID)) //if we saw this ID before then add to the QList in there + if (interestedIDs.contains(frame.frameId())) //if we saw this ID before then add to the QList in there { - for (unsigned int y = 0; y < frame.len; y++) + for (unsigned int y = 0; y < frame.payload().length(); y++) { - interestedIDs[frame.ID].values[y][frame.data[y]]++; - tmp = frame.data[y]; + interestedIDs[frame.frameId()].values[y][frame.payload()[y]]++; + tmp = frame.payload()[y]; tmp = tmp << (8 * y); - interestedIDs[frame.ID].bitmap |= tmp; - //qDebug() << "bitmap: " << QString::number(interestedIDs[frame.ID].bitmap, 16); + interestedIDs[frame.frameId()].bitmap |= tmp; + //qDebug() << "bitmap: " << QString::number(interestedIDs[frame.frameId()].bitmap, 16); } } else //never seen this ID before so add one { FrameData *newData = new FrameData(); - newData->ID = frame.ID; - newData->dataLen = frame.len; + newData->ID = frame.frameId(); + newData->dataLen = frame.payload().length(); //it would be possible to implement a constructor for FrameData //that sets the bitmap and values to zero. That would be cleaner and better. newData->bitmap = 0; @@ -179,15 +179,15 @@ void FileComparatorWindow::calculateDetails() } } //memset(newData->values, 0, 256 * 8); - for (unsigned int y = 0; y < frame.len; y++) + for (unsigned int y = 0; y < frame.payload().length(); y++) { - newData->values[y][frame.data[y]] = 1; - tmp = frame.data[y]; + newData->values[y][frame.payload()[y]] = 1; + tmp = frame.payload()[y]; tmp = tmp << (8 * y); newData->bitmap |= tmp; //qDebug() << "bitmap: " << QString::number(newData->bitmap, 16); } - interestedIDs.insert(frame.ID, *newData); + interestedIDs.insert(frame.frameId(), *newData); } } @@ -196,22 +196,22 @@ void FileComparatorWindow::calculateDetails() for (int x = 0; x < referenceFrames.count(); x++) { CANFrame frame = referenceFrames.at(x); - if (referenceIDs.contains(frame.ID)) //if we saw this ID before then add to the QList in there + if (referenceIDs.contains(frame.frameId())) //if we saw this ID before then add to the QList in there { - for (unsigned int y = 0; y < frame.len; y++) + for (unsigned int y = 0; y < frame.payload().length(); y++) { - referenceIDs[frame.ID].values[y][frame.data[y]]++; - tmp = frame.data[y]; + referenceIDs[frame.frameId()].values[y][frame.payload()[y]]++; + tmp = frame.payload()[y]; tmp = tmp << (8 * y); - referenceIDs[frame.ID].bitmap |= tmp; - //qDebug() << "bitmap: " << QString::number(referenceIDs[frame.ID].bitmap, 16); + referenceIDs[frame.frameId()].bitmap |= tmp; + //qDebug() << "bitmap: " << QString::number(referenceIDs[frame.frameId()].bitmap, 16); } } else //never seen this ID before so add one { FrameData *newData = new FrameData(); - newData->ID = frame.ID; - newData->dataLen = frame.len; + newData->ID = frame.frameId(); + newData->dataLen = frame.payload().length(); newData->bitmap = 0; for (int x = 0; x < 8; x++) { @@ -221,15 +221,15 @@ void FileComparatorWindow::calculateDetails() } } //memset(newData->values, 0, 256 * 8); - for (unsigned int y = 0; y < frame.len; y++) + for (unsigned int y = 0; y < frame.payload().length(); y++) { - newData->values[y][frame.data[y]] = 1; - tmp = frame.data[y]; + newData->values[y][frame.payload()[y]] = 1; + tmp = frame.payload()[y]; tmp = tmp << (8 * y); newData->bitmap |= tmp; //qDebug() << "bitmap: " << QString::number(newData->bitmap, 16); } - referenceIDs.insert(frame.ID, *newData); + referenceIDs.insert(frame.frameId(), *newData); } } diff --git a/re/flowviewwindow.cpp b/re/flowviewwindow.cpp index c3ec0c0..dcd627a 100644 --- a/re/flowviewwindow.cpp +++ b/re/flowviewwindow.cpp @@ -250,7 +250,7 @@ void FlowViewWindow::plottableDoubleClick(QCPAbstractPlottable* plottable, QMous int id = 0; //apply transforms to get the X axis value where we double clicked double coord = plottable->keyAxis()->pixelToCoord(event->localPos().x()); - if (frameCache.count() > 0) id = frameCache[0].ID; + if (frameCache.count() > 0) id = frameCache[0].frameId(); if (secondsMode) emit sendCenterTimeID(id, coord); else emit sendCenterTimeID(id, coord / 1000000.0); } @@ -278,7 +278,7 @@ void FlowViewWindow::gotCenterTimeID(int32_t ID, double timestamp) int bestIdx = -1; for (int i = 0; i < frameCache.count(); i++) { - if (frameCache[i].timestamp > t_stamp) + if (frameCache[i].timeStamp().microSeconds() > t_stamp) { bestIdx = i - 1; break; @@ -428,20 +428,20 @@ void FlowViewWindow::updatedFrames(int numFrames) { if (numFrames > modelFrames->count()) return; unsigned int refID; - if (frameCache.count() > 0) refID = frameCache[0].ID; + if (frameCache.count() > 0) refID = frameCache[0].frameId(); else refID = 0; bool needRefresh = false; for (int i = modelFrames->count() - numFrames; i < modelFrames->count(); i++) { thisFrame = modelFrames->at(i); - if (!foundID.contains(thisFrame.ID)) + if (!foundID.contains(thisFrame.frameId())) { - foundID.append(thisFrame.ID); - FilterUtility::createFilterItem(thisFrame.ID, ui->listFrameID); + foundID.append(thisFrame.frameId()); + FilterUtility::createFilterItem(thisFrame.frameId(), ui->listFrameID); } - if (thisFrame.ID == refID) + if (thisFrame.frameId() == refID) { frameCache.append(thisFrame); @@ -450,18 +450,18 @@ void FlowViewWindow::updatedFrames(int numFrames) if (ui->cbTimeGraph->isChecked()) { if (secondsMode){ - newX[k].append((double)(thisFrame.timestamp) / 1000000.0); + newX[k].append((double)(thisFrame.timeStamp().microSeconds()) / 1000000.0); } else { - newX[k].append(thisFrame.timestamp); + newX[k].append(thisFrame.timeStamp().microSeconds()); } } else { newX[k].append(x[k].count()); } - newY[k].append(thisFrame.data[k]); + newY[k].append(thisFrame.payload()[k]); needRefresh = true; } } @@ -469,7 +469,7 @@ void FlowViewWindow::updatedFrames(int numFrames) if (ui->cbLiveMode->checkState() == Qt::Checked) { currentPosition = frameCache.count() - 1; - memcpy(currBytes, frameCache.at(currentPosition).data, 8); + memcpy(currBytes, frameCache.at(currentPosition).payload(), 8); memcpy(refBytes, currBytes, 8); } @@ -482,7 +482,7 @@ void FlowViewWindow::updatedFrames(int numFrames) } ui->graphView->replot(); updateDataView(); - if (ui->cbSync->checkState() == Qt::Checked) emit sendCenterTimeID(frameCache[currentPosition].ID, frameCache[currentPosition].timestamp / 1000000.0); + if (ui->cbSync->checkState() == Qt::Checked) emit sendCenterTimeID(frameCache[currentPosition].frameId(), frameCache[currentPosition].timeStamp().microSeconds() / 1000000.0); } } updateFrameLabel(); @@ -512,16 +512,16 @@ void FlowViewWindow::createGraph(int byteNum) for (int j = 0; j < numEntries; j++) { - tempVal = frameCache[j].data[byteNum]; + tempVal = frameCache[j].payload()[byteNum]; if (graphByTime) { if (secondsMode){ - x[byteNum][j] = (double)(frameCache[j].timestamp) / 1000000.0; + x[byteNum][j] = (double)(frameCache[j].timeStamp().microSeconds()) / 1000000.0; } else { - x[byteNum][j] = frameCache[j].timestamp; + x[byteNum][j] = frameCache[j].timeStamp().microSeconds(); } } else @@ -551,7 +551,7 @@ void FlowViewWindow::refreshIDList() for (int i = 0; i < modelFrames->count(); i++) { CANFrame thisFrame = modelFrames->at(i); - id = thisFrame.ID; + id = thisFrame.frameId(); if (!foundID.contains(id)) { foundID.append(id); @@ -581,9 +581,9 @@ void FlowViewWindow::changeID(QString newID) for (int x = 0; x < modelFrames->count(); x++) { CANFrame thisFrame = modelFrames->at(x); - if (thisFrame.ID == id) + if (thisFrame.frameId() == id) { - for (int j = thisFrame.len; j < 8; j++) thisFrame.data[j] = 0; + thisFrame.payload().clear(); frameCache.append(thisFrame); } } @@ -600,7 +600,7 @@ void FlowViewWindow::changeID(QString newID) updateGraphLocation(); - memcpy(currBytes, frameCache.at(currentPosition).data, 8); + memcpy(currBytes, frameCache.at(currentPosition).payload(), 8); memcpy(refBytes, currBytes, 8); updateDataView(); @@ -635,7 +635,8 @@ void FlowViewWindow::btnStopClick() playbackActive = false; currentPosition = 0; - memcpy(currBytes, frameCache.at(currentPosition).data, 8); + + memcpy(currBytes, frameCache.at(currentPosition).payload().data_ptr(), 8); memcpy(refBytes, currBytes, 8); updateFrameLabel(); @@ -757,7 +758,7 @@ void FlowViewWindow::updatePosition(bool forward) uint8_t cngByte; for (int i = 0; i < 8; i++) { - cngByte = currBytes[i] ^ frameCache.at(currentPosition).data[i]; + cngByte = currBytes[i] ^ frameCache.at(currentPosition).payload()[i]; changedBits |= (uint64_t)cngByte << (8ull * i); } @@ -771,9 +772,9 @@ void FlowViewWindow::updatePosition(bool forward) playbackTimer->stop(); } - memcpy(currBytes, frameCache.at(currentPosition).data, 8); + memcpy(currBytes, frameCache.at(currentPosition).payload().data(), 8); - if (ui->cbSync->checkState() == Qt::Checked) emit sendCenterTimeID(frameCache[currentPosition].ID, frameCache[currentPosition].timestamp / 1000000.0); + if (ui->cbSync->checkState() == Qt::Checked) emit sendCenterTimeID(frameCache[currentPosition].frameId(), frameCache[currentPosition].timeStamp().microSeconds() / 1000000.0); } void FlowViewWindow::updateGraphLocation() @@ -787,9 +788,9 @@ void FlowViewWindow::updateGraphLocation() { if (secondsMode) { - ui->graphView->xAxis->setRange(frameCache[start].timestamp / 1000000.0, frameCache[end].timestamp / 1000000.0); + ui->graphView->xAxis->setRange(frameCache[start].timeStamp().microSeconds() / 1000000.0, frameCache[end].timeStamp().microSeconds() / 1000000.0); /* - ui->graphView->xAxis->setTickStep((frameCache[end].timestamp - frameCache[start].timestamp)/ 3000000.0); + ui->graphView->xAxis->setTickStep((frameCache[end].timeStamp().microSeconds() - frameCache[start].timeStamp().microSeconds())/ 3000000.0); ui->graphView->xAxis->setSubTickCount(0); ui->graphView->xAxis->setNumberFormat("f"); ui->graphView->xAxis->setNumberPrecision(6); @@ -797,9 +798,9 @@ void FlowViewWindow::updateGraphLocation() } else { - ui->graphView->xAxis->setRange(frameCache[start].timestamp, frameCache[end].timestamp); + ui->graphView->xAxis->setRange(frameCache[start].timeStamp().microSeconds(), frameCache[end].timeStamp().microSeconds()); /* - ui->graphView->xAxis->setTickStep((frameCache[end].timestamp - frameCache[start].timestamp)/ 3.0); + ui->graphView->xAxis->setTickStep((frameCache[end].timeStamp().microSeconds() - frameCache[start].timeStamp().microSeconds())/ 3.0); ui->graphView->xAxis->setSubTickCount(0); ui->graphView->xAxis->setNumberFormat("f"); ui->graphView->xAxis->setNumberPrecision(0); */ diff --git a/re/frameinfowindow.cpp b/re/frameinfowindow.cpp index 894e6f8..3410565 100644 --- a/re/frameinfowindow.cpp +++ b/re/frameinfowindow.cpp @@ -211,14 +211,14 @@ void FrameInfoWindow::updatedFrames(int numFrames) for (int x = modelFrames->count() - numFrames; x < modelFrames->count(); x++) { CANFrame thisFrame = modelFrames->at(x); - int32_t id = static_cast(thisFrame.ID); + int32_t id = static_cast(thisFrame.frameId()); if (!foundID.contains(id)) { foundID.append(id); FilterUtility::createFilterItem(id, ui->listFrameID); } - if (currID == modelFrames->at(x).ID) + if (currID == modelFrames->at(x).frameId()) { thisID = true; break; @@ -275,7 +275,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) for (int i = 0; i < modelFrames->count(); i++) { CANFrame thisFrame = modelFrames->at(i); - if (thisFrame.ID == static_cast(targettedID)) frameCache.append(thisFrame); + if (thisFrame.frameId() == static_cast(targettedID)) frameCache.append(thisFrame); } ui->treeDetails->clear(); @@ -285,7 +285,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) baseNode = new QTreeWidgetItem(); baseNode->setText(0, QString("ID: ") + newID ); - if (frameCache[0].extended) //if these frames seem to be extended then try for J1939 decoding + if (frameCache[0].hasExtendedFrameFormat()) //if these frames seem to be extended then try for J1939 decoding { // ------- J1939 decoding ---------- J1939ID jid; @@ -367,7 +367,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) for (int c = 0; c < 8; c++) { changedBits[c] = 0; - referenceBits[c] = frameCache.at(0).data[c]; + referenceBits[c] = frameCache.at(0).payload()[c]; //qDebug() << referenceBits[c]; } @@ -378,19 +378,19 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) for (int j = 0; j < frameCache.count(); j++) { byteGraphX.append(j); - for (uint32_t bytcnt = 0; bytcnt < frameCache[j].len; bytcnt++) + for (uint32_t bytcnt = 0; bytcnt < frameCache[j].payload().length(); bytcnt++) { - byteGraphY[bytcnt].append(frameCache[j].data[bytcnt]); + byteGraphY[bytcnt].append(frameCache[j].payload()[bytcnt]); } if (j != 0) { //TODO - we try the interval whichever way doesn't go negative. But, we should probably sort the frame list before //starting so that the intervals are all correct. - if (frameCache[j].timestamp > frameCache[j-1].timestamp) - thisInterval = (frameCache[j].timestamp - frameCache[j-1].timestamp); + if (frameCache[j].timeStamp().microSeconds() > frameCache[j-1].timeStamp().microSeconds()) + thisInterval = (frameCache[j].timeStamp().microSeconds() - frameCache[j-1].timeStamp().microSeconds()); else - thisInterval = (frameCache[j-1].timestamp - frameCache[j].timestamp); + thisInterval = (frameCache[j-1].timeStamp().microSeconds() - frameCache[j].timeStamp().microSeconds()); sortedIntervals.push_back(thisInterval); intervalSum += thisInterval; @@ -398,12 +398,12 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) if (thisInterval < minInterval) minInterval = thisInterval; avgInterval += thisInterval; } - thisLen = frameCache.at(j).len; + thisLen = frameCache.at(j).payload().length(); if (thisLen > maxLen) maxLen = thisLen; if (thisLen < minLen) minLen = thisLen; for (int c = 0; c < thisLen; c++) { - unsigned char dat = frameCache.at(j).data[c]; + unsigned char dat = frameCache.at(j).payload()[c]; if (minData[c] > dat) minData[c] = dat; if (maxData[c] < dat) maxData[c] = dat; dataHistogram[dat][c]++; //add one to count for this @@ -600,7 +600,7 @@ void FrameInfoWindow::refreshIDList() for (int i = 0; i < modelFrames->count(); i++) { CANFrame thisFrame = modelFrames->at(i); - id = (int)thisFrame.ID; + id = (int)thisFrame.frameId(); if (!foundID.contains(id)) { foundID.append(id); diff --git a/re/fuzzingwindow.cpp b/re/fuzzingwindow.cpp index 43c60c7..57a5238 100644 --- a/re/fuzzingwindow.cpp +++ b/re/fuzzingwindow.cpp @@ -94,7 +94,7 @@ void FuzzingWindow::updatedFrames(int numFrames) if (numFrames > modelFrames->count()) return; for (int i = modelFrames->count() - numFrames; i < modelFrames->count(); i++) { - id = modelFrames->at(i).ID; + id = modelFrames->at(i).frameId(); if (!foundIDs.contains(id)) { foundIDs.append(id); @@ -133,17 +133,17 @@ void FuzzingWindow::changedNumDataBytes(int newVal) void FuzzingWindow::timerTriggered() { CANFrame thisFrame; - thisFrame.remote = false; sendingBuffer.clear(); int buses = ui->cbBuses->currentIndex(); for (int count = 0; count < ui->spinBurst->value(); count++) { - thisFrame.ID = currentID; - for (int i = 0; i < 8; i++) thisFrame.data[i] = currentBytes[i]; - if (currentID > 0x7FF) thisFrame.extended = true; - else thisFrame.extended = false; - thisFrame.bus = 0; //hard coded for now. TODO: do not hard code - thisFrame.len = ui->spinBytes->value(); + thisFrame.setFrameId(currentID); + QByteArray bytes(ui->spinBytes->value(), 0); + for (int i = 0; i < bytes.length(); i++) bytes[i] = currentBytes[i]; + thisFrame.setPayload(bytes); + if (currentID > 0x7FF) thisFrame.setExtendedFrameFormat(true); + else thisFrame.setExtendedFrameFormat(false); + thisFrame.bus = 0; //hard coded for now. TODO: do not hard code if (buses < (ui->cbBuses->count() - 1)) { @@ -371,7 +371,7 @@ void FuzzingWindow::refreshIDList() for (int i = 0; i < modelFrames->count(); i++) { CANFrame thisFrame = modelFrames->at(i); - id = thisFrame.ID; + id = thisFrame.frameId(); if (!foundIDs.contains(id)) { foundIDs.append(id); diff --git a/re/graphingwindow.cpp b/re/graphingwindow.cpp index d33d439..6a709ab 100644 --- a/re/graphingwindow.cpp +++ b/re/graphingwindow.cpp @@ -186,7 +186,7 @@ void GraphingWindow::updatedFrames(int numFrames) for (int i = modelFrames->count() - numFrames; i < modelFrames->count(); i++) { thisFrame = modelFrames->at(i); - if (graphParams[j].ID == thisFrame.ID) + if (graphParams[j].ID == thisFrame.frameId()) { appendToGraph(graphParams[j], thisFrame, x, y); appendedToGraph = true; @@ -1075,15 +1075,15 @@ void GraphingWindow::appendToGraph(GraphParams ¶ms, CANFrame &frame, QVector { params.strideSoFar = 0; int64_t tempVal; //64 bit temp value. - tempVal = Utility::processIntegerSignal(frame.data, params.startBit, params.numBits, params.intelFormat, params.isSigned); //& params.mask; + tempVal = Utility::processIntegerSignal(frame.payload(), params.startBit, params.numBits, params.intelFormat, params.isSigned); //& params.mask; double xVal, yVal; if (secondsMode) { - xVal = ((double)(frame.timestamp) / 1000000.0 - params.xbias); + xVal = ((double)(frame.timeStamp().microSeconds()) / 1000000.0 - params.xbias); } else { - xVal = (frame.timestamp - params.xbias); + xVal = (frame.timeStamp().microSeconds() - params.xbias); } yVal = (tempVal * params.scale) + params.bias; params.x.append(xVal); @@ -1113,7 +1113,7 @@ void GraphingWindow::createGraph(GraphParams ¶ms, bool createGraphParam) for (int i = 0; i < modelFrames->count(); i++) { CANFrame thisFrame = modelFrames->at(i); - if (thisFrame.ID == params.ID && thisFrame.remote == false) frameCache.append(thisFrame); + if (thisFrame.frameId() == params.ID && thisFrame.frameType() == QCanBusFrame::DataFrame) frameCache.append(thisFrame); } //to fix weirdness where a graph that has no data won't be able to be edited, selected, or deleted properly @@ -1122,12 +1122,10 @@ void GraphingWindow::createGraph(GraphParams ¶ms, bool createGraphParam) if (frameCache.count() == 0) { CANFrame dummy; - dummy.ID = params.ID; + dummy.setFrameId(params.ID); dummy.bus = 0; - dummy.len = 8; - dummy.remote = false; - dummy.timestamp = 0; - for (int i = 0; i < 8; i++) dummy.data[i] = 0; + dummy.setPayload(QByteArray(8, 0)); + dummy.setFrameType(QCanBusFrame::DataFrame); frameCache.append(dummy); } @@ -1147,15 +1145,15 @@ void GraphingWindow::createGraph(GraphParams ¶ms, bool createGraphParam) for (int j = 0; j < numEntries; j++) { int k = j * params.stride; - tempVal = Utility::processIntegerSignal(frameCache[k].data, sBit, bits, intelFormat, isSigned); //& params.mask; + tempVal = Utility::processIntegerSignal(frameCache[k].payload(), sBit, bits, intelFormat, isSigned); //& params.mask; //qDebug() << tempVal; if (secondsMode) { - params.x[j] = (frameCache[k].timestamp) / 1000000.0; + params.x[j] = (frameCache[k].timeStamp().microSeconds()) / 1000000.0; } else { - params.x[j] = frameCache[k].timestamp; + params.x[j] = frameCache[k].timeStamp.microSeconds(); } params.y[j] = (tempVal * params.scale) + params.bias; if (params.y[j] < yminval) yminval = params.y[j]; diff --git a/re/rangestatewindow.cpp b/re/rangestatewindow.cpp index e51f264..30ccd44 100644 --- a/re/rangestatewindow.cpp +++ b/re/rangestatewindow.cpp @@ -158,10 +158,10 @@ void RangeStateWindow::updatedFrames(int numFrames) for (int i = modelFrames->count() - numFrames; i < modelFrames->count(); i++) { thisFrame = modelFrames->at(i); - if (!idFilters.contains(thisFrame.ID)) + if (!idFilters.contains(thisFrame.frameId())) { - idFilters.insert(thisFrame.ID, true); - QListWidgetItem* listItem = new QListWidgetItem(Utility::formatCANID(thisFrame.ID, thisFrame.extended), ui->listFilter); + idFilters.insert(thisFrame.frameId(), true); + QListWidgetItem* listItem = new QListWidgetItem(Utility::formatCANID(thisFrame.frameId(), thisFrame.hasExtendedFrameFormat()), ui->listFilter); listItem->setFlags(listItem->flags() | Qt::ItemIsUserCheckable); // set checkable flag listItem->setCheckState(Qt::Checked); //default all filters to be set active } @@ -178,7 +178,7 @@ void RangeStateWindow::refreshFilterList() for (int i = 0; i < modelFrames->length(); i++) { - id = modelFrames->at(i).ID; + id = modelFrames->at(i).frameId(); if (!idFilters.contains(id)) { idFilters.insert(id, true); @@ -223,7 +223,7 @@ void RangeStateWindow::recalcButton() id = iter.key(); for (int j = 0; j < modelFrames->count(); j++) { - if (modelFrames->at(j).ID == id) frameCache.append(modelFrames->at(j)); + if (modelFrames->at(j).frameId() == id) frameCache.append(modelFrames->at(j)); } //now we've got a list with all the same ID. Time to send it off for processing signalsFactory(); @@ -247,7 +247,7 @@ void RangeStateWindow::signalsFactory() int granularity = ui->spinGranularity->value(); int sigType = ui->cbSignalMode->currentIndex() + 1; int signedType = ui->cbSignedMode->currentIndex() + 1; - int maxBits = frameCache.at(0).len * 8; + int maxBits = frameCache.at(0).payload().length() * 8; int sens = ui->slideSensitivity->value(); for (int sigSize = maxSig; sigSize >= minSig; sigSize -= granularity) @@ -297,7 +297,7 @@ bool RangeStateWindow::processSignal(int startBit, int bitLength, int sensitivit for (i = 0; i < numFrames; i++) { - valu = Utility::processIntegerSignal(frameCache.at(i).data, startBit, bitLength, !bigEndian, isSigned); + valu = Utility::processIntegerSignal(frameCache.at(i).payload(), startBit, bitLength, !bigEndian, isSigned); if (valu < lowestValue) lowestValue = valu; if (valu > highestValue) highestValue = valu; } @@ -316,7 +316,7 @@ bool RangeStateWindow::processSignal(int startBit, int bitLength, int sensitivit return false; //doesn't range enough. for (i = 0; i < numFrames; i++) - scaledVals.append((int)((Utility::processIntegerSignal(frameCache.at(i).data, startBit, bitLength, !bigEndian, isSigned) - lowestValue))); + scaledVals.append((int)((Utility::processIntegerSignal(frameCache.at(i).payload(), startBit, bitLength, !bigEndian, isSigned) - lowestValue))); for (i = 1; i < numFrames; i++) { @@ -365,9 +365,9 @@ bool RangeStateWindow::processSignal(int startBit, int bitLength, int sensitivit { //createGraph(scaledVals); QString temp; - temp = "ID: " + QString::number(frameCache.at(0).ID, 16) + " startBit: " + QString::number(startBit) + " len: " + QString::number(bitLength); + temp = "ID: " + QString::number(frameCache.at(0).frameId(), 16) + " startBit: " + QString::number(startBit) + " len: " + QString::number(bitLength); int64_t foundSig; - foundSig = frameCache.at(0).ID; + foundSig = frameCache.at(0).frameId(); foundSig += (int64_t)startBit << 32; foundSig += (int64_t)bitLength << 40; @@ -474,12 +474,12 @@ void RangeStateWindow::clickedSignalList(int idx) for (int j = 0; j < modelFrames->count(); j++) { - if (modelFrames->at(j).ID == id) frameCache.append(modelFrames->at(j)); + if (modelFrames->at(j).frameId() == id) frameCache.append(modelFrames->at(j)); } int numFrames = frameCache.count(); QVector values; values.reserve(numFrames); - for (int i = 0; i < numFrames; i++) values.append((int)((Utility::processIntegerSignal(frameCache.at(i).data, startBit, bitLength, !isBigEndian, isSigned)))); + for (int i = 0; i < numFrames; i++) values.append((int)((Utility::processIntegerSignal(frameCache.at(i).payload(), startBit, bitLength, !isBigEndian, isSigned)))); createGraph(values); } diff --git a/re/sniffer/snifferitem.cpp b/re/sniffer/snifferitem.cpp index b86462b..2267835 100644 --- a/re/sniffer/snifferitem.cpp +++ b/re/sniffer/snifferitem.cpp @@ -10,11 +10,11 @@ SnifferItem::SnifferItem(const CANFrame& pFrame, quint32 seq): mNotch[i] =0; mMarker.data[i] = 0; mMarker.dataTimestamp[i] = 0; - mCurrent.data[i] = pFrame.data[i]; + mCurrent.data[i] = pFrame.payload()[i]; mCurrent.dataTimestamp[i] = seq; } mLastMarker = mMarker; - mCurrent.len = pFrame.len; + mCurrent.len = pFrame.payload().length(); /* that's dirty */ update(pFrame, seq, false); update(pFrame, seq, false); @@ -115,18 +115,18 @@ void SnifferItem::update(const CANFrame& pFrame, quint32 timeSeq, bool mute) /* copy new value */ for (int i = 0; i < 8; i++) { - maskedData = pFrame.data[i]; + maskedData = pFrame.payload()[i]; if (mute) maskedData &= ~mNotch[i]; maskedCurr = mCurrent.data[i]; if (mute) maskedCurr &= ~mNotch[i]; if (maskedCurr != maskedData) { - mCurrent.data[i] = pFrame.data[i]; + mCurrent.data[i] = pFrame.payload()[i]; mCurrent.dataTimestamp[i] = timeSeq; } } - mCurrent.len = pFrame.len; - mCurrentTime = pFrame.timestamp; + mCurrent.len = pFrame.payload().length(); + mCurrentTime = pFrame.timeStamp().microSeconds(); /* update marker */ //We "OR" our stored marker with the changed bits. diff --git a/re/sniffer/sniffermodel.cpp b/re/sniffer/sniffermodel.cpp index 2fb0b77..667c791 100644 --- a/re/sniffer/sniffermodel.cpp +++ b/re/sniffer/sniffermodel.cpp @@ -300,20 +300,20 @@ void SnifferModel::update(CANConnection*, QVector& pFrames) { foreach(const CANFrame& frame, pFrames) { - if(!mMap.contains(frame.ID)) + if(!mMap.contains(frame.frameId())) { - int index = std::distance(mMap.begin(), mMap.lowerBound(frame.ID)); + int index = std::distance(mMap.begin(), mMap.lowerBound(frame.frameId())); /* add the frame */ beginInsertRows(QModelIndex(), index, index); - mMap[frame.ID] = new SnifferItem(frame, mTimeSequence); - mMap[frame.ID]->update(frame, mTimeSequence, mMuteNotched); + mMap[frame.frameId()] = new SnifferItem(frame, mTimeSequence); + mMap[frame.frameId()]->update(frame, mTimeSequence, mMuteNotched); endInsertRows(); - emit idChange(frame.ID, true); + emit idChange(frame.frameId(), true); } else //updateData - mMap[frame.ID]->update(frame, mTimeSequence, mMuteNotched); + mMap[frame.frameId()]->update(frame, mTimeSequence, mMuteNotched); } } diff --git a/re/temporalgraphwindow.cpp b/re/temporalgraphwindow.cpp index afd4d42..2b3ff4c 100644 --- a/re/temporalgraphwindow.cpp +++ b/re/temporalgraphwindow.cpp @@ -196,13 +196,13 @@ void TemporalGraphWindow::generateGraph() x.reserve(frameCount); y.reserve(frameCount); - xminval = xmaxval = modelFrames->at(0).timestamp / 1000000.0; - yminval = ymaxval = modelFrames->at(0).ID; + xminval = xmaxval = modelFrames->at(0).timeStamp().microSeconds() / 1000000.0; + yminval = ymaxval = modelFrames->at(0).frameId(); for (int i = 0; i < frameCount; i++) { - x.append(modelFrames->at(i).timestamp / 1000000.0); - y.append(modelFrames->at(i).ID); + x.append(modelFrames->at(i).timeStamp().microSeconds() / 1000000.0); + y.append(modelFrames->at(i).frameId()); if (x[i] > xmaxval) xmaxval = x[i]; if (x[i] < xminval) xminval = x[i]; if (y[i] > ymaxval) ymaxval = y[i]; @@ -246,8 +246,8 @@ void TemporalGraphWindow::generateGraph() for (int i = 0; i < frameCount; i++) { - int x = static_cast(((modelFrames->at(i).timestamp / 1000000.0) - xminval) * 4.0); - int y = static_cast(modelFrames->at(i).ID - yminval) / 30; + int x = static_cast(((modelFrames->at(i).timeStamp().microSeconds() / 1000000.0) - xminval) * 4.0); + int y = static_cast(modelFrames->at(i).frameId() - yminval) / 30; double val = colorMap->data()->cell(x, y); double inc; inc = 1 / (val + 1); //logarithmic decay diff --git a/utility.h b/utility.h index e9f8b65..8160aa7 100644 --- a/utility.h +++ b/utility.h @@ -179,7 +179,7 @@ public: return (value1 * (1.0 - samplePoint)) + (value2 * samplePoint); } - static int64_t processIntegerSignal(const uint8_t *data, int startBit, int sigSize, bool littleEndian, bool isSigned) + static int64_t processIntegerSignal(const QByteArray data, int startBit, int sigSize, bool littleEndian, bool isSigned) { int64_t result = 0; From ef24036d1cfcce81e2a8dd9d6816b2cce3768b01 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Fri, 13 Dec 2019 21:06:48 -0500 Subject: [PATCH 03/58] Finished switching over everything to the new can format (I hope). It seems to function now but much more testing is needed. --- blfhandler.cpp | 29 +- canframemodel.cpp | 86 ++--- connections/canconnection.cpp | 2 +- dbc/dbchandler.cpp | 2 +- dbc/dbcsignaleditor.cpp | 2 +- firmwareuploaderwindow.cpp | 85 +++-- framefileio.cpp | 605 ++++++++++++++++---------------- frameplaybackobject.cpp | 18 +- frameplaybackwindow.cpp | 2 +- framesenderwindow.cpp | 58 +-- mainwindow.cpp | 21 +- motorcontrollerconfigwindow.cpp | 64 ++-- re/flowviewwindow.cpp | 34 +- re/frameinfowindow.cpp | 8 +- re/graphingwindow.cpp | 2 +- re/isotp_interpreterwindow.cpp | 4 +- re/sniffer/SnifferDelegate.cpp | 2 +- re/sniffer/snifferitem.cpp | 2 +- scriptcontainer.cpp | 24 +- signalviewerwindow.cpp | 2 +- utility.h | 9 + 21 files changed, 552 insertions(+), 509 deletions(-) diff --git a/blfhandler.cpp b/blfhandler.cpp index 7dde7b3..7192854 100644 --- a/blfhandler.cpp +++ b/blfhandler.cpp @@ -100,19 +100,20 @@ bool BLFHandler::loadBLF(QString filename, QVector* frames) memcpy(&canObject, fileData.constData(), sizeof(BLF_CAN_OBJ)); CANFrame frame; frame.bus = canObject.channel; - frame.extended = (canObject.id & 0x80000000ull)?true:false; - frame.ID = canObject.id & 0x1FFFFFFFull; + frame.setExtendedFrameFormat((canObject.id & 0x80000000ull)?true:false); + frame.setFrameId(canObject.id & 0x1FFFFFFFull); frame.isReceived = true; - frame.len = canObject.dlc; + QByteArray bytes(canObject.dlc, 0); if (canObject.flags & BLF_REMOTE_FLAG) { - frame.remote = true; + frame.setFrameType(QCanBusFrame::RemoteRequestFrame); } else { - frame.remote = false; - for (int i = 0; i < 8; i++) frame.data[i] = canObject.data[i]; + frame.setFrameType(QCanBusFrame::DataFrame); + for (int i = 0; i < 8; i++) bytes[i] = canObject.data[i]; } + frame.setPayload(bytes); //Should we divide by a thousand or a million? Unsure here. It appears some logs are stamped in microseconds and some in milliseconds? - frame.timestamp = obj.header.v1Obj.uncompSize / 1000.0; //uncompsize field also used for timestamp oddly enough + frame.setTimeStamp(QCanBusFrame::TimeStamp(0, obj.header.v1Obj.uncompSize / 1000.0)); //uncompsize field also used for timestamp oddly enough frames->append(frame); } else if (obj.header.base.objType == BLF_CAN_MSG2) @@ -120,19 +121,19 @@ bool BLFHandler::loadBLF(QString filename, QVector* frames) memcpy(&canObject2, fileData.constData(), sizeof(BLF_CAN_OBJ2)); CANFrame frame; frame.bus = canObject2.channel; - frame.extended = (canObject2.id & 0x80000000ull)?true:false; - frame.ID = canObject2.id & 0x1FFFFFFFull; + frame.setExtendedFrameFormat((canObject2.id & 0x80000000ull)?true:false); + frame.setFrameId(canObject2.id & 0x1FFFFFFFull); frame.isReceived = true; - frame.len = canObject2.dlc; + QByteArray bytes(canObject2.dlc, 0); if (canObject2.flags & BLF_REMOTE_FLAG) { - frame.remote = true; + frame.setFrameType(QCanBusFrame::RemoteRequestFrame); } else { - frame.remote = false; - for (int i = 0; i < 8; i++) frame.data[i] = canObject2.data[i]; + frame.setFrameType(QCanBusFrame::DataFrame); + for (int i = 0; i < 8; i++) bytes[i] = canObject2.data[i]; } //Should we divide by a thousand or a million? Unsure here. It appears some logs are stamped in microseconds and some in milliseconds? - frame.timestamp = obj.header.v1Obj.uncompSize / 1000.0; //uncompsize field also used for timestamp oddly enough + frame.setTimeStamp(QCanBusFrame::TimeStamp(0, obj.header.v1Obj.uncompSize / 1000.0)); //uncompsize field also used for timestamp oddly enough frames->append(frame); } else diff --git a/canframemodel.cpp b/canframemodel.cpp index 95f30a2..8a70ecc 100644 --- a/canframemodel.cpp +++ b/canframemodel.cpp @@ -133,23 +133,23 @@ void CANFrameModel::normalizeTiming() { mutex.lock(); if (frames.count() == 0) return; - timeOffset = frames[0].timestamp; + timeOffset = frames[0].timeStamp().microSeconds(); //find the absolute lowest timestamp in the whole time. Needed because maybe timestamp was reset in the middle. for (int j = 0; j < frames.count(); j++) { - if (frames[j].timestamp < timeOffset) timeOffset = frames[j].timestamp; + if (frames[j].timeStamp().microSeconds() < timeOffset) timeOffset = frames[j].timeStamp().microSeconds(); } for (int i = 0; i < frames.count(); i++) { - frames[i].timestamp -= timeOffset; + frames[i].setTimeStamp(QCanBusFrame::TimeStamp(0, frames[i].timeStamp().microSeconds() - timeOffset)); } this->beginResetModel(); for (int i = 0; i < filteredFrames.count(); i++) { - filteredFrames[i].timestamp -= timeOffset; + filteredFrames[i].setTimeStamp(QCanBusFrame::TimeStamp(0, filteredFrames[i].timeStamp().microSeconds() - timeOffset)); } this->endResetModel(); @@ -195,15 +195,15 @@ uint64_t CANFrameModel::getCANFrameVal(int row, Column col) { case Column::TimeStamp: if (overwriteDups) return frame.timedelta; - return frame.timestamp; + return frame.timeStamp().microSeconds(); case Column::FrameId: - return frame.ID; + return frame.frameId(); case Column::Extended: - if (frame.extended) return 1; + if (frame.hasExtendedFrameFormat()) return 1; return 0; case Column::Remote: if (overwriteDups) return frame.frameCount; - if (frame.remote) return 1; + if (frame.frameType() == QCanBusFrame::RemoteRequestFrame) return 1; return 0; case Column::Direction: if (frame.isReceived) return 1; @@ -211,10 +211,10 @@ uint64_t CANFrameModel::getCANFrameVal(int row, Column col) case Column::Bus: return static_cast(frame.bus); case Column::Length: - return static_cast(frame.len); + return static_cast(frame.payload().length()); case Column::ASCII: //sort both the same for now case Column::Data: - for (int i = 0; i < frame.len; i++) temp += (static_cast(frame.data[i]) << (56 - (8 * i))); + for (int i = 0; i < frame.payload().length(); i++) temp += (static_cast(frame.payload()[i]) << (56 - (8 * i))); //qDebug() << temp; return temp; case Column::NUM_COLUMN: @@ -311,7 +311,7 @@ void CANFrameModel::recalcOverwrite() uint64_t idAugmented; //id in lower 29 bits, bus number shifted up 29 bits foreach(CANFrame frame, frames) { - idAugmented = frame.ID; + idAugmented = frame.frameId(); idAugmented = idAugmented + (frame.bus << 29ull); if (!overWriteFrames.contains(idAugmented)) { @@ -321,7 +321,7 @@ void CANFrameModel::recalcOverwrite() } else { - frame.timedelta = frame.timestamp - overWriteFrames[idAugmented].timestamp; + frame.timedelta = frame.timeStamp().microSeconds() - overWriteFrames[idAugmented].timeStamp().microSeconds(); frame.frameCount = overWriteFrames[idAugmented].frameCount + 1; overWriteFrames[idAugmented] = frame; } @@ -335,7 +335,7 @@ void CANFrameModel::recalcOverwrite() for (int i = 0; i < frames.count(); i++) { - if (filters[frames[i].ID]) + if (filters[frames[i].frameId()]) { filteredFrames.append(frames[i]); } @@ -404,17 +404,17 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const if (timeSeconds) return QString::number(thisFrame.timedelta / 1000000.0, 'f', 5); return QString::number(thisFrame.timedelta); } - else ts = Utility::formatTimestamp(thisFrame.timestamp); + else ts = Utility::formatTimestamp(thisFrame.timeStamp().microSeconds()); if (ts.type() == QVariant::Double) return QString::number(ts.toDouble(), 'f', 5); //never scientific notation, 5 decimal places if (ts.type() == QVariant::LongLong) return QString::number(ts.toLongLong()); //never scientific notion, all digits shown if (ts.type() == QVariant::DateTime) return ts.toDateTime().toString(timeFormat); //custom set format for dates and times - return Utility::formatTimestamp(thisFrame.timestamp); + return Utility::formatTimestamp(thisFrame.timeStamp().microSeconds()); case Column::FrameId: - return Utility::formatCANID(thisFrame.ID, thisFrame.extended); + return Utility::formatCANID(thisFrame.frameId(), thisFrame.hasExtendedFrameFormat()); case Column::Extended: - return QString::number(thisFrame.extended); + return QString::number(thisFrame.hasExtendedFrameFormat()); case Column::Remote: - if (!overwriteDups) return QString::number(thisFrame.remote); + if (!overwriteDups) return QString::number(thisFrame.frameType() == QCanBusFrame::RemoteRequestFrame); return QString::number(thisFrame.frameCount); case Column::Direction: if (thisFrame.isReceived) return QString(tr("Rx")); @@ -422,21 +422,21 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const case Column::Bus: return QString::number(thisFrame.bus); case Column::Length: - return QString::number(thisFrame.len); + return QString::number(thisFrame.payload().length()); case Column::ASCII: - if (thisFrame.ID >= 0x7FFFFFF0ull) + if (thisFrame.frameId() >= 0x7FFFFFF0ull) { tempString.append("MARK "); - tempString.append(QString::number(thisFrame.ID & 0x7)); + tempString.append(QString::number(thisFrame.frameId() & 0x7)); return tempString; } - dLen = thisFrame.len; - if (!thisFrame.remote) { + dLen = thisFrame.payload().length(); + if (!thisFrame.frameType() != QCanBusFrame::RemoteRequestFrame) { if (dLen < 0) dLen = 0; if (dLen > 8) dLen = 8; for (int i = 0; i < dLen; i++) { - quint8 byt = thisFrame.data[i]; + quint8 byt = thisFrame.payload()[i]; //0x20 through 0x7E are printable characters. Outside of that range they aren't. So use dots instead if (byt < 0x20) byt = 0x2E; //dot character if (byt > 0x7E) byt = 0x2E; @@ -445,17 +445,17 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const } return tempString; case Column::Data: - dLen = thisFrame.len; + dLen = thisFrame.payload().length(); if (dLen < 0) dLen = 0; if (dLen > 8) dLen = 8; //if (useHexMode) tempString.append("0x "); - if (thisFrame.remote) { + if (thisFrame.frameType() == QCanBusFrame::RemoteRequestFrame) { return tempString; } for (int i = 0; i < dLen; i++) { - if (useHexMode) tempString.append( QString::number(thisFrame.data[i], 16).toUpper().rightJustified(2, '0')); - else tempString.append(QString::number(thisFrame.data[i], 10)); + if (useHexMode) tempString.append( QString::number((unsigned char)thisFrame.payload()[i], 16).toUpper().rightJustified(2, '0')); + else tempString.append(QString::number(thisFrame.payload()[i], 10)); tempString.append(" "); } //now, if we're supposed to interpret the data and the DBC handler is loaded then use it @@ -552,25 +552,25 @@ void CANFrameModel::addFrame(const CANFrame& frame, bool autoRefresh = false) mutex.lock(); CANFrame tempFrame; tempFrame = frame; - tempFrame.timestamp -= timeOffset; + tempFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, tempFrame.timeStamp().microSeconds() - timeOffset)); lastUpdateNumFrames++; //if this ID isn't found in the filters list then add it and show it by default - if (!filters.contains(tempFrame.ID)) + if (!filters.contains(tempFrame.frameId())) { // if there are any filters already configured, leave the new filter disabled if (any_filters_are_configured()) - filters.insert(tempFrame.ID, false); + filters.insert(tempFrame.frameId(), false); else - filters.insert(tempFrame.ID, true); + filters.insert(tempFrame.frameId(), true); needFilterRefresh = true; } if (!overwriteDups) { frames.append(tempFrame); - if (filters[tempFrame.ID]) + if (filters[tempFrame.frameId()]) { if (autoRefresh) beginInsertRows(QModelIndex(), filteredFrames.count(), filteredFrames.count()); tempFrame.frameCount = 1; @@ -583,10 +583,10 @@ void CANFrameModel::addFrame(const CANFrame& frame, bool autoRefresh = false) bool found = false; for (int i = 0; i < frames.count(); i++) { - if ( (frames[i].ID == tempFrame.ID) && (frames[i].bus == tempFrame.bus) ) + if ( (frames[i].frameId() == tempFrame.frameId()) && (frames[i].bus == tempFrame.bus) ) { tempFrame.frameCount = frames[i].frameCount + 1; - tempFrame.timedelta = tempFrame.timestamp - frames[i].timestamp; + tempFrame.timedelta = tempFrame.timeStamp().microSeconds() - frames[i].timeStamp().microSeconds(); frames.replace(i, tempFrame); found = true; break; @@ -595,7 +595,7 @@ void CANFrameModel::addFrame(const CANFrame& frame, bool autoRefresh = false) if (!found) { frames.append(tempFrame); - if (filters[tempFrame.ID]) + if (filters[tempFrame.frameId()]) { if (autoRefresh) beginInsertRows(QModelIndex(), filteredFrames.count(), filteredFrames.count()); tempFrame.frameCount = 1; @@ -608,7 +608,7 @@ void CANFrameModel::addFrame(const CANFrame& frame, bool autoRefresh = false) { for (int j = 0; j < filteredFrames.count(); j++) { - if ( (filteredFrames[j].ID == tempFrame.ID) && (filteredFrames[j].bus == tempFrame.bus) ) + if ( (filteredFrames[j].frameId() == tempFrame.frameId()) && (filteredFrames[j].bus == tempFrame.bus) ) { if (autoRefresh) beginResetModel(); filteredFrames.replace(j, tempFrame); @@ -642,7 +642,7 @@ void CANFrameModel::sendRefresh() int count = frames.count(); for (int i = 0; i < count; i++) { - if (filters[frames[i].ID]) + if (filters[frames[i].frameId()]) { tempContainer.append(frames[i]); } @@ -718,12 +718,12 @@ void CANFrameModel::insertFrames(const QVector &newFrames) for (int i = 0; i < newFrames.count(); i++) { frames.append(newFrames[i]); - if (!filters.contains(newFrames[i].ID)) + if (!filters.contains(newFrames[i].frameId())) { - filters.insert(newFrames[i].ID, true); + filters.insert(newFrames[i].frameId(), true); needFilterRefresh = true; } - if (filters[newFrames[i].ID]) + if (filters[newFrames[i].frameId()]) { insertedFiltered++; filteredFrames.append(newFrames[i]); @@ -743,9 +743,9 @@ int CANFrameModel::getIndexFromTimeID(unsigned int ID, double timestamp) uint64_t intTimeStamp = timestamp * 1000000l; for (int i = 0; i < frames.count(); i++) { - if ((frames[i].ID == ID)) + if ((frames[i].frameId() == ID)) { - if (frames[i].timestamp <= intTimeStamp) bestIndex = i; + if (frames[i].timeStamp().microSeconds() <= intTimeStamp) bestIndex = i; else break; //drop out of loop as soon as we pass the proper timestamp } } diff --git a/connections/canconnection.cpp b/connections/canconnection.cpp index 9dfc0ff..27850ff 100644 --- a/connections/canconnection.cpp +++ b/connections/canconnection.cpp @@ -343,7 +343,7 @@ void CANConnection::checkTargettedFrame(CANFrame &frame) foreach (const CANFltObserver filt, mBusData[frame.bus].mTargettedFrames) { //qDebug() << "Checking filter with id " << filt.id << " mask " << filt.mask; - maskedID = frame.ID & filt.mask; + maskedID = frame.frameId() & filt.mask; if (maskedID == filt.id) { qDebug() << "In connection object I got a targetted frame. Forwarding it."; QMetaObject::invokeMethod(filt.observer, "gotTargettedFrame",Qt::QueuedConnection, Q_ARG(CANFrame, frame)); diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index e10cb87..7871227 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -1531,7 +1531,7 @@ DBC_MESSAGE* DBCHandler::findMessage(const CANFrame &frame) { if (loadedFiles[i].getAssocBus() == -1 || frame.bus == (unsigned int)loadedFiles[i].getAssocBus()) { - DBC_MESSAGE* msg = loadedFiles[i].messageHandler->findMsgByID(frame.ID); + DBC_MESSAGE* msg = loadedFiles[i].messageHandler->findMsgByID(frame.frameId()); if (msg != nullptr) return msg; } } diff --git a/dbc/dbcsignaleditor.cpp b/dbc/dbcsignaleditor.cpp index 2d528c4..6c579ec 100644 --- a/dbc/dbcsignaleditor.cpp +++ b/dbc/dbcsignaleditor.cpp @@ -585,7 +585,7 @@ void DBCSignalEditor::fillValueTable(DBC_SIGNAL *sig) for (int i = 0; i < sig->valList.count(); i++) { - QTableWidgetItem *val = new QTableWidgetItem(Utility::formatNumber(sig->valList[i].value)); + QTableWidgetItem *val = new QTableWidgetItem(Utility::formatNumber((uint64_t)sig->valList[i].value)); QTableWidgetItem *desc = new QTableWidgetItem(sig->valList[i].descript); rowIdx = ui->valuesTable->rowCount(); ui->valuesTable->insertRow(rowIdx); diff --git a/firmwareuploaderwindow.cpp b/firmwareuploaderwindow.cpp index 2b13746..a94a55f 100644 --- a/firmwareuploaderwindow.cpp +++ b/firmwareuploaderwindow.cpp @@ -4,6 +4,13 @@ #include +//You might wonder: Collin, what in the hell is this for? Firmware uploader? For what? I'm interested! Well, it's a custom +//firmware uploader for a motor controller I built. Why would that be in this project. Cuz. It's not really relevant +//to anyone else but might serve as a decent reference for a few things: How to make an uploader interface that runs over CAN, +//how to lay out a screen like this, how to make a comm protocol for firmware updating over CAN. But, most things use UDS +//for firmware updates and wouldn't need this specific code. But, it might be able to be turned into a UDS firmware uploader or downloader. +//Note that this screen is specifically hidden by default because of it's oddball status. You have to re-enable it in mainwindow.cpp to see it. + FirmwareUploaderWindow::FirmwareUploaderWindow(const QVector *frames, QWidget *parent) : QDialog(parent), ui(new Ui::FirmwareUploaderWindow) @@ -70,17 +77,17 @@ void FirmwareUploaderWindow::updatedFrames(int numFrames) void FirmwareUploaderWindow::gotTargettedFrame(CANFrame frame) { - qDebug() << "FUW: Got targetted frame with id " << frame.ID; - if (frame.ID == (uint32_t)(baseAddress + 0x10)) { + qDebug() << "FUW: Got targetted frame with id " << frame.frameId(); + if (frame.frameId() == (uint32_t)(baseAddress + 0x10)) { qDebug() << "Start firmware reply"; - if ((frame.data[0] == 0xAD) && (frame.data[1] == 0xDE)) + if (((char)frame.payload()[0] == (char)0xAD) && ((char)frame.payload()[1] == (char)0xDE)) { - if ((frame.data[2] == 0xAF) && (frame.data[3] == 0xDE)) + if (((char)frame.payload()[2] == (char)0xAF) && ((char)frame.payload()[3] == (char)0xDE)) { - qDebug() << "THere's dead beef here"; - if ((frame.data[4] == (token & 0xFF)) && (frame.data[5] == ((token >> 8) & 0xFF))) + qDebug() << "There's dead beef here"; + if (((char)frame.payload()[4] == (char)(token & 0xFF)) && ((char)frame.payload()[5] == (char)((token >> 8) & 0xFF))) { - if ((frame.data[6] == ((token >> 16) & 0xFF)) && (frame.data[7] == ((token >> 24) & 0xFF))) + if (((char)frame.payload()[6] == (char)((token >> 16) & 0xFF)) && ((char)frame.payload()[7] == (char)((token >> 24) & 0xFF))) { qDebug() << "starting firmware process"; //MainWindow::getReference()->setTargettedID(baseAddress + 0x20); @@ -92,9 +99,9 @@ void FirmwareUploaderWindow::gotTargettedFrame(CANFrame frame) } } - if (frame.ID == (uint32_t)(baseAddress + 0x20)) { + if (frame.frameId() == (uint32_t)(baseAddress + 0x20)) { qDebug() << "Firmware reception success reply"; - int seq = frame.data[0] + (256 * frame.data[1]); + int seq = frame.payload()[0] + (256 * frame.payload()[1]); if (seq == currentSendingPosition) { currentSendingPosition++; @@ -126,18 +133,19 @@ void FirmwareUploaderWindow::sendFirmwareChunk() CANFrame *output = new CANFrame; int firmwareLocation = currentSendingPosition * 4; int xorByte = 0; - output->extended = false; - output->len = 7; + output->setExtendedFrameFormat(false); + QByteArray bytes(7,0); output->bus = bus; - output->ID = baseAddress + 0x16; - output->data[0] = currentSendingPosition & 0xFF; - output->data[1] = (currentSendingPosition >> 8) & 0xFF; - output->data[2] = firmwareData[firmwareLocation++]; - output->data[3] = firmwareData[firmwareLocation++]; - output->data[4] = firmwareData[firmwareLocation++]; - output->data[5] = firmwareData[firmwareLocation++]; - for (int i = 0; i < 6; i++) xorByte = xorByte ^ output->data[i]; - output->data[6] = xorByte; + output->setFrameId(baseAddress + 0x16); + output->payload()[0] = currentSendingPosition & 0xFF; + output->payload()[1] = (currentSendingPosition >> 8) & 0xFF; + output->payload()[2] = firmwareData[firmwareLocation++]; + output->payload()[3] = firmwareData[firmwareLocation++]; + output->payload()[4] = firmwareData[firmwareLocation++]; + output->payload()[5] = firmwareData[firmwareLocation++]; + for (int i = 0; i < 6; i++) xorByte = xorByte ^ output->payload()[i]; + output->payload()[6] = xorByte; + output->setPayload(bytes); sendCANFrame(output); timer->start(); } @@ -145,14 +153,15 @@ void FirmwareUploaderWindow::sendFirmwareChunk() void FirmwareUploaderWindow::sendFirmwareEnding() { CANFrame *output = new CANFrame; - output->extended = false; + output->setExtendedFrameFormat(false); output->bus = bus; - output->len = 4; - output->ID = baseAddress + 0x30; - output->data[3] = 0xC0; - output->data[2] = 0xDE; - output->data[1] = 0xFA; - output->data[0] = 0xDE; + QByteArray bytes(4,0); + output->setFrameId(baseAddress + 0x30); + output->payload()[3] = 0xC0; + output->payload()[2] = 0xDE; + output->payload()[1] = 0xFA; + output->payload()[0] = 0xDE; + output->setPayload(bytes); //sendCANFrame(output, bus); } @@ -171,19 +180,19 @@ void FirmwareUploaderWindow::handleStartStopTransfer() CANConManager::getInstance()->addTargettedFrame(bus, baseAddress + 0x10, 0x7FF, this); CANConManager::getInstance()->addTargettedFrame(bus, baseAddress + 0x20, 0x7FF, this); CANFrame *output = new CANFrame; - output->extended = false; - output->len = 8; + output->setExtendedFrameFormat(false); + QByteArray bytes(8,0); output->bus = bus; - output->ID = baseAddress; + output->setFrameId(baseAddress); - output->data[0] = 0xEF; - output->data[1] = 0xBE; - output->data[2] = 0xAD; - output->data[3] = 0xDE; - output->data[4] = token & 0xFF; - output->data[5] = (token >> 8) & 0xFF; - output->data[6] = (token >> 16) & 0xFF; - output->data[7] = (token >> 24) & 0xFF; + output->payload()[0] = 0xEF; + output->payload()[1] = 0xBE; + output->payload()[2] = 0xAD; + output->payload()[3] = 0xDE; + output->payload()[4] = token & 0xFF; + output->payload()[5] = (token >> 8) & 0xFF; + output->payload()[6] = (token >> 16) & 0xFF; + output->payload()[7] = (token >> 24) & 0xFF; sendCANFrame(output); } else //stop anything in process diff --git a/framefileio.cpp b/framefileio.cpp index 3944744..760c062 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -511,26 +511,25 @@ bool FrameFileIO::loadVehicleSpyFile(QString filename, QVector *frames if (tokens.length() > 20) { thisFrame.bus = 0; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); tempTime = now; tempTime = tempTime.addMSecs(static_cast(tokens[1].toDouble() * 1000.0)); - thisFrame.timestamp = static_cast(tempTime.toMSecsSinceEpoch() * 1000); + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, static_cast(tempTime.toMSecsSinceEpoch() * 1000))); if (tokens[5].startsWith("T")) thisFrame.isReceived = false; else thisFrame.isReceived = true; - thisFrame.ID = static_cast(tokens[9].toInt(nullptr, 16)); - if (tokens[11].startsWith("T")) thisFrame.extended = true; - else thisFrame.extended = false; - - thisFrame.len = 0; + thisFrame.setFrameId(static_cast(tokens[9].toInt(nullptr, 16))); + if (tokens[11].startsWith("T")) thisFrame.setExtendedFrameFormat(true); + else thisFrame.setExtendedFrameFormat(false); + QByteArray bytes; for (int i = 0; i < 8; i++) { if (tokens[12 + i].length() > 0) { - thisFrame.data[i] = static_cast(tokens[12 + i].toInt(nullptr, 16)); - thisFrame.len++; + bytes.append(static_cast(tokens[12 + i].toInt(nullptr, 16))); } else break; } + thisFrame.setPayload(bytes); frames->append(thisFrame); } else foundErrors = true; @@ -667,7 +666,7 @@ bool FrameFileIO::loadCRTDFile(QString filename, QVector* frames) multiplier = 1; //special case. Assume no decimal means microseconds } //qDebug() << "decimal places " << decimalPlaces; - thisFrame.timestamp = static_cast((tokens[0].toDouble() * multiplier)); + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, static_cast((tokens[0].toDouble() * multiplier)))); thisFrame.bus = 0; char firstChar = tokens[1].left(1)[0]; if (firstChar >= '1' && firstChar <= '9') @@ -678,21 +677,22 @@ bool FrameFileIO::loadCRTDFile(QString filename, QVector* frames) } if (firstChar == 'R' || firstChar == 'T') { - thisFrame.ID = static_cast(tokens[2].toInt(nullptr, 16)); - if (tokens[1] == "R29" || tokens[1] == "T29") thisFrame.extended = true; - else thisFrame.extended = false; + thisFrame.setFrameId(static_cast(tokens[2].toInt(nullptr, 16))); + if (tokens[1] == "R29" || tokens[1] == "T29") thisFrame.setExtendedFrameFormat(true); + else thisFrame.setExtendedFrameFormat(false); if (firstChar == 'T') thisFrame.isReceived = false; else thisFrame.isReceived = true; - thisFrame.len = tokens.length() - 3; - thisFrame.remote = false; - for (int d = 0; d < thisFrame.len; d++) + QByteArray bytes(tokens.length() - 3, 0); + thisFrame.setFrameType(QCanBusFrame::DataFrame); + for (int d = 0; d < bytes.length(); d++) { if (tokens[d + 3] != "") { - thisFrame.data[d] = static_cast(tokens[d + 3].toInt(nullptr, 16)); + bytes[d] = static_cast(tokens[d + 3].toInt(nullptr, 16)); } - else thisFrame.data[d] = 0; + else bytes[d] = 0; } + thisFrame.setPayload(bytes); frames->append(thisFrame); } } @@ -798,21 +798,22 @@ bool FrameFileIO::loadCANHackerFile(QString filename, QVector* frames) multiplier = 1; //special case. Assume no decimal means microseconds } //qDebug() << "decimal places " << decimalPlaces; - thisFrame.timestamp = static_cast(tokens[0].toDouble() * multiplier); - thisFrame.ID = static_cast(tokens[1].toInt(nullptr, 16)); - thisFrame.extended = (thisFrame.ID > 0x7FF); + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, static_cast(tokens[0].toDouble() * multiplier))); + thisFrame.setFrameId( static_cast(tokens[1].toInt(nullptr, 16)) ); + thisFrame.setExtendedFrameFormat((thisFrame.frameId() > 0x7FF)); thisFrame.isReceived = true; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); thisFrame.bus = 0; - thisFrame.len = tokens[2].toInt(nullptr, 16); - for (int d = 0; d < thisFrame.len; d++) + QByteArray bytes( tokens[2].toInt(nullptr, 16), 0); + for (int d = 0; d < thisFrame.payload().length(); d++) { if (tokens[d + 3] != "") { - thisFrame.data[d] = static_cast(tokens[d + 3].toInt(nullptr, 16)); + bytes[d] = static_cast(tokens[d + 3].toInt(nullptr, 16)); } - else thisFrame.data[d] = 0; + else bytes[d] = 0; } + thisFrame.setPayload(bytes); frames->append(thisFrame); } else foundErrors = true; @@ -914,22 +915,23 @@ bool FrameFileIO::loadCANOpenFile(QString filename, QVector* frames) QList tokens = line.split(','); if (tokens.length() > 11) { - thisFrame.timestamp = static_cast(tokens[1].simplified().toDouble() * 1000.0); - thisFrame.ID = static_cast(Utility::ParseStringToNum(tokens[5].simplified())); - thisFrame.extended = (thisFrame.ID > 0x7FF); + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, static_cast(tokens[1].simplified().toDouble() * 1000.0))); + thisFrame.setFrameId(static_cast(Utility::ParseStringToNum(tokens[5].simplified()))); + thisFrame.setExtendedFrameFormat( (thisFrame.frameId() > 0x7FF) ); thisFrame.isReceived = true; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); thisFrame.bus = 0; QList dataTok = tokens[11].simplified().split(' '); - thisFrame.len = dataTok.length(); - for (int d = 0; d < thisFrame.len; d++) + QByteArray bytes(dataTok.length(), 0); + for (int d = 0; d < thisFrame.payload().length(); d++) { if (dataTok[d] != "") { - thisFrame.data[d] = static_cast(dataTok[d].simplified().toInt(nullptr, 16)); + bytes[d] = static_cast(dataTok[d].simplified().toInt(nullptr, 16)); } - else thisFrame.data[d] = 0; + else bytes[d] = 0; } + thisFrame.setPayload(bytes); frames->append(thisFrame); } else foundErrors = true; @@ -952,7 +954,7 @@ bool FrameFileIO::saveCRTDFile(QString filename, const QVector* frames } //write in float format with 6 digits after the decimal point - outFile->write(QString::number(frames->at(0).timestamp / 1000000.0, 'f', 6).toUtf8() + tr(" CXX GVRET-PC Reverse Engineering Tool Output V").toUtf8() + QString::number(VERSION).toUtf8()); + outFile->write(QString::number(frames->at(0).timeStamp().microSeconds() / 1000000.0, 'f', 6).toUtf8() + tr(" CXX GVRET-PC Reverse Engineering Tool Output V").toUtf8() + QString::number(VERSION).toUtf8()); outFile->write("\n"); for (int c = 0; c < frames->count(); c++) @@ -964,24 +966,24 @@ bool FrameFileIO::saveCRTDFile(QString filename, const QVector* frames lineCounter = 0; } - outFile->write(QString::number(frames->at(c).timestamp / 1000000.0, 'f', 6).toUtf8()); + outFile->write(QString::number(frames->at(c).timeStamp().microSeconds() / 1000000.0, 'f', 6).toUtf8()); outFile->putChar(' '); outFile->write(QString::number(frames->at(c).bus + 1).toUtf8()); if (frames->at(c).isReceived) outFile->putChar('R'); else outFile->putChar('T'); - if (frames->at(c).extended) + if (frames->at(c).hasExtendedFrameFormat()) { outFile->write("29 "); } else outFile->write("11 "); - outFile->write(QString::number(frames->at(c).ID, 16).toUpper().rightJustified(8, '0').toUtf8()); + outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); outFile->putChar(' '); - for (int temp = 0; temp < frames->at(c).len; temp++) + for (int temp = 0; temp < frames->at(c).payload().length(); temp++) { - outFile->write(QString::number(frames->at(c).data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number(frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(' '); } @@ -1112,65 +1114,67 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) { if (fileVersion == 1) { - thisFrame.timestamp = (uint64_t)(line.mid(10, 8).simplified().toDouble() * 1000.0); - thisFrame.ID = line.mid(28, 8).simplified().toUInt(nullptr, 16); - if (thisFrame.ID < 0x1FFFFFFF) + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, (uint64_t)(line.mid(10, 8).simplified().toDouble() * 1000.0))); + thisFrame.setFrameId(line.mid(28, 8).simplified().toUInt(nullptr, 16)); + if (thisFrame.frameId() < 0x1FFFFFFF) { - thisFrame.len = line.mid(38,1).toInt(); + QByteArray bytes(line.mid(38,1).toInt(), 0); thisFrame.isReceived = true; thisFrame.bus = 0; if (line.at(28) == ' ') { - thisFrame.extended = false; + thisFrame.setExtendedFrameFormat(false); } else { - thisFrame.extended = true; + thisFrame.setExtendedFrameFormat(true); } if (line.at(41) == 'R') { - thisFrame.remote = true; + thisFrame.setFrameType(QCanBusFrame::RemoteRequestFrame); } else { - QList tokens = line.mid(41, thisFrame.len * 3).split(' '); - thisFrame.remote = false; - for (int d = 0; d < thisFrame.len; d++) + QList tokens = line.mid(41, thisFrame.payload().length() * 3).split(' '); + thisFrame.setFrameType(QCanBusFrame::DataFrame); + for (int d = 0; d < thisFrame.payload().length(); d++) { if (tokens[d] != "") { - thisFrame.data[d] = static_cast(tokens[d].toInt(nullptr, 16)); + bytes[d] = static_cast(tokens[d].toInt(nullptr, 16)); } - else thisFrame.data[d] = 0; + else bytes[d] = 0; } } + thisFrame.setPayload(bytes); frames->append(thisFrame); } } else if (fileVersion == 2) { - thisFrame.timestamp = static_cast(line.mid(8, 13).simplified().toDouble() * 1000.0); - thisFrame.ID = line.mid(25, 8).simplified().toUInt(nullptr, 16); - if (thisFrame.ID < 0x1FFFFFFF) + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, static_cast(line.mid(8, 13).simplified().toDouble() * 1000.0))); + thisFrame.setFrameId(line.mid(25, 8).simplified().toUInt(nullptr, 16)); + if (thisFrame.frameId() < 0x1FFFFFFF) { - thisFrame.len = line.mid(37,2).trimmed().toInt(); - qDebug() << thisFrame.len; + QByteArray bytes(line.mid(37,2).trimmed().toInt(), 0); + qDebug() << thisFrame.payload().length(); thisFrame.isReceived = true; thisFrame.bus = 0; if (line.at(25) == ' ') { - thisFrame.extended = false; + thisFrame.setExtendedFrameFormat(false); } else { - thisFrame.extended = true; + thisFrame.setExtendedFrameFormat(true); } if (line.at(40) == 'R') { - thisFrame.remote = true; + thisFrame.setFrameType(QCanBusFrame::RemoteRequestFrame); } else { - QList tokens = line.mid(40, thisFrame.len * 3).split(' '); - thisFrame.remote = false; - for (int d = 0; d < thisFrame.len; d++) + QList tokens = line.mid(40, thisFrame.payload().length() * 3).split(' '); + thisFrame.setFrameType(QCanBusFrame::DataFrame); + for (int d = 0; d < thisFrame.payload().length(); d++) { if (tokens[d] != "") { - thisFrame.data[d] = static_cast(tokens[d].toInt(nullptr, 16)); + bytes[d] = static_cast(tokens[d].toInt(nullptr, 16)); } - else thisFrame.data[d] = 0; + else bytes[d] = 0; } } + thisFrame.setPayload(bytes); frames->append(thisFrame); } } @@ -1255,7 +1259,7 @@ bool FrameFileIO::loadCanalyzerASC(QString filename, QVector* frames) int lineCounter = 0; bool foundErrors = false; bool inHeader = true; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); QList tokens; if (!inFile->open(QIODevice::ReadOnly | QIODevice::Text)) @@ -1286,37 +1290,38 @@ bool FrameFileIO::loadCanalyzerASC(QString filename, QVector* frames) tokens = line.simplified().split(' '); if (tokens.length() > 5) { - thisFrame.timestamp = static_cast(tokens[0].toDouble() * 1000000.0); + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, static_cast(tokens[0].toDouble() * 1000000.0))); if (tokens[2].endsWith('x')) { QByteArray copied_id = tokens[2]; copied_id.chop(1); - thisFrame.ID = copied_id.toUInt(nullptr, 16); - thisFrame.extended = true; + thisFrame.setFrameId(copied_id.toUInt(nullptr, 16)); + thisFrame.setExtendedFrameFormat(true); } else { - thisFrame.ID = tokens[2].toUInt(nullptr, 16); - thisFrame.extended = false; + thisFrame.setFrameId(tokens[2].toUInt(nullptr, 16)); + thisFrame.setExtendedFrameFormat(false); } - thisFrame.len = tokens[5].toInt(); - if (thisFrame.len > 8) return false; - if (thisFrame.len < 0) return false; + QByteArray bytes(tokens[5].toInt(), 0); + if (thisFrame.payload().length() > 8) return false; + if (thisFrame.payload().length() < 0) return false; thisFrame.isReceived = tokens[3].toUpper().contains("RX"); thisFrame.bus = tokens[1].toInt(); - thisFrame.remote = tokens[4] == "r"; - for (int d = 6; d < (6 + static_cast(thisFrame.len)); d++) + if (tokens[4] == "r") thisFrame.setFrameType(QCanBusFrame::RemoteRequestFrame); + for (int d = 6; d < (6 + static_cast(thisFrame.payload().length())); d++) { if (tokens.count() > d) { - thisFrame.data[d - 6] = static_cast(tokens[d].toInt(nullptr, 16)); + bytes[d - 6] = static_cast(tokens[d].toInt(nullptr, 16)); } else //expected byte wasn't there to read. Set it zero and set error flag { - thisFrame.data[d - 6] = 0; + bytes[d - 6] = 0; foundErrors = true; } } + thisFrame.setPayload(bytes); } frames->append(thisFrame); } @@ -1331,10 +1336,10 @@ bool FrameFileIO::saveCanalyzerASC(QString filename, const QVector* fr { QFile *outFile = new QFile(filename); int lineCounter = 0; - uint64_t offsetTime = frames->at(0).timestamp; + uint64_t offsetTime = frames->at(0).timeStamp().microSeconds(); for (int c = 0; c < frames->count(); c++) { - if (frames->at(c).timestamp < offsetTime) offsetTime = frames->at(c).timestamp; + if (frames->at(c).timeStamp().microSeconds() < offsetTime) offsetTime = frames->at(c).timeStamp().microSeconds(); } if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) @@ -1364,24 +1369,24 @@ bool FrameFileIO::saveCanalyzerASC(QString filename, const QVector* fr lineCounter = 0; } - uint64_t timeStamp = (frames->at(c).timestamp - offsetTime) / 1000000ull; + uint64_t timeStamp = (frames->at(c).timeStamp().microSeconds() - offsetTime) / 1000000ull; int tsLen = QString::number(timeStamp).length(); int precision = 6; //vector seems to keep 10 bytes at the start of the line for the timestamp. It should never exceed this //and there should never be a precision over 6 digits after the decimal if (tsLen > 3) precision = 9 - tsLen; - outFile->write(QString::number((frames->at(c).timestamp - offsetTime) / 1000000.0, 'f', precision).rightJustified(10, ' ').toUtf8()); + outFile->write(QString::number((frames->at(c).timeStamp().microSeconds() - offsetTime) / 1000000.0, 'f', precision).rightJustified(10, ' ').toUtf8()); outFile->putChar(' '); outFile->write(QString::number(frames->at(c).bus + 1).toUtf8()); outFile->write(" "); - if (frames->at(c).extended) + if (frames->at(c).hasExtendedFrameFormat()) { - outFile->write(QString::number(frames->at(c).ID, 16).toUpper().rightJustified(8, '0').toUtf8()); + outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); outFile->write("x"); } else { - outFile->write(QString::number(frames->at(c).ID, 16).toUpper().rightJustified(3, '0').toUtf8()); + outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(3, '0').toUtf8()); outFile->write(" "); } outFile->write(" "); @@ -1389,15 +1394,15 @@ bool FrameFileIO::saveCanalyzerASC(QString filename, const QVector* fr if (frames->at(c).isReceived) outFile->write("Rx "); else outFile->write("Tx "); - if (frames->at(c).remote) outFile->write("r "); + if (frames->at(c).frameType() == QCanBusFrame::RemoteRequestFrame) outFile->write("r "); else outFile->write("d "); - outFile->write(QString::number(frames->at(c).len).toUtf8()); + outFile->write(QString::number(frames->at(c).payload().length()).toUtf8()); outFile->write(" "); - for (int temp = 0; temp < frames->at(c).len; temp++) + for (int temp = 0; temp < frames->at(c).payload().length(); temp++) { - outFile->write(QString::number(frames->at(c).data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->write(" "); } @@ -1506,7 +1511,7 @@ bool FrameFileIO::loadNativeCSVFile(QString filename, QVector* frames) uint64_t timeStamp = Utility::GetTimeMS(); int lineCounter = 0; bool foundErrors = false; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); if (!inFile->open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -1533,44 +1538,48 @@ bool FrameFileIO::loadNativeCSVFile(QString filename, QVector* frames) { if (tokens[0].length() > 3) { - thisFrame.timestamp = tokens[0].toULongLong(); + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, tokens[0].toULongLong())); } else { timeStamp += 5; - thisFrame.timestamp = timeStamp; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp)); } - thisFrame.ID = tokens[1].toUInt(nullptr, 16); - if (tokens[2].toUpper().contains("TRUE")) thisFrame.extended = 1; - else thisFrame.extended = 0; + thisFrame.setFrameId(tokens[1].toUInt(nullptr, 16)); + if (tokens[2].toUpper().contains("TRUE")) thisFrame.setExtendedFrameFormat(true); + else thisFrame.setExtendedFrameFormat(false); - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); if (fileVersion == 1) { thisFrame.isReceived = true; thisFrame.bus = tokens[3].toInt(); - thisFrame.len = tokens[4].toInt(); - if (thisFrame.len > 8) thisFrame.len = 8; - if (thisFrame.len < 0) thisFrame.len = 0; - for (int c = 0; c < 8; c++) thisFrame.data[c] = 0; - if (thisFrame.len + 5 > tokens.length()) thisFrame.len = tokens.length() - 5; - for (int d = 0; d < thisFrame.len; d++) - thisFrame.data[d] = static_cast(tokens[5 + d].toInt(nullptr, 16)); + int lng = tokens[4].toInt(); + if (lng > 8) lng = 8; + if (lng < 0) lng = 0; + if (lng + 5 > tokens.length()) lng = tokens.length() - 5; + QByteArray bytes(lng, 0); + for (int c = 0; c < 8; c++) bytes[c] = 0; + for (int d = 0; d < thisFrame.payload().length(); d++) + bytes[d] = static_cast(tokens[5 + d].toInt(nullptr, 16)); + thisFrame.setPayload(bytes); } else if (fileVersion == 2) { if (tokens[3].at(0) == 'R') thisFrame.isReceived = true; else thisFrame.isReceived = false; - thisFrame.bus = tokens[4].toInt(); - thisFrame.len = tokens[5].toInt(); - if (thisFrame.len > 8) thisFrame.len = 8; - if (thisFrame.len < 0) thisFrame.len = 0; - if (thisFrame.len + 6 > tokens.length()) thisFrame.len = tokens.length() - 6; - for (int c = 0; c < 8; c++) thisFrame.data[c] = 0; - for (int d = 0; d < thisFrame.len; d++) - thisFrame.data[d] = static_cast(tokens[6 + d].toInt(nullptr, 16)); + thisFrame.bus = tokens[4].toInt(); + int lng = tokens[5].toInt(); + if (lng > 8) lng = 8; + if (lng < 0) lng = 0; + if (lng + 6 > tokens.length()) lng = tokens.length() - 6; + QByteArray bytes(lng, 0); + for (int c = 0; c < 8; c++) bytes[c] = 0; + for (int d = 0; d < thisFrame.payload().length(); d++) + bytes[d] = static_cast(tokens[6 + d].toInt(nullptr, 16)); + thisFrame.setPayload(bytes); } frames->append(thisFrame); @@ -1606,13 +1615,13 @@ bool FrameFileIO::saveNativeCSVFile(QString filename, const QVector* f lineCounter = 0; } - outFile->write(QString::number(frames->at(c).timestamp).toUtf8()); + outFile->write(QString::number(frames->at(c).timeStamp().microSeconds()).toUtf8()); outFile->putChar(44); - outFile->write(QString::number(frames->at(c).ID, 16).toUpper().rightJustified(8, '0').toUtf8()); + outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); outFile->putChar(44); - if (frames->at(c).extended) outFile->write("true,"); + if (frames->at(c).hasExtendedFrameFormat()) outFile->write("true,"); else outFile->write("false,"); if (frames->at(c).isReceived) outFile->write("Rx,"); @@ -1621,13 +1630,13 @@ bool FrameFileIO::saveNativeCSVFile(QString filename, const QVector* f outFile->write(QString::number(frames->at(c).bus).toUtf8()); outFile->putChar(44); - outFile->write(QString::number(frames->at(c).len).toUtf8()); + outFile->write(QString::number(frames->at(c).payload().length()).toUtf8()); outFile->putChar(44); for (int temp = 0; temp < 8; temp++) { - if (temp < frames->at(c).len) - outFile->write(QString::number(frames->at(c).data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + if (temp < frames->at(c).payload().length()) + outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); else outFile->write("00"); outFile->putChar(44); @@ -1689,13 +1698,13 @@ bool FrameFileIO::writeContinuousNative(const QVector* frames, int beg qDebug() << "Bgn: " << beginningFrame << " Count: " << frames->count(); for (int c = beginningFrame; c < frames->count(); c++) { - continuousFile.write(QString::number(frames->at(c).timestamp).toUtf8()); + continuousFile.write(QString::number(frames->at(c).timeStamp().microSeconds()).toUtf8()); continuousFile.putChar(44); - continuousFile.write(QString::number(frames->at(c).ID, 16).toUpper().rightJustified(8, '0').toUtf8()); + continuousFile.write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); continuousFile.putChar(44); - if (frames->at(c).extended) continuousFile.write("true,"); + if (frames->at(c).hasExtendedFrameFormat()) continuousFile.write("true,"); else continuousFile.write("false,"); if (frames->at(c).isReceived) continuousFile.write("Rx,"); @@ -1704,13 +1713,13 @@ bool FrameFileIO::writeContinuousNative(const QVector* frames, int beg continuousFile.write(QString::number(frames->at(c).bus).toUtf8()); continuousFile.putChar(44); - continuousFile.write(QString::number(frames->at(c).len).toUtf8()); + continuousFile.write(QString::number(frames->at(c).payload().length()).toUtf8()); continuousFile.putChar(44); for (int temp = 0; temp < 8; temp++) { - if (temp < frames->at(c).len) - continuousFile.write(QString::number(frames->at(c).data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + if (temp < frames->at(c).payload().length()) + continuousFile.write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); else continuousFile.write("00"); continuousFile.putChar(44); @@ -1788,7 +1797,7 @@ bool FrameFileIO::loadGenericCSVFile(QString filename, QVector* frames uint64_t timeStamp = Utility::GetTimeMS(); int lineCounter = 0; bool foundErrors = false; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); if (!inFile->open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -1812,17 +1821,16 @@ bool FrameFileIO::loadGenericCSVFile(QString filename, QVector* frames QList tokens = line.split(','); timeStamp += 5000; - thisFrame.timestamp = timeStamp; - thisFrame.ID = tokens[0].toUInt(nullptr, 16); - if (thisFrame.ID > 0x7FF) thisFrame.extended = true; - else thisFrame.extended = false; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp)); + thisFrame.setFrameId(tokens[0].toUInt(nullptr, 16)); + if (thisFrame.frameId() > 0x7FF) thisFrame.setExtendedFrameFormat(true); + else thisFrame.setExtendedFrameFormat(false); thisFrame.bus = 0; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); QList dataTok = tokens[1].split(' '); - thisFrame.len = dataTok.length(); - if (thisFrame.len > 8) thisFrame.len = 8; - for (int d = 0; d < thisFrame.len; d++) thisFrame.data[d] = static_cast(dataTok[d].toInt(nullptr, 16)); - + QByteArray bytes(dataTok.length(), 0); + for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast(dataTok[d].toInt(nullptr, 16)); + thisFrame.setPayload(bytes); frames->append(thisFrame); } else foundErrors = true; @@ -1856,12 +1864,12 @@ bool FrameFileIO::saveGenericCSVFile(QString filename, const QVector* lineCounter = 0; } - outFile->write(QString::number(frames->at(c).ID, 16).toUpper().rightJustified(8, '0').toUtf8()); + outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); outFile->putChar(44); - for (int temp = 0; temp < frames->at(c).len; temp++) + for (int temp = 0; temp < frames->at(c).payload().length(); temp++) { - outFile->write(QString::number(frames->at(c).data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(' '); } @@ -1992,31 +2000,34 @@ bool FrameFileIO::loadLogFile(QString filename, QVector* frames) QList timeToks = tokens[0].split(':'); timeStamp = (timeToks[0].toUInt() * (1000ul * 1000ul * 60ul * 60ul)) + (timeToks[1].toUInt() * (1000ul * 1000ul * 60ul)) + (timeToks[2].toUInt() * (1000ul * 1000ul)) + (timeToks[3].toUInt() * 100ul); - thisFrame.timestamp = timeStamp; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp)); if (tokens[1].at(0) == 'R') thisFrame.isReceived = true; else thisFrame.isReceived = false; - thisFrame.ID = tokens[3].right(tokens[3].length() - 2).toUInt(nullptr, 16); + thisFrame.setFrameId(tokens[3].right(tokens[3].length() - 2).toUInt(nullptr, 16)); if (tokens[4] == "S") { - thisFrame.extended = false; - thisFrame.remote = false; + thisFrame.setExtendedFrameFormat(false); + thisFrame.setFrameType(QCanBusFrame::DataFrame); } else if (tokens[4] == "X") { - thisFrame.extended = true; - thisFrame.remote = false; + thisFrame.setExtendedFrameFormat(true); + thisFrame.setFrameType(QCanBusFrame::DataFrame); } else if (tokens[4] == "SR") { - thisFrame.extended = false; - thisFrame.remote = true; + thisFrame.setExtendedFrameFormat(false); + thisFrame.setFrameType(QCanBusFrame::RemoteRequestFrame); } else { // XR - thisFrame.extended = true; - thisFrame.remote = true; + thisFrame.setExtendedFrameFormat(true); + thisFrame.setFrameType(QCanBusFrame::RemoteRequestFrame); } thisFrame.bus = tokens[2].toInt() - 1; - thisFrame.len = tokens[5].toInt(); - if (thisFrame.len > 8) thisFrame.len = 8; - if (thisFrame.len < 0) thisFrame.len = 0; - if (!thisFrame.remote) { - for (int d = 0; d < thisFrame.len; d++) - thisFrame.data[d] = static_cast(tokens[d + 6].toInt(nullptr, 16)); + + int lng = tokens[5].toInt(); + if (lng > 8) lng = 8; + if (lng < 0) lng = 0; + QByteArray bytes(lng, 0); + if (thisFrame.frameType() != QCanBusFrame::RemoteRequestFrame) { + for (int d = 0; d < thisFrame.payload().length(); d++) + bytes[d] = static_cast(tokens[d + 6].toInt(nullptr, 16)); } + thisFrame.setPayload(bytes); frames->append(thisFrame); } else foundErrors = true; @@ -2066,28 +2077,28 @@ bool FrameFileIO::saveLogFile(QString filename, const QVector* frames) lineCounter = 0; } - tempStamp = QDateTime::fromMSecsSinceEpoch(frames->at(c).timestamp / 1000); + tempStamp = QDateTime::fromMSecsSinceEpoch(frames->at(c).timeStamp().microSeconds() / 1000); outFile->write(tempStamp.toString("hh:mm:ss:zzz").toUtf8()); if (frames->at(c).isReceived) outFile->write(" Rx "); else outFile->write(" Tx "); // busmaster channel start at 1 outFile->write(QString::number(frames->at(c).bus+1).toUtf8() + " "); outFile->write("0x"); - if (frames->at(c).extended && frames->at(c).ID > 0x7FF) { - outFile->write(QString::number(frames->at(c).ID, 16).toUpper().rightJustified(8, '0').toUtf8()); + if (frames->at(c).hasExtendedFrameFormat() && frames->at(c).frameId() > 0x7FF) { + outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); } else { - outFile->write(QString::number(frames->at(c).ID, 16).toUpper().rightJustified(3, '0').toUtf8()); + outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(3, '0').toUtf8()); } - if (frames->at(c).extended) outFile->write(" x"); + if (frames->at(c).hasExtendedFrameFormat()) outFile->write(" x"); else outFile->write(" s"); - if (frames->at(c).remote) outFile->write("r "); + if (frames->at(c).frameType() == QCanBusFrame::RemoteRequestFrame) outFile->write("r "); else outFile->write(" "); - outFile->write(QString::number(frames->at(c).len).toUtf8() + " "); + outFile->write(QString::number(frames->at(c).payload().length()).toUtf8() + " "); - if (!frames->at(c).remote) { - for (int temp = 0; temp < frames->at(c).len; temp++) + if (frames->at(c).frameType() != QCanBusFrame::RemoteRequestFrame) { + for (int temp = 0; temp < frames->at(c).payload().length(); temp++) { - outFile->write(QString::number(frames->at(c).data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(' '); } } @@ -2144,7 +2155,7 @@ bool FrameFileIO::loadIXXATFile(QString filename, QVector* frames) uint64_t timeStamp = Utility::GetTimeMS(); int lineCounter = 0; bool foundErrors = false; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); if (!inFile->open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -2181,29 +2192,30 @@ bool FrameFileIO::loadIXXATFile(QString filename, QVector* frames) foundErrors = true; return false; } - thisFrame.timestamp = timeStamp; - thisFrame.ID = Utility::unQuote(tokens[1]).toUInt(nullptr, 16); + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp)); + thisFrame.setFrameId(Utility::unQuote(tokens[1]).toUInt(nullptr, 16)); QString tempStr = Utility::unQuote(tokens[2]).toUpper(); if (tempStr.length() > 0) { - if (tempStr.at(0) == 'S') thisFrame.extended = false; - else thisFrame.extended = true; + if (tempStr.at(0) == 'S') thisFrame.setExtendedFrameFormat(false); + else thisFrame.setExtendedFrameFormat(true); } else { - thisFrame.extended = false; + thisFrame.setExtendedFrameFormat(false); foundErrors = true; return false; } thisFrame.isReceived = true; thisFrame.bus = 0; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); QStringList dataToks = Utility::unQuote(tokens[4]).simplified().split(' '); - thisFrame.len = dataToks.length(); - if (thisFrame.len > 8) return false; - for (int d = 0; d < thisFrame.len; d++) thisFrame.data[d] = static_cast(dataToks[d].toInt(nullptr, 16)); + QByteArray bytes(dataToks.length(), 0); + if (thisFrame.payload().length() > 8) return false; + for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast(dataToks[d].toInt(nullptr, 16)); + thisFrame.setPayload(bytes); frames->append(thisFrame); } else return false; @@ -2231,7 +2243,7 @@ bool FrameFileIO::saveIXXATFile(QString filename, const QVector* frame outFile->write("ASCII Trace IXXAT SavvyCAN V" + QString::number(VERSION).toUtf8() + "\n"); outFile->write("Date: " + timestamp.toString("d:M:yyyy").toUtf8() + "\n"); outFile->write("Start time: " + timestamp.toString("h:m:s").toUtf8() + "\n"); - timestamp = timestamp.addMSecs((frames->last().timestamp - frames->first().timestamp) / 1000); + timestamp = timestamp.addMSecs((frames->last().timeStamp().microSeconds() - frames->first().timeStamp().microSeconds()) / 1000); outFile->write("Stop time: " + timestamp.toString("h:m:s").toUtf8() + "\n"); outFile->write("Overruns: 0\n"); outFile->write("Baudrate: 500 kbit/s\n"); //could be a lie... this code has no way to know the baud rate (at the moment) @@ -2245,17 +2257,17 @@ bool FrameFileIO::saveIXXATFile(QString filename, const QVector* frame qApp->processEvents(); lineCounter = 0; } - tempStamp = QDateTime::fromMSecsSinceEpoch(frames->at(c).timestamp / 1000); + tempStamp = QDateTime::fromMSecsSinceEpoch(frames->at(c).timeStamp().microSeconds() / 1000); outFile->write("\"" + tempStamp.toString("h:m:s.").toUtf8() + tempStamp.toString("z").rightJustified(3, '0').toUtf8() + "\""); - outFile->write(",\"" + QString::number(frames->at(c).ID, 16).toUpper().rightJustified(8, '0').toUtf8() + "\""); - if (frames->at(c).extended) outFile->write(",\"Ext\""); + outFile->write(",\"" + QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8() + "\""); + if (frames->at(c).hasExtendedFrameFormat()) outFile->write(",\"Ext\""); else outFile->write(",\"Std\""); outFile->write(",\"\",\""); - for (int temp = 0; temp < frames->at(c).len; temp++) + for (int temp = 0; temp < frames->at(c).payload().length(); temp++) { - outFile->write(QString::number(frames->at(c).data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(' '); } @@ -2324,7 +2336,7 @@ bool FrameFileIO::loadCANDOFile(QString filename, QVector* frames) int timeOffset = 0; uint64_t lastTimeStamp = 0; bool foundErrors = false; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); if (!inFile->open(QIODevice::ReadOnly)) { @@ -2350,23 +2362,25 @@ bool FrameFileIO::loadCANDOFile(QString filename, QVector* frames) thisFrame.bus = 0; thisFrame.isReceived = true; - thisFrame.extended = false; //format is incapable of extended frames - thisFrame.remote = false; - - thisFrame.timestamp = 1000000ul * ((unsigned char)data[0] >> 2); - thisFrame.timestamp += (((data[0] & 3) << 8) + (unsigned char)data[1]) * 1000; - thisFrame.timestamp += timeOffset; - if (thisFrame.timestamp < lastTimeStamp) + thisFrame.setExtendedFrameFormat(false); //format is incapable of extended frames + thisFrame.setFrameType(QCanBusFrame::DataFrame); + qint64 tempStamp; + tempStamp = 1000000ul * ((unsigned char)data[0] >> 2); + tempStamp += (((data[0] & 3) << 8) + (unsigned char)data[1]) * 1000; + tempStamp += timeOffset; + if (tempStamp < lastTimeStamp) { timeOffset += 60000000ul; } - lastTimeStamp = thisFrame.timestamp; - thisFrame.ID = (((unsigned char)data[3] & 0x0F) * 256 + (unsigned char)data[2]) & 0x7FF; - thisFrame.len = (unsigned char)data[3] >> 4; + lastTimeStamp = tempStamp; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, tempStamp)); + thisFrame.setFrameId((((unsigned char)data[3] & 0x0F) * 256 + (unsigned char)data[2]) & 0x7FF); + QByteArray bytes((unsigned char)data[3] >> 4, 0); - if (thisFrame.len <= 8 && thisFrame.ID <= 0x7FF) + if (thisFrame.payload().length() <= 8 && thisFrame.frameId() <= 0x7FF) { - for (unsigned int d = 0; d < thisFrame.len; d++) thisFrame.data[d] = (unsigned char)data[4 + d]; + for (unsigned int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = (unsigned char)data[4 + d]; + thisFrame.setPayload(bytes); frames->append(thisFrame); } else foundErrors = true; @@ -2395,7 +2409,7 @@ bool FrameFileIO::saveCANDOFile(QString filename, const QVector* frame //The initial frame in official files sets the global time but I don't care so it is set all zeros here. thisFrame = frames->at(0); - ms = (thisFrame.timestamp / 1000); + ms = (thisFrame.timeStamp().microSeconds() / 1000); data[0] = (((ms / 1000) % 60) << 2) + ((ms % 1000) >> 8); data[1] = (char)(ms & 0xFF); data[2] = (char)0xFF; @@ -2414,15 +2428,15 @@ bool FrameFileIO::saveCANDOFile(QString filename, const QVector* frame for (int j = 0; j < 8; j++) data[4 + j] = (char)0xFF; thisFrame = frames->at(c); - if (!thisFrame.extended) + if (!thisFrame.hasExtendedFrameFormat()) { - ms = (thisFrame.timestamp / 1000); - id = thisFrame.ID & 0x7FF; + ms = (thisFrame.timeStamp().microSeconds() / 1000); + id = thisFrame.frameId() & 0x7FF; data[0] = (((ms / 1000) % 60) << 2) + ((ms % 1000) >> 8); data[1] = (char)(ms & 0xFF); data[2] = (char)(id & 0xFF); - data[3] = (char)((id >> 8) + (thisFrame.len << 4)); - for (unsigned int d = 0; d < thisFrame.len; d++) data[4 + d] = (char)thisFrame.data[d]; + data[3] = (char)((id >> 8) + (thisFrame.payload().length() << 4)); + for (unsigned int d = 0; d < thisFrame.payload().length(); d++) data[4 + d] = (char)thisFrame.payload()[d]; outFile->write(data); } } @@ -2502,7 +2516,7 @@ bool FrameFileIO::loadMicrochipFile(QString filename, QVector* frames) long long timeStamp; int lineCounter = 0; bool foundErrors = false; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); if (!inFile->open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -2535,18 +2549,19 @@ bool FrameFileIO::loadMicrochipFile(QString filename, QVector* frames) if (tokens.length() >= 4) { timeStamp = tokens[0].toInt() * 1000; - thisFrame.timestamp = timeStamp; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp)); if (tokens[1].at(0) == 'R') thisFrame.isReceived = true; else thisFrame.isReceived = false; - thisFrame.remote = false; - thisFrame.ID = Utility::ParseStringToNum(tokens[2]); - if (thisFrame.ID <= 0x7FF) thisFrame.extended = false; - else thisFrame.extended = true; + thisFrame.setFrameType(QCanBusFrame::DataFrame); + thisFrame.setFrameId(Utility::ParseStringToNum(tokens[2])); + if (thisFrame.frameId() <= 0x7FF) thisFrame.setExtendedFrameFormat(false); + else thisFrame.setExtendedFrameFormat(true); thisFrame.bus = 0; - thisFrame.len = tokens[3].toUInt(); - if (thisFrame.len > 8) thisFrame.len = 8; - if (thisFrame.len + 4 > (unsigned int) tokens.length()) thisFrame.len = tokens.length() - 4; - for (unsigned int d = 0; d < thisFrame.len; d++) thisFrame.data[d] = (unsigned char)Utility::ParseStringToNum(tokens[4 + d]); + QByteArray bytes(tokens[3].toUInt(), 0); + if (thisFrame.payload().length() > 8) thisFrame.payload().resize(8); + if (thisFrame.payload().length() + 4 > (unsigned int) tokens.length()) thisFrame.payload().resize( tokens.length() - 4 ); + for (unsigned int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = (unsigned char)Utility::ParseStringToNum(tokens[4 + d]); + thisFrame.setPayload(bytes); frames->append(thisFrame); } else foundErrors = true; @@ -2602,15 +2617,15 @@ bool FrameFileIO::saveMicrochipFile(QString filename, const QVector* f lineCounter = 0; } - outFile->write(QString::number((int)(frames->at(c).timestamp / 1000)).toUtf8()); + outFile->write(QString::number((int)(frames->at(c).timeStamp().microSeconds() / 1000)).toUtf8()); if (frames->at(c).isReceived) outFile->write(";RX;"); else outFile->write(";TX;"); - outFile->write("0x" + QString::number(frames->at(c).ID, 16).toUpper().rightJustified(8, '0').toUtf8() + ";"); - outFile->write(QString::number(frames->at(c).len).toUtf8() + ";"); + outFile->write("0x" + QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8() + ";"); + outFile->write(QString::number(frames->at(c).payload().length()).toUtf8() + ";"); - for (unsigned int temp = 0; temp < frames->at(c).len; temp++) + for (unsigned int temp = 0; temp < frames->at(c).payload().length(); temp++) { - outFile->write("0x" + QString::number(frames->at(c).data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write("0x" + QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(';'); } @@ -2715,7 +2730,7 @@ bool FrameFileIO::loadTraceFile(QString filename, QVector* frames) long long timeStamp = 0; int lineCounter = 0; bool foundErrors = false; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); if (!inFile->open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -2751,18 +2766,19 @@ bool FrameFileIO::loadTraceFile(QString filename, QVector* frames) timeStamp += timestampToks[2].toInt() * 1000000l; timeStamp += timestampToks[3].toInt() * 100; - thisFrame.timestamp = static_cast(timeStamp); + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, static_cast(timeStamp))); - thisFrame.ID = static_cast(tokens[2].toLong(nullptr, 16)); - if (thisFrame.ID <= 0x7FF) thisFrame.extended = false; - else thisFrame.extended = true; + thisFrame.setFrameId(static_cast(tokens[2].toLong(nullptr, 16))); + if (thisFrame.frameId() <= 0x7FF) thisFrame.setExtendedFrameFormat(false); + else thisFrame.setExtendedFrameFormat(true); thisFrame.bus = 0; - thisFrame.remote = false; - thisFrame.len = tokens[3].toUInt(); - if (thisFrame.len > 8) thisFrame.len = 8; + thisFrame.setFrameType(QCanBusFrame::DataFrame); + QByteArray bytes(tokens[3].toUInt(), 0); + if (thisFrame.payload().length() > 8) thisFrame.payload().resize(8); QList dataToks = tokens[4].split(' '); - if (thisFrame.len > (unsigned int) dataToks.length()) thisFrame.len = static_cast(dataToks.length()); - for (unsigned int d = 0; d < thisFrame.len; d++) thisFrame.data[d] = (unsigned char)dataToks[d].toInt(nullptr, 16); + if (thisFrame.payload().length() > (unsigned int) dataToks.length()) thisFrame.payload().resize( static_cast(dataToks.length()) ); + for (unsigned int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = (unsigned char)dataToks[d].toInt(nullptr, 16); + thisFrame.setPayload(bytes); frames->append(thisFrame); } else foundErrors = true; @@ -2825,7 +2841,7 @@ bool FrameFileIO::saveTraceFile(QString filename, const QVector * fram outFile->write(QString::number(lineCounter).rightJustified(10, ' ').toUtf8()); outFile->write("\t"); - tempTime = frames->at(c).timestamp; + tempTime = frames->at(c).timeStamp().microSeconds(); tempTimePiece = tempTime / 1000000ul / 60 / 60; tempTime -= tempTimePiece * 1000000ul * 60 * 60; outFile->write(QString::number(tempTimePiece).rightJustified(2, '0').toUtf8()); @@ -2845,13 +2861,13 @@ bool FrameFileIO::saveTraceFile(QString filename, const QVector * fram outFile->write(QString::number(tempTimePiece).rightJustified(4, '0').toUtf8()); outFile->write("\t"); - outFile->write(QString::number(frames->at(c).ID, 16).toUpper().rightJustified(8, '0').toUtf8() + "\t"); + outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8() + "\t"); - outFile->write(QString::number(frames->at(c).len).toUtf8() + "\t"); + outFile->write(QString::number(frames->at(c).payload().length()).toUtf8() + "\t"); - for (unsigned int temp = 0; temp < frames->at(c).len; temp++) + for (unsigned int temp = 0; temp < frames->at(c).payload().length(); temp++) { - outFile->write(QString::number(frames->at(c).data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(' '); } @@ -2888,25 +2904,25 @@ bool FrameFileIO::saveCanDumpFile(QString filename, const QVector * fr outFile->write("("); - tempTime = frames->at(c).timestamp / 1000000.0; + tempTime = frames->at(c).timeStamp().microSeconds() / 1000000.0; outFile->write(QString::number(tempTime,'f', 6).rightJustified(17, '0').toUtf8()); outFile->write(") vcan0 "); - if (frames->at(c).extended) { - outFile->write(QString::number(frames->at(c).ID, 16).rightJustified(8,'0').toUpper().toUtf8()); + if (frames->at(c).hasExtendedFrameFormat()) { + outFile->write(QString::number(frames->at(c).frameId(), 16).rightJustified(8,'0').toUpper().toUtf8()); } else { - outFile->write(QString::number(frames->at(c).ID, 16).rightJustified(3,'0').toUpper().toUtf8()); + outFile->write(QString::number(frames->at(c).frameId(), 16).rightJustified(3,'0').toUpper().toUtf8()); } outFile->write("#"); - if (frames->at(c).remote) { + if (frames->at(c).frameType() == QCanBusFrame::RemoteRequestFrame) { outFile->write("R"); - outFile->write(QString::number(frames->at(c).len).toUtf8()); + outFile->write(QString::number(frames->at(c).payload().length()).toUtf8()); } else { - for (unsigned int temp = 0; temp < frames->at(c).len; temp++) + for (unsigned int temp = 0; temp < frames->at(c).payload().length(); temp++) { - outFile->write(QString::number(frames->at(c).data[temp], 16).rightJustified(2,'0').toUpper().toUtf8()); + outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).rightJustified(2,'0').toUpper().toUtf8()); } } @@ -3079,22 +3095,23 @@ bool FrameFileIO::loadCanDumpFile(QString filename, QVector* frames) ret = timeExp.exactMatch(tokens[0]); if(!ret) continue; - thisFrame.timestamp = (uint64_t)(timeExp.cap(1).toDouble(&ret) * (double)1000000.0); + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, (uint64_t)(timeExp.cap(1).toDouble(&ret) * (double)1000000.0))); if(!ret) continue; if (line.contains('[')) //the expanded format (second one from the above list) { //(1551774790.942758) can1 7A8 [8] F4 DC D1 83 0E 02 00 00 // 0 1 2 3 4 5 6 7 8 9 10 11 - thisFrame.ID = tokens[2].toULong(nullptr, 16); - if (thisFrame.ID > 0x7FF) thisFrame.extended = true; - else thisFrame.extended = false; - thisFrame.remote = false; - thisFrame.len = tokens[3].at(1) - '0'; - for (uint32_t c = 0; c < thisFrame.len; c++) + thisFrame.setFrameId(tokens[2].toULong(nullptr, 16)); + if (thisFrame.frameId() > 0x7FF) thisFrame.setExtendedFrameFormat(true); + else thisFrame.setExtendedFrameFormat(false); + thisFrame.setFrameType(QCanBusFrame::DataFrame); + QByteArray bytes(tokens[3].at(1) - '0', 0); + for (uint32_t c = 0; c < thisFrame.payload().length(); c++) { - thisFrame.data[c] = static_cast(tokens[4 + c].toInt(nullptr, 16)); + bytes[c] = static_cast(tokens[4 + c].toInt(nullptr, 16)); } + thisFrame.setPayload(bytes); } else //the more concise format (first one from list above) { @@ -3108,32 +3125,32 @@ bool FrameFileIO::loadCanDumpFile(QString filename, QVector* frames) } /* ID */ - thisFrame.ID = static_cast(IdValExp.cap(1).toInt(&ret, 16)); + thisFrame.setFrameId(static_cast(IdValExp.cap(1).toInt(&ret, 16))); if (IdValExp.cap(1).length() > 3) { - thisFrame.extended = true; + thisFrame.setExtendedFrameFormat(true); } else { - thisFrame.extended = false; + thisFrame.setExtendedFrameFormat(false); } QString val= IdValExp.cap(2); pos = 0; - thisFrame.len = 0; + QByteArray bytes; if (val.startsWith("R") && val.at(1).isDigit()) { - thisFrame.len = val.at(1).toLatin1() - '0'; - thisFrame.remote = true; + thisFrame.payload().resize( val.at(1).toLatin1() - '0' ); + thisFrame.setFrameType(QCanBusFrame::RemoteRequestFrame); } else { - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); /* val byte per byte */ while ((pos = valExp.indexIn(val, pos)) != -1) { - thisFrame.data[thisFrame.len] = valExp.cap(1).toInt(&ret, 16); + bytes.append((char)valExp.cap(1).toInt(&ret, 16)); if(!ret) continue; - thisFrame.len++; pos += valExp.matchedLength(); } } + thisFrame.setPayload(bytes); } /*NB: should we make sure len <= 8? */ @@ -3218,18 +3235,19 @@ bool FrameFileIO::loadLawicelFile(QString filename, QVector* frames) line = inFile->readLine().simplified(); if (line.length() > 4 && !line.startsWith("S")) { - thisFrame.timestamp = (timeStamp += 100); - thisFrame.ID = line.mid(0, 3).toInt(nullptr, 16); - thisFrame.extended = false; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp += 100)); + thisFrame.setFrameId( line.mid(0, 3).toInt(nullptr, 16) ); + thisFrame.setExtendedFrameFormat(false); thisFrame.isReceived = true; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); thisFrame.bus = 0; line.remove(0, 3); - thisFrame.len = line.length() / 2; - for (int d = 0; d < thisFrame.len; d++) + QByteArray bytes(line.length() / 2, 0); + for (int d = 0; d < bytes.length(); d++) { - thisFrame.data[d] = static_cast(line.mid(d * 2, 2).toInt(nullptr, 16)); + bytes[d] = static_cast(line.mid(d * 2, 2).toInt(nullptr, 16)); } + thisFrame.setPayload(bytes); frames->append(thisFrame); } } @@ -3292,7 +3310,7 @@ bool FrameFileIO::loadKvaserFile(QString filename, QVector *frames, bo int lineCounter = 0; int base = 10; bool foundErrors = false; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); if (useHex) base = 16; @@ -3322,18 +3340,18 @@ bool FrameFileIO::loadKvaserFile(QString filename, QVector *frames, bo //Chn Identifier Flg DLC D0...1...2...3...4...5...6..D7 Time Dir // 0 000000AD 8 FF FF 00 00 00 00 00 00 154.266550 R thisFrame.bus = line.mid(0,3).simplified().toInt(); - thisFrame.ID = line.mid(4,10).simplified().toInt(nullptr, base); - if (thisFrame.ID > 0x7FF) thisFrame.extended = true; - else thisFrame.extended = false; - thisFrame.remote = false; - thisFrame.len = line.mid(21, 3).simplified().toInt(); + thisFrame.setFrameId(line.mid(4,10).simplified().toInt(nullptr, base)); + if (thisFrame.frameId() > 0x7FF) thisFrame.setExtendedFrameFormat(true); + else thisFrame.setExtendedFrameFormat(false); + thisFrame.setFrameType(QCanBusFrame::DataFrame); + QByteArray bytes(line.mid(21, 3).simplified().toInt(), 0); for (int i = 0; i < 8; i++) { - thisFrame.data[i] = line.mid(25 + i * 4, 3).simplified().toInt(nullptr, base); + bytes[i] = line.mid(25 + i * 4, 3).simplified().toInt(nullptr, base); } - thisFrame.timestamp = line.mid(57, 14).simplified().toDouble() * 1000000; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, line.mid(57, 14).simplified().toDouble() * 1000000)); if (line.mid(72, 1).toUpper() == "R") thisFrame.isReceived = true; else thisFrame.isReceived = false; - + thisFrame.setPayload(bytes); frames->append(thisFrame); } //else foundErrors = true; @@ -3404,7 +3422,7 @@ bool FrameFileIO::loadCabanaFile(QString filename, QVector* frames) uint64_t lastTimeStamp = 0; int lineCounter = 0; bool foundErrors = false; - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); if (!inFile->open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -3440,42 +3458,41 @@ bool FrameFileIO::loadCabanaFile(QString filename, QVector* frames) } if(timeStampBaseSet) { - thisFrame.timestamp = ((uint64_t)(temp) - timeStampBase); - lastTimeStamp = thisFrame.timestamp; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, ((uint64_t)(temp) - timeStampBase))); + lastTimeStamp = thisFrame.timeStamp().microSeconds(); } else { - thisFrame.timestamp = (lastTimeStamp + 1); - lastTimeStamp = thisFrame.timestamp; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, (lastTimeStamp + 1))); + lastTimeStamp = thisFrame.timeStamp().microSeconds(); } } else { - thisFrame.timestamp = (lastTimeStamp + 1); - lastTimeStamp = thisFrame.timestamp; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, (lastTimeStamp + 1))); + lastTimeStamp = thisFrame.timeStamp().microSeconds(); } - thisFrame.ID = tokens[1].toInt(); - if (thisFrame.ID > 0x7ff) thisFrame.extended = 1; - else thisFrame.extended = 0; + thisFrame.setFrameId(tokens[1].toInt()); + if (thisFrame.frameId() > 0x7ff) thisFrame.setExtendedFrameFormat(true); + else thisFrame.setExtendedFrameFormat(false); - thisFrame.remote = false; + thisFrame.setFrameType(QCanBusFrame::DataFrame); thisFrame.isReceived = true; thisFrame.bus = tokens[2].toInt(); - thisFrame.len = 8; - + QByteArray bytes(8,0); { unsigned long long int tempData = tokens[3].toULongLong(nullptr, 16); - thisFrame.data[0] = ((tempData >> 56) & 0xFF); - thisFrame.data[1] = ((tempData >> 48) & 0xFF); - thisFrame.data[2] = ((tempData >> 40) & 0xFF); - thisFrame.data[3] = ((tempData >> 32) & 0xFF); - thisFrame.data[4] = ((tempData >> 24) & 0xFF); - thisFrame.data[5] = ((tempData >> 16) & 0xFF); - thisFrame.data[6] = ((tempData >> 8) & 0xFF); - thisFrame.data[7] = (tempData & 0xFF); + bytes[0] = ((tempData >> 56) & 0xFF); + bytes[1] = ((tempData >> 48) & 0xFF); + bytes[2] = ((tempData >> 40) & 0xFF); + bytes[3] = ((tempData >> 32) & 0xFF); + bytes[4] = ((tempData >> 24) & 0xFF); + bytes[5] = ((tempData >> 16) & 0xFF); + bytes[6] = ((tempData >> 8) & 0xFF); + bytes[7] = (tempData & 0xFF); } - + thisFrame.setPayload(bytes); frames->append(thisFrame); } else foundErrors = true; @@ -3509,14 +3526,14 @@ bool FrameFileIO::saveCabanaFile(QString filename, const QVector* fram lineCounter = 0; } - double tempTimeStamp = frames->at(c).timestamp; + double tempTimeStamp = frames->at(c).timeStamp().microSeconds(); tempTimeStamp /= 1000000; outFile->write(QString::number(tempTimeStamp, 'f').toUtf8()); outFile->write(".0"); outFile->putChar(44); - outFile->write(QString::number(frames->at(c).ID, 10).toUpper().toUtf8()); + outFile->write(QString::number(frames->at(c).frameId(), 10).toUpper().toUtf8()); outFile->putChar(44); outFile->write(QString::number(frames->at(c).bus).toUtf8()); @@ -3524,8 +3541,8 @@ bool FrameFileIO::saveCabanaFile(QString filename, const QVector* fram for (unsigned int temp = 0; temp < 8; temp++) { - if (temp < frames->at(c).len) - outFile->write(QString::number(frames->at(c).data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + if (temp < frames->at(c).payload().length()) + outFile->write(QString::number(frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); else outFile->write("00"); } diff --git a/frameplaybackobject.cpp b/frameplaybackobject.cpp index e990efe..b918b07 100644 --- a/frameplaybackobject.cpp +++ b/frameplaybackobject.cpp @@ -68,7 +68,7 @@ quint64 FramePlaybackObject::updatePosition(bool forward) //only send frame out if its ID is checked in the list. Otherwise discard it. CANFrame *thisFrame = ¤tSeqItem->data[currentPosition]; uint32_t originalBus = thisFrame->bus; - if (currentSeqItem->idFilters.find(thisFrame->ID).value()) + if (currentSeqItem->idFilters.find(thisFrame->frameId()).value()) { if (whichBusSend > -1) { @@ -90,7 +90,7 @@ quint64 FramePlaybackObject::updatePosition(bool forward) thisFrame->bus = originalBus; } - return thisFrame->timestamp; + return thisFrame->timeStamp().microSeconds(); } quint64 FramePlaybackObject::peekPosition(bool forward) @@ -113,7 +113,7 @@ quint64 FramePlaybackObject::peekPosition(bool forward) } } CANFrame *thisFrame = ¤tSeqItem->data[peekCurrentPosition]; - return thisFrame->timestamp; + return thisFrame->timeStamp().microSeconds(); } void FramePlaybackObject::piStart() @@ -197,8 +197,8 @@ void FramePlaybackObject::startPlaybackForward() { playbackTimer->setInterval(1); playbackElapsed.start(); - if (currentSeqItem->data[currentPosition].timestamp > 2000) - playbackLastTimeStamp = currentSeqItem->data[currentPosition].timestamp - 2000; + if (currentSeqItem->data[currentPosition].timeStamp().microSeconds() > 2000) + playbackLastTimeStamp = currentSeqItem->data[currentPosition].timeStamp().microSeconds() - 2000; else playbackLastTimeStamp = 0; } playbackTimer->start(); @@ -219,7 +219,7 @@ void FramePlaybackObject::startPlaybackBackward() { playbackElapsed.start(); playbackTimer->setInterval(1); - playbackLastTimeStamp = currentSeqItem->data[currentPosition].timestamp + 2000; + playbackLastTimeStamp = currentSeqItem->data[currentPosition].timeStamp().microSeconds() + 2000; } playbackTimer->start(); } @@ -350,8 +350,8 @@ void FramePlaybackObject::timerTriggered() if (peekPosition(true) == 0xFFFFFFFFFFFFFFFFull) { updatePosition(true); //this'll go to the next log (if there is one) - if (currentSeqItem->data[currentPosition].timestamp > 1000) - playbackLastTimeStamp = currentSeqItem->data[currentPosition].timestamp - 1000; + if (currentSeqItem->data[currentPosition].timeStamp().microSeconds() > 1000) + playbackLastTimeStamp = currentSeqItem->data[currentPosition].timeStamp().microSeconds() - 1000; else playbackLastTimeStamp = 0; } } @@ -367,7 +367,7 @@ void FramePlaybackObject::timerTriggered() if (peekPosition(false) == 0xFFFFFFFFFFFFFFFFull) { updatePosition(false); //this'll go to the next log (if there is one) - playbackLastTimeStamp = currentSeqItem->data[currentPosition].timestamp + 1000; + playbackLastTimeStamp = currentSeqItem->data[currentPosition].timeStamp().microSeconds() + 1000; } } statusCounter++; diff --git a/frameplaybackwindow.cpp b/frameplaybackwindow.cpp index 975df44..5f9ff76 100644 --- a/frameplaybackwindow.cpp +++ b/frameplaybackwindow.cpp @@ -426,7 +426,7 @@ void FramePlaybackWindow::fillIDHash(SequenceItem &item) for (int i = 0; i < item.data.count(); i++) { - id = item.data[i].ID; + id = item.data[i].frameId(); if (!item.idFilters.contains(id)) { item.idFilters.insert(id, true); diff --git a/framesenderwindow.cpp b/framesenderwindow.cpp index fbd4093..1a8a4a7 100644 --- a/framesenderwindow.cpp +++ b/framesenderwindow.cpp @@ -130,13 +130,13 @@ void FrameSenderWindow::buildFrameCache() for (int i = 0; i < modelFrames->count(); i++) { thisFrame = modelFrames->at(i); - if (!frameCache.contains(thisFrame.ID)) + if (!frameCache.contains(thisFrame.frameId())) { - frameCache.insert(thisFrame.ID, thisFrame); + frameCache.insert(thisFrame.frameId(), thisFrame); } else { - frameCache[thisFrame.ID] = thisFrame; + frameCache[thisFrame.frameId()] = thisFrame; } } } @@ -160,13 +160,13 @@ void FrameSenderWindow::updatedFrames(int numFrames) for (int i = modelFrames->count() - numFrames; i < modelFrames->count(); i++) { thisFrame = modelFrames->at(i); - if (!frameCache.contains(thisFrame.ID)) + if (!frameCache.contains(thisFrame.frameId())) { - frameCache.insert(thisFrame.ID, thisFrame); + frameCache.insert(thisFrame.frameId(), thisFrame); } else { - frameCache[thisFrame.ID] = thisFrame; + frameCache[thisFrame.frameId()] = thisFrame; } processIncomingFrame(&thisFrame); } @@ -182,8 +182,8 @@ void FrameSenderWindow::processIncomingFrame(CANFrame *frame) { Trigger *thisTrigger = &sendingData[sd].triggers[trig]; qDebug() << "Trigger ID: " << thisTrigger->ID; - qDebug() << "Frame ID: " << frame->ID; - if (thisTrigger->ID > 0 && (uint32_t)thisTrigger->ID == frame->ID) + qDebug() << "Frame ID: " << frame->frameId(); + if (thisTrigger->ID > 0 && (uint32_t)thisTrigger->ID == frame->frameId()) { if ((uint32_t)thisTrigger->bus == frame->bus || thisTrigger->bus == -1) { @@ -511,7 +511,7 @@ void FrameSenderWindow::doModifiers(int idx) } } //Finally, drop the result into the proper data byte - sendData->data[mod->destByte] = (unsigned char) shadowReg; + sendData->payload().data()[mod->destByte] = (char) shadowReg; } } @@ -525,16 +525,16 @@ int FrameSenderWindow::fetchOperand(int idx, ModifierOperand op) } else if (op.ID == -2) //fetch data from a data byte within the output frame { - if (op.notOper) return ~sendingData.at(idx).data[op.databyte]; - else return sendingData.at(idx).data[op.databyte]; + if (op.notOper) return ~sendingData.at(idx).payload()[op.databyte]; + else return sendingData.at(idx).payload()[op.databyte]; } else //look up external data byte { tempFrame = lookupFrame(op.ID, op.bus); if (tempFrame != nullptr) { - if (op.notOper) return ~tempFrame->data[op.databyte]; - else return tempFrame->data[op.databyte]; + if (op.notOper) return ~tempFrame->payload()[op.databyte]; + else return tempFrame->payload()[op.databyte]; } else return 0; } @@ -643,7 +643,7 @@ void FrameSenderWindow::processModifierText(int line) } else thisOp.second.notOper = false; thisOp.second.bus = sendingData[line].bus; - thisOp.second.ID = sendingData[line].ID; + thisOp.second.ID = sendingData[line].frameId(); parseOperandString(secondOp.split(":"), thisOp.second); thisMod.operations.append(thisOp); } @@ -793,10 +793,10 @@ void FrameSenderWindow::updateGridRow(int idx) QTableWidgetItem *item = ui->tableSender->item(gridLine, 9); if (item == nullptr) item = new QTableWidgetItem(); item->setText(QString::number(temp->count)); - if (!temp->remote) { - for (unsigned int i = 0; i < temp->len; i++) + if (temp->frameType() != QCanBusFrame::RemoteRequestFrame) { + for (unsigned int i = 0; i < temp->payload().length(); i++) { - dataString.append(Utility::formatNumber(temp->data[i])); + dataString.append(Utility::formatNumber(temp->payload()[i])); dataString.append(" "); } ui->tableSender->item(gridLine, 6)->setText(dataString); @@ -818,8 +818,8 @@ void FrameSenderWindow::processCellChange(int line, int col) { FrameSendData tempData; tempData.enabled = false; - tempData.remote = false; - tempData.extended = false; + tempData.setFrameType(QCanBusFrame::DataFrame); + tempData.setExtendedFrameFormat(false); sendingData.append(tempData); } @@ -848,9 +848,9 @@ void FrameSenderWindow::processCellChange(int line, int col) tempVal = Utility::ParseStringToNum(ui->tableSender->item(line, 2)->text()); if (tempVal < 0) tempVal = 0; if (tempVal > 0x7FFFFFFF) tempVal = 0x7FFFFFFF; - sendingData[line].ID = tempVal; - if (sendingData[line].ID > 0x7FF) { - sendingData[line].extended = true; + sendingData[line].setFrameId(tempVal); + if (sendingData[line].frameId() > 0x7FF) { + sendingData[line].setExtendedFrameFormat(true); ui->tableSender->blockSignals(true); ui->tableSender->item(line, 4)->setCheckState(Qt::Checked); ui->tableSender->blockSignals(false); @@ -861,29 +861,29 @@ void FrameSenderWindow::processCellChange(int line, int col) tempVal = Utility::ParseStringToNum(ui->tableSender->item(line, 3)->text()); if (tempVal < 0) tempVal = 0; if (tempVal > 8) tempVal = 8; - sendingData[line].len = tempVal; + sendingData[line].payload().resize(tempVal); break; case 4: // Ext if (ui->tableSender->item(line, 4)->checkState() == Qt::Checked) { - sendingData[line].extended = true; + sendingData[line].setExtendedFrameFormat(true); } else { - sendingData[line].extended = false; + sendingData[line].setExtendedFrameFormat(false); } break; case 5: // Rem if (ui->tableSender->item(line, 5)->checkState() == Qt::Checked) { - sendingData[line].remote = true; + sendingData[line].setFrameType(QCanBusFrame::RemoteRequestFrame); } else { - sendingData[line].remote = false; + sendingData[line].setFrameType(QCanBusFrame::DataFrame); } break; case 6: //Data bytes - for (int i = 0; i < 8; i++) sendingData[line].data[i] = 0; + for (int i = 0; i < 8; i++) sendingData[line].payload().data()[i] = 0; tokens = ui->tableSender->item(line, 6)->text().split(" "); for (int j = 0; j < tokens.count(); j++) { - sendingData[line].data[j] = (uint8_t)Utility::ParseStringToNum(tokens[j]); + sendingData[line].payload().data()[j] = (uint8_t)Utility::ParseStringToNum(tokens[j]); } break; case 7: //triggers diff --git a/mainwindow.cpp b/mainwindow.cpp index 7cee894..0e9fc71 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -168,12 +168,9 @@ MainWindow::MainWindow(QWidget *parent) : //of scaling or font differences between different computers. CANFrame temp; temp.bus = 0; - temp.ID = 0x100; - temp.len = 0; - temp.extended = false; + temp.setFrameId(0x100); temp.isReceived = true; - temp.remote = false; - temp.timestamp = 100000000; + temp.setTimeStamp(QCanBusFrame::TimeStamp(0, 100000000)); model->addFrame(temp, true); qApp->processEvents(); tickGUIUpdate(); //force a GUI refresh so that the row exists to measure @@ -402,7 +399,7 @@ void MainWindow::gridDoubleClicked(QModelIndex idx) //qDebug() << "Grid double clicked"; //grab ID and timestamp and send them away CANFrame frame = model->getListReference()->at(idx.row()); - emit sendCenterTimeID(frame.ID, frame.timestamp / 1000000.0); + emit sendCenterTimeID(frame.frameId(), frame.timeStamp().microSeconds() / 1000000.0); } void MainWindow::interpretToggled(bool state) @@ -743,18 +740,18 @@ Data Bytes: 88 10 00 13 BB 00 06 00 { CANFrame thisFrame = frames->at(c); QString builderString; - builderString += tr("Time: ") + QString::number((thisFrame.timestamp / 1000000.0), 'f', 6); - builderString += tr(" ID: ") + Utility::formatCANID(thisFrame.ID, thisFrame.extended); - if (thisFrame.extended) builderString += tr(" Ext "); + builderString += tr("Time: ") + QString::number((thisFrame.timeStamp().microSeconds() / 1000000.0), 'f', 6); + builderString += tr(" ID: ") + Utility::formatCANID(thisFrame.frameId(), thisFrame.hasExtendedFrameFormat()); + if (thisFrame.hasExtendedFrameFormat()) builderString += tr(" Ext "); else builderString += tr(" Std "); builderString += tr("Bus: ") + QString::number(thisFrame.bus); - builderString += " Len: " + QString::number(thisFrame.len) + "\n"; + builderString += " Len: " + QString::number(thisFrame.payload().length()) + "\n"; outFile->write(builderString.toUtf8()); builderString = tr("Data Bytes: "); - for (unsigned int temp = 0; temp < thisFrame.len; temp++) + for (unsigned int temp = 0; temp < thisFrame.payload().length(); temp++) { - builderString += Utility::formatNumber(thisFrame.data[temp]) + " "; + builderString += Utility::formatNumber(thisFrame.payload()[temp]) + " "; } builderString += "\n"; outFile->write(builderString.toUtf8()); diff --git a/motorcontrollerconfigwindow.cpp b/motorcontrollerconfigwindow.cpp index eafc31a..1fe9dae 100644 --- a/motorcontrollerconfigwindow.cpp +++ b/motorcontrollerconfigwindow.cpp @@ -4,6 +4,17 @@ #include "mainwindow.h" #include +/* + * Nothing to see here, these are not the droids you're looking for. Go away + * + * + * + * You didn't go away, did you... This is a screen that allows for setting EEPROM configuration for a + * custom motor controller project I was working on. It's hidden by default. You could re-enable it and play around + * with it if you're bored. It might be a good basis for how to set a list of parameters on a device. But, it could be broken + * these days too. It is not maintained any longer as the project it was meant for is abandoned. YMMV. +*/ + MotorControllerConfigWindow::MotorControllerConfigWindow(const QVector *frames, QWidget *parent) : QDialog(parent), ui(new Ui::MotorControllerConfigWindow) @@ -57,18 +68,18 @@ void MotorControllerConfigWindow::updatedFrames(int numFrames) for (int i = modelFrames->count() - numFrames; i < modelFrames->count(); i++) { thisFrame = modelFrames->at(i); - id = thisFrame.ID; + id = thisFrame.frameId(); if (id == 0xC2) //response to a query we made { - if (thisFrame.data[2] == 0) + if ((char)thisFrame.payload()[2] == 0) { - int paramID = thisFrame.data[0] + (thisFrame.data[1] * 256); + int paramID = thisFrame.payload()[0] + (thisFrame.payload()[1] * 256); for (int i = 0; i < params.length(); i++) { if (params[i].paramID == paramID) { - params[i].value = thisFrame.data[4] + (thisFrame.data[5] * 256); + params[i].value = thisFrame.payload()[4] + (thisFrame.payload()[5] * 256); if (params[i].paramType == ASCII) item = new QTableWidgetItem(); //QString::fromUtf8((char *)params[i].value, 2)); if (params[i].paramType == HEX) item = new QTableWidgetItem(Utility::formatHexNum(params[i].value)); if (params[i].paramType == DEC) @@ -177,18 +188,19 @@ void MotorControllerConfigWindow::timerTick() { qDebug() << "Request: " << QString::number(transmitStep); - outFrame.ID = 0xC1; - outFrame.len = 8; + outFrame.setFrameId(0xC1); + QByteArray bytes(8, 0); outFrame.bus = 0; - outFrame.extended = false; - outFrame.data[0] = params[transmitStep].paramID & 0xFF; - outFrame.data[1] = (params[transmitStep].paramID >> 8) & 0xFF; - outFrame.data[2] = 0; //0 = read, 1 = write - outFrame.data[3] = 0; //reserved - outFrame.data[4] = 0; //value goes in bytes 4,5 when writing - outFrame.data[5] = 0; - outFrame.data[6] = 0; //reserved - outFrame.data[7] = 0; //reserved + outFrame.setExtendedFrameFormat(false); + bytes[0] = params[transmitStep].paramID & 0xFF; + bytes[1] = (params[transmitStep].paramID >> 8) & 0xFF; + bytes[2] = 0; //0 = read, 1 = write + bytes[3] = 0; //reserved + bytes[4] = 0; //value goes in bytes 4,5 when writing + bytes[5] = 0; + bytes[6] = 0; //reserved + bytes[7] = 0; //reserved + outFrame.setPayload(bytes); CANConManager::getInstance()->sendFrame(outFrame); @@ -213,18 +225,18 @@ void MotorControllerConfigWindow::timerTick() if (thisValue != params[transmitStep].value) { - outFrame.ID = 0xC1; - outFrame.len = 8; + outFrame.setFrameId(0xC1); + QByteArray bytes(8, 0); outFrame.bus = 0; - outFrame.extended = false; - outFrame.data[0] = params[transmitStep].paramID & 0xFF; - outFrame.data[1] = (params[transmitStep].paramID >> 8) & 0xFF; - outFrame.data[2] = 1; //0 = read, 1 = write - outFrame.data[3] = 0; //reserved - outFrame.data[4] = params[transmitStep].value & 0xFF; - outFrame.data[5] = (params[transmitStep].value >> 8) & 0xFF; - outFrame.data[6] = 0; //reserved - outFrame.data[7] = 0; //reserved + outFrame.setExtendedFrameFormat(false); + bytes[0] = params[transmitStep].paramID & 0xFF; + bytes[1] = (params[transmitStep].paramID >> 8) & 0xFF; + bytes[2] = 1; //0 = read, 1 = write + bytes[3] = 0; //reserved + bytes[4] = params[transmitStep].value & 0xFF; + bytes[5] = (params[transmitStep].value >> 8) & 0xFF; + bytes[6] = 0; //reserved + bytes[7] = 0; //reserved CANConManager::getInstance()->sendFrame(outFrame); } diff --git a/re/flowviewwindow.cpp b/re/flowviewwindow.cpp index dcd627a..8261aef 100644 --- a/re/flowviewwindow.cpp +++ b/re/flowviewwindow.cpp @@ -293,7 +293,7 @@ void FlowViewWindow::gotCenterTimeID(int32_t ID, double timestamp) memcpy(refBytes, currBytes, 8); } - memcpy(currBytes, frameCache.at(currentPosition).data, 8); + memcpy(currBytes, frameCache.at(currentPosition).payload().data(), 8); updateDataView(); } @@ -696,23 +696,23 @@ void FlowViewWindow::timerTriggered() void FlowViewWindow::updateDataView() { - ui->txtCurr1->setText(Utility::formatNumber(currBytes[0])); - ui->txtCurr2->setText(Utility::formatNumber(currBytes[1])); - ui->txtCurr3->setText(Utility::formatNumber(currBytes[2])); - ui->txtCurr4->setText(Utility::formatNumber(currBytes[3])); - ui->txtCurr5->setText(Utility::formatNumber(currBytes[4])); - ui->txtCurr6->setText(Utility::formatNumber(currBytes[5])); - ui->txtCurr7->setText(Utility::formatNumber(currBytes[6])); - ui->txtCurr8->setText(Utility::formatNumber(currBytes[7])); + ui->txtCurr1->setText(Utility::formatNumber((char)currBytes[0])); + ui->txtCurr2->setText(Utility::formatNumber((char)currBytes[1])); + ui->txtCurr3->setText(Utility::formatNumber((char)currBytes[2])); + ui->txtCurr4->setText(Utility::formatNumber((char)currBytes[3])); + ui->txtCurr5->setText(Utility::formatNumber((char)currBytes[4])); + ui->txtCurr6->setText(Utility::formatNumber((char)currBytes[5])); + ui->txtCurr7->setText(Utility::formatNumber((char)currBytes[6])); + ui->txtCurr8->setText(Utility::formatNumber((char)currBytes[7])); - ui->txtRef1->setText(Utility::formatNumber(refBytes[0])); - ui->txtRef2->setText(Utility::formatNumber(refBytes[1])); - ui->txtRef3->setText(Utility::formatNumber(refBytes[2])); - ui->txtRef4->setText(Utility::formatNumber(refBytes[3])); - ui->txtRef5->setText(Utility::formatNumber(refBytes[4])); - ui->txtRef6->setText(Utility::formatNumber(refBytes[5])); - ui->txtRef7->setText(Utility::formatNumber(refBytes[6])); - ui->txtRef8->setText(Utility::formatNumber(refBytes[7])); + ui->txtRef1->setText(Utility::formatNumber((char)refBytes[0])); + ui->txtRef2->setText(Utility::formatNumber((char)refBytes[1])); + ui->txtRef3->setText(Utility::formatNumber((char)refBytes[2])); + ui->txtRef4->setText(Utility::formatNumber((char)refBytes[3])); + ui->txtRef5->setText(Utility::formatNumber((char)refBytes[4])); + ui->txtRef6->setText(Utility::formatNumber((char)refBytes[5])); + ui->txtRef7->setText(Utility::formatNumber((char)refBytes[6])); + ui->txtRef8->setText(Utility::formatNumber((char)refBytes[7])); ui->flowView->setReference(refBytes, false); ui->flowView->updateData(currBytes, true); diff --git a/re/frameinfowindow.cpp b/re/frameinfowindow.cpp index 3410565..e200a8b 100644 --- a/re/frameinfowindow.cpp +++ b/re/frameinfowindow.cpp @@ -336,13 +336,13 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) baseNode->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr(" Priority bits: ") + Utility::formatNumber( FilterUtility::getGMLanPriorityBits(targettedID))); + tempItem->setText(0, tr(" Priority bits: ") + Utility::formatNumber( (uint64_t)FilterUtility::getGMLanPriorityBits(targettedID))); baseNode->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr(" Arbitration Id: ") + Utility::formatNumber( FilterUtility::getGMLanArbitrationId(targettedID))); + tempItem->setText(0, tr(" Arbitration Id: ") + Utility::formatNumber( (uint64_t)FilterUtility::getGMLanArbitrationId(targettedID))); baseNode->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr(" Sender Id: ") + Utility::formatNumber( FilterUtility::getGMLanSenderId(targettedID))); + tempItem->setText(0, tr(" Sender Id: ") + Utility::formatNumber( (uint64_t)FilterUtility::getGMLanSenderId(targettedID))); baseNode->addChild(tempItem); } @@ -506,7 +506,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) dataBase->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr("Range: ") + Utility::formatNumber(minData[c]) + tr(" to ") + Utility::formatNumber(maxData[c])); + tempItem->setText(0, tr("Range: ") + Utility::formatNumber((char)minData[c]) + tr(" to ") + Utility::formatNumber((char)maxData[c])); dataBase->addChild(tempItem); histBase->setText(0, tr("Histogram")); dataBase->addChild(histBase); diff --git a/re/graphingwindow.cpp b/re/graphingwindow.cpp index 6a709ab..8849183 100644 --- a/re/graphingwindow.cpp +++ b/re/graphingwindow.cpp @@ -1153,7 +1153,7 @@ void GraphingWindow::createGraph(GraphParams ¶ms, bool createGraphParam) } else { - params.x[j] = frameCache[k].timeStamp.microSeconds(); + params.x[j] = frameCache[k].timeStamp().microSeconds(); } params.y[j] = (tempVal * params.scale) + params.bias; if (params.y[j] < yminval) yminval = params.y[j]; diff --git a/re/isotp_interpreterwindow.cpp b/re/isotp_interpreterwindow.cpp index 3aeb8fc..5fe264b 100644 --- a/re/isotp_interpreterwindow.cpp +++ b/re/isotp_interpreterwindow.cpp @@ -218,7 +218,7 @@ void ISOTP_InterpreterWindow::showDetailView() buildString.append(tr("Raw Payload: ")); for (int i = 0; i < messages[rowNum].data.count(); i++) { - buildString.append(Utility::formatNumber(messages[rowNum].data[i])); + buildString.append(Utility::formatNumber((char)messages[rowNum].data[i])); buildString.append(" "); } buildString.append("\r\r"); @@ -291,7 +291,7 @@ void ISOTP_InterpreterWindow::newISOMessage(ISOTP_MESSAGE msg) for (int i = 0; i < msg.data.count(); i++) { - tempString.append(Utility::formatNumber(msg.data[i])); + tempString.append(Utility::formatNumber((char)msg.data[i])); tempString.append(" "); } ui->tableIsoFrames->setItem(rowNum, 5, new QTableWidgetItem(tempString)); diff --git a/re/sniffer/SnifferDelegate.cpp b/re/sniffer/SnifferDelegate.cpp index bd594a0..f9358d6 100644 --- a/re/sniffer/SnifferDelegate.cpp +++ b/re/sniffer/SnifferDelegate.cpp @@ -116,7 +116,7 @@ void SnifferDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio painter->setOpacity(1.0); painter->setPen(QApplication::palette().color(QPalette::Text)); painter->setFont(mainFont); - painter->drawText(QRect(viewport.left(), viewport.top() + xSector + yOffset, xSpan, mainFontInfo->pixelSize()), Qt::AlignCenter, Utility::formatNumber(val)); + painter->drawText(QRect(viewport.left(), viewport.top() + xSector + yOffset, xSpan, mainFontInfo->pixelSize()), Qt::AlignCenter, Utility::formatNumber((char)val)); } QSize SnifferDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const diff --git a/re/sniffer/snifferitem.cpp b/re/sniffer/snifferitem.cpp index 2267835..8e102eb 100644 --- a/re/sniffer/snifferitem.cpp +++ b/re/sniffer/snifferitem.cpp @@ -4,7 +4,7 @@ SnifferItem::SnifferItem(const CANFrame& pFrame, quint32 seq): - mID(pFrame.ID) + mID(pFrame.frameId()) { for (int i = 0; i < 8; i++) { mNotch[i] =0; diff --git a/scriptcontainer.cpp b/scriptcontainer.cpp index 83c64d9..c3ef718 100644 --- a/scriptcontainer.cpp +++ b/scriptcontainer.cpp @@ -230,23 +230,21 @@ void CANScriptHelper::clearFilters() void CANScriptHelper::sendFrame(QJSValue bus, QJSValue id, QJSValue length, QJSValue data) { CANFrame frame; - frame.extended = false; - frame.remote = false; - frame.ID = static_cast(id.toInt()); - frame.len = length.toUInt(); - if (frame.len > 8) frame.len = 8; + frame.setExtendedFrameFormat(false); + frame.setFrameId(static_cast(id.toInt())); + QByteArray bytes(length.toUInt(), 0); if (!data.isArray()) qDebug() << "data isn't an array"; - for (unsigned int i = 0; i < frame.len; i++) + for (unsigned int i = 0; i < bytes.length(); i++) { - frame.data[i] = (uint8_t)data.property(i).toInt(); + bytes[i] = (uint8_t)data.property(i).toInt(); } - + frame.setPayload(bytes); frame.bus = (uint32_t)bus.toInt(); //if (frame.bus > 1) frame.bus = 1; - if (frame.ID > 0x7FF) frame.extended = true; + if (frame.frameId() > 0x7FF) frame.setExtendedFrameFormat(true); qDebug() << "sending frame from script"; CANConManager::getInstance()->sendFrame(frame); @@ -258,13 +256,13 @@ void CANScriptHelper::gotTargettedFrame(const CANFrame &frame) //qDebug() << "Got frame in script interface"; for (int i = 0; i < filters.length(); i++) { - if (filters[i].checkFilter(frame.ID, frame.bus)) + if (filters[i].checkFilter(frame.frameId(), frame.bus)) { QJSValueList args; - args << frame.bus << frame.ID << frame.len; - QJSValue dataBytes = scriptEngine->newArray(frame.len); + args << frame.bus << frame.frameId() << frame.payload().length(); + QJSValue dataBytes = scriptEngine->newArray(frame.payload().length()); - for (unsigned int j = 0; j < frame.len; j++) dataBytes.setProperty(j, QJSValue(frame.data[j])); + for (unsigned int j = 0; j < frame.payload().length(); j++) dataBytes.setProperty(j, QJSValue((unsigned char)frame.payload()[j])); args.append(dataBytes); gotFrameFunction.call(args); return; //as soon as one filter matches we jump out diff --git a/signalviewerwindow.cpp b/signalviewerwindow.cpp index a093bee..c401b49 100644 --- a/signalviewerwindow.cpp +++ b/signalviewerwindow.cpp @@ -71,7 +71,7 @@ void SignalViewerWindow::processFrame(CANFrame &frame) { sig = signalList.at(i); if (!sig) return; - if (sig->parentMessage->ID == frame.ID) + if (sig->parentMessage->ID == frame.frameId()) { if (sig->processAsText(frame, sigString, false)) //if true we could interpret the signal so update it in the list { diff --git a/utility.h b/utility.h index 8160aa7..d4f7f62 100644 --- a/utility.h +++ b/utility.h @@ -97,6 +97,15 @@ public: else return formatHexNum(value); } + static QString formatNumber(char value) + { + if (decimalMode) + { + return QString::number((unsigned char)value, 10); + } + else return formatHexNum((unsigned char)value); + } + static QString formatCANID(uint64_t id, bool extended) { if (decimalMode) return QString::number(id, 10); From 8beb0e31a085b5df30418d80add17b41ba52130b Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 15 Dec 2019 15:02:06 -0500 Subject: [PATCH 04/58] Converted more code to be easier to generalize to larger CAN payloads for CAN-FD. Improved some code to be faster. Fixed up some compile time warnings. --- blfhandler.cpp | 4 +- bus_protocols/isotp_handler.cpp | 76 +++---- bus_protocols/isotp_handler.h | 2 +- bus_protocols/isotp_message.h | 15 +- bus_protocols/uds_handler.cpp | 111 +++++----- canframemodel.cpp | 31 ++- canframemodel.h | 2 +- connections/canconnectionmodel.cpp | 6 +- connections/connectionwindow.cpp | 6 +- connections/gvretserial.cpp | 60 ++--- dbc/dbc_classes.cpp | 2 +- filterutility.cpp | 12 +- filterutility.h | 12 +- firmwareuploaderwindow.cpp | 17 +- framefileio.cpp | 341 +++++++++++++++++++---------- frameplaybackwindow.cpp | 6 +- framesenderwindow.cpp | 20 +- mainwindow.cpp | 29 ++- re/filecomparatorwindow.cpp | 36 +-- re/flowviewwindow.cpp | 46 ++-- re/flowviewwindow.h | 2 +- re/frameinfowindow.cpp | 27 ++- re/isotp_interpreterwindow.cpp | 42 ++-- re/sniffer/snifferitem.cpp | 26 ++- re/udsscanwindow.cpp | 24 +- scriptcontainer.cpp | 59 ++--- scriptingwindow.cpp | 1 - utility.h | 9 - 28 files changed, 589 insertions(+), 435 deletions(-) diff --git a/blfhandler.cpp b/blfhandler.cpp index 7192854..9c3fb1a 100644 --- a/blfhandler.cpp +++ b/blfhandler.cpp @@ -78,7 +78,7 @@ bool BLFHandler::loadBLF(QString filename, QVector* frames) } qDebug() << "Uncompressed size: " << uncompressedData.count(); pos = 0; - bool foundHeader = false; + //bool foundHeader = false; //first skip forward to find a header signature - usually not necessary while ( (int)(pos + sizeof(BLF_OBJ_HEADER)) < uncompressedData.count()) { @@ -163,5 +163,7 @@ bool BLFHandler::loadBLF(QString filename, QVector* frames) bool BLFHandler::saveBLF(QString filename, QVector *frames) { + Q_UNUSED(filename) + Q_UNUSED(frames) return false; } diff --git a/bus_protocols/isotp_handler.cpp b/bus_protocols/isotp_handler.cpp index 9534b26..d56477c 100644 --- a/bus_protocols/isotp_handler.cpp +++ b/bus_protocols/isotp_handler.cpp @@ -47,7 +47,7 @@ void ISOTP_HANDLER::setReception(bool mode) } } -void ISOTP_HANDLER::sendISOTPFrame(int bus, int ID, QVector data) +void ISOTP_HANDLER::sendISOTPFrame(int bus, int ID, QByteArray data) { CANFrame frame; frame.setFrameType(QCanBusFrame::DataFrame); @@ -159,6 +159,8 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) //int offset; ISOTP_MESSAGE msg; ISOTP_MESSAGE *pMsg; + unsigned char *data = reinterpret_cast(frame.payload().data()); + //int dataLen = frame.payload().count(); frameType = 0; frameLen = 0; @@ -166,14 +168,14 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) if (useExtendedAddressing) { ID = ID << 8; - ID += frame.payload()[0]; - frameType = frame.payload()[1] >> 4; - frameLen = frame.payload()[1] & 0xF; + ID += data[0]; + frameType = data[1] >> 4; + frameLen = data[1] & 0xF; } else { - frameType = frame.payload()[0] >> 4; - frameLen = frame.payload()[0] & 0xF; + frameType = data[0] >> 4; + frameLen = data[0] & 0xF; } switch(frameType) @@ -186,42 +188,42 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) if (frameLen > 7) return; msg.bus = frame.bus; - msg.extended = frame.hasExtendedFrameFormat(); - msg.ID = ID; + msg.setExtendedFrameFormat( frame.hasExtendedFrameFormat() ); + msg.setFrameId(ID); msg.isReceived = frame.isReceived; - msg.len = frameLen; - msg.data.reserve(frameLen); - msg.timestamp = frame.timeStamp().microSeconds(); + msg.payload().reserve(frameLen); + msg.reportedLength = frameLen; + msg.setTimeStamp(frame.timeStamp()); msg.isMultiframe = false; - if (useExtendedAddressing) for (int j = 0; j < frameLen; j++) msg.data.append(frame.payload()[j+2]); - else for (int j = 0; j < frameLen; j++) msg.data.append(frame.payload()[j+1]); + if (useExtendedAddressing) for (int j = 0; j < frameLen; j++) msg.payload().append(frame.payload()[j+2]); + else for (int j = 0; j < frameLen; j++) msg.payload().append(frame.payload()[j+1]); //qDebug() << "Emitting single frame ISOTP message"; emit newISOMessage(msg); break; case 1: //first frame of a multi-frame message checkNeedFlush(ID); msg.bus = frame.bus; - msg.extended = frame.hasExtendedFrameFormat(); - msg.ID = ID; - msg.timestamp = frame.timeStamp().microSeconds(); + msg.setExtendedFrameFormat( frame.hasExtendedFrameFormat() ); + msg.setFrameId(ID); + msg.setTimeStamp(frame.timeStamp()); msg.isReceived = frame.isReceived; msg.isMultiframe = true; frameLen = frameLen << 8; if (useExtendedAddressing) { - frameLen += frame.payload()[2]; + frameLen += data[2]; frameLen = frameLen & 0xFFF; - msg.len = frameLen; - msg.data.reserve(frameLen); - for (int j = 0; j < 5; j++) msg.data.append(frame.payload()[3 + j]); + msg.payload().reserve(frameLen); + msg.reportedLength = frameLen; + for (int j = 0; j < 5; j++) msg.payload().append(frame.payload()[3 + j]); } else { - frameLen += frame.payload()[1]; + frameLen += data[1]; frameLen = frameLen & 0xFFF; - msg.len = frameLen; - msg.data.reserve(frameLen); - for (int j = 0; j < 6; j++) msg.data.append(frame.payload()[2 + j]); + msg.payload().reserve(frameLen); + msg.reportedLength = frameLen; + for (int j = 0; j < 6; j++) msg.payload().append(frame.payload()[2 + j]); } msg.lastSequence = -1; messageBuffer.append(msg); @@ -245,7 +247,7 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) pMsg = nullptr; for (int i = 0; i < messageBuffer.length(); i++) { - if (messageBuffer[i].ID == ID) + if (messageBuffer[i].frameId() == ID) { pMsg = &messageBuffer[i]; break; @@ -253,22 +255,22 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) } if (!pMsg) return; if (!pMsg->isMultiframe) return; //if we didn't get a frame type 1 (start of multiframe) first then ignore this frame. - ln = pMsg->len - pMsg->data.count(); + ln = pMsg->payload().length() - pMsg->payload().count(); //offset = pMsg->data.count(); if (useExtendedAddressing) { if (ln > 6) ln = 6; - for (int j = 0; j < ln; j++) pMsg->data.append(frame.payload()[j+2]); + for (int j = 0; j < ln; j++) pMsg->payload().append(frame.payload()[j+2]); } else { if (ln > 7) ln = 7; - for (int j = 0; j < ln; j++) pMsg->data.append(frame.payload()[j+1]); + for (int j = 0; j < ln; j++) pMsg->payload().append(frame.payload()[j+1]); } - if (pMsg->len <= pMsg->data.count()) + if (pMsg->reportedLength <= pMsg->payload().count()) { //qDebug() << "Emitting multiframe ISOTP message"; - checkNeedFlush(pMsg->ID); + checkNeedFlush(pMsg->frameId()); } break; case 3: //flow control messages @@ -277,10 +279,10 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) case 0: //continue to send frames but maybe change inter-frame delay waitingForFlow = false; //data[1] contains number of frames to send before waiting for next flow control - framesUntilFlow = frame.payload()[1]; + framesUntilFlow = data[1]; if (framesUntilFlow == 0) framesUntilFlow = -1; //-1 means don't count frames and just keep going //data[2] contains the interframe delay to use (0xF1 through 0xF9 are special through) - if (frame.payload()[2] < (char)0xF1) frameTimer.start(frame.payload()[2]); //set proper delay between frames + if (data[2] < 0xF1) frameTimer.start(data[2]); //set proper delay between frames else frameTimer.start(1); //can't do sub-millisecond sending with this code so just use 1ms timing break; case 1: //wait - do not send any more frames until other side says so @@ -303,18 +305,18 @@ void ISOTP_HANDLER::checkNeedFlush(uint64_t ID) { for (int i = 0; i < messageBuffer.length(); i++) { - if (messageBuffer[i].ID == ID) + if (messageBuffer[i].frameId() == ID) { //used to pass by reference but now newISOMessage should pass by value which makes it easier to use cross thread - if (messageBuffer[i].ID > 0x600 && messageBuffer[i].ID < 0x630) + if (messageBuffer[i].frameId() > 0x600 && messageBuffer[i].frameId() < 0x630) { - if (messageBuffer[i].len <= messageBuffer[i].data.count()) + if (messageBuffer[i].reportedLength <= messageBuffer[i].payload().count()) { - qDebug() << "Flushing full frame" << QString::number(messageBuffer[i].ID, 16) << " " << messageBuffer[i].len << " " << messageBuffer[i].data.count(); + qDebug() << "Flushing full frame" << QString::number(messageBuffer[i].frameId(), 16) << " " << messageBuffer[i].reportedLength << " " << messageBuffer[i].payload().count(); } else { - qDebug() << "Flushing a partial frame " << QString::number(messageBuffer[i].ID, 16) << " " << messageBuffer[i].len << " " << messageBuffer[i].data.count(); + qDebug() << "Flushing a partial frame " << QString::number(messageBuffer[i].frameId(), 16) << " " << messageBuffer[i].reportedLength << " " << messageBuffer[i].payload().count(); } } emit newISOMessage(messageBuffer[i]); diff --git a/bus_protocols/isotp_handler.h b/bus_protocols/isotp_handler.h index 7cdc16d..72ada5e 100644 --- a/bus_protocols/isotp_handler.h +++ b/bus_protocols/isotp_handler.h @@ -19,7 +19,7 @@ public: ~ISOTP_HANDLER(); void setExtendedAddressing(bool mode); void setReception(bool mode); //set whether to accept and forward frames or not - void sendISOTPFrame(int bus, int ID, QVector data); + void sendISOTPFrame(int bus, int ID, QByteArray data); void setProcessAll(bool state); void setFlowCtrl(bool state); void addFilter(uint32_t pBusId, uint32_t ID, uint32_t mask); diff --git a/bus_protocols/isotp_message.h b/bus_protocols/isotp_message.h index 5e59580..788a069 100644 --- a/bus_protocols/isotp_message.h +++ b/bus_protocols/isotp_message.h @@ -3,21 +3,16 @@ #include #include +#include -//the same as the CANFrame struct but with arbitrary data size. -class ISOTP_MESSAGE +//Now a child class of CANFrame. We just add the ability to track how long it was supposed to be and other +//ISOTP related details. But, mostly just CANFrame. +class ISOTP_MESSAGE : public CANFrame { public: - uint32_t ID; - int bus; - bool extended; - bool isReceived; - int len; //# of bytes this message should have (as reported) - int actualSize; //# we actually got + int reportedLength; int lastSequence; bool isMultiframe; - QVector data; - uint64_t timestamp; }; #endif // ISOTP_MESSAGE_H diff --git a/bus_protocols/uds_handler.cpp b/bus_protocols/uds_handler.cpp index 5508800..e0fb085 100644 --- a/bus_protocols/uds_handler.cpp +++ b/bus_protocols/uds_handler.cpp @@ -158,7 +158,6 @@ UDS_MESSAGE::UDS_MESSAGE() subFunc = 0; service = 0; subFuncLen = 1; - extended = false; isErrorReply = false; } @@ -178,39 +177,39 @@ UDS_HANDLER::~UDS_HANDLER() void UDS_HANDLER::gotISOTPFrame(ISOTP_MESSAGE msg) { qDebug() << "UDS handler got ISOTP frame"; + unsigned char *data = reinterpret_cast(msg.payload().data()); + int dataLen = msg.payload().count(); UDS_MESSAGE udsMsg; udsMsg.bus = msg.bus; - udsMsg.extended = msg.extended; - udsMsg.ID = msg.ID; + udsMsg.setExtendedFrameFormat(msg.hasExtendedFrameFormat()); + udsMsg.setFrameId(msg.frameId()); udsMsg.isReceived = msg.isReceived; - udsMsg.timestamp = msg.timestamp; - udsMsg.actualSize = msg.actualSize; + udsMsg.setTimeStamp(msg.timeStamp()); + udsMsg.reportedLength = msg.reportedLength; udsMsg.service = 0; udsMsg.subFunc = 0; udsMsg.subFuncLen = 0; udsMsg.isErrorReply = false; - udsMsg.len = msg.len; - if (msg.data.length() > 0) { - udsMsg.service = msg.data.at(0); + udsMsg.setPayload(msg.payload()); + if (dataLen > 0) { + udsMsg.service = data[0]; if (udsMsg.service == 0x7F) { udsMsg.isErrorReply = true; - if (msg.data.length() > 1) + if (dataLen > 1) { - udsMsg.service = msg.data.at(1); - if (msg.data.length() > 2) udsMsg.subFunc = msg.data.at(2); + udsMsg.service = data[1]; + if (dataLen > 2) udsMsg.subFunc = data[2]; else return; } else return; - udsMsg.data = msg.data.mid(2, -1); //don't copy error byte nor service byte - udsMsg.len -= 2; //and remove those bytes from the length reported too. + udsMsg.payload().remove(0, 2); } else { udsMsg.isErrorReply = false; - if (msg.data.length() > 1) udsMsg.subFunc = msg.data.at(1); - udsMsg.data = msg.data.mid(1, -1); //don't copy service byte - udsMsg.len -= 1; //and remove service byte from length too. + if (dataLen > 1) udsMsg.subFunc = data[1]; + udsMsg.payload().remove(0, 1); } } else return; @@ -245,7 +244,7 @@ void UDS_HANDLER::setReception(bool mode) void UDS_HANDLER::sendUDSFrame(const UDS_MESSAGE &msg) { - QVector data; + QByteArray data; if (msg.bus < 0) return; if (msg.bus >= CANConManager::getInstance()->getNumBuses()) return; if (msg.service > 0xFF) return; @@ -256,8 +255,8 @@ void UDS_HANDLER::sendUDSFrame(const UDS_MESSAGE &msg) data.append((msg.subFunc >> (8 * b)) & 0xFF); } - data.append(msg.data); - isoHandler->sendISOTPFrame(msg.bus, msg.ID, data); + data.append(msg.payload()); + isoHandler->sendISOTPFrame(msg.bus, msg.frameId(), data); qDebug() << "Sent UDS service: " << getServiceShortDesc(msg.service) << " on bus " << msg.bus; } @@ -328,6 +327,8 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) bool isResponse = true; int dataSize; int addrSize; + unsigned char *data = reinterpret_cast(msg.payload().data()); + int dataLen = msg.payload().length(); if (msg.isErrorReply) { @@ -363,15 +364,15 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) case UDS_SERVICES::DIAG_CONTROL + 0x40: //positive response buildString.append("Session Request: " + getLongDesc(UDS_DIAG_CTRL_SUB, msg.subFunc)); //there should be four extra bytes now - if (msg.data.length() < 5) //5 because subfunc codes are left in data so it starts with one subfunc byte + if (dataLen < 5) //5 because subfunc codes are left in data so it starts with one subfunc byte { //buildString.append("\nReturned data payload wasn't at least \n4 bytes like it should have been"); } else { - int p2 = msg.data[1] * 256 + msg.data[2]; + int p2 = data[1] * 256 + data[2]; buildString.append("\nP2MAX (Max Wait / Resp Time): " + QString::number(p2) + "ms"); - p2 = (msg.data[3] * 256 + msg.data[4]) * 10; + p2 = (data[3] * 256 + data[4]) * 10; buildString.append("\nP2 Ext MAX: " + QString::number(p2) + "ms"); } break; @@ -382,11 +383,11 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) case UDS_SERVICES::ECU_RESET + 0x40: buildString.append("Reset Type: " + getLongDesc(UDS_ECU_RESET_SUB, msg.subFunc)); //There should be one additional byte which encodes power down time - if (msg.data.length() > 1) + if (dataLen > 1) { - if (msg.data[1] < 0xFF) + if (data[1] < 0xFF) { - buildString.append("\nMinimum powered down time: " + QString::number(msg.data[1])); + buildString.append("\nMinimum powered down time: " + QString::number(data[1])); } else buildString.append("\nPowerdown time not available"); } @@ -395,11 +396,11 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) case UDS_SERVICES::COMM_CTRL: //Comm control has potentially a lot of parameters. control type, comm type, nodeID buildString.append("Control type: " + getLongDesc(UDS_COMM_CTRL_SUB, msg.subFunc)); - if (msg.data.length() > 1) - buildString.append("\nComm Type: " + QString::number(msg.data[1])); //TODO: no attempt to interpret yet - if (msg.data.length() > 3) + if (dataLen > 1) + buildString.append("\nComm Type: " + QString::number(data[1])); //TODO: no attempt to interpret yet + if (dataLen > 3) { - int nodeID = (msg.data[2] * 256 + msg.data[3]); + int nodeID = (data[2] * 256 + data[3]); buildString.append("\nNode ID: " + Utility::formatHexNum(nodeID)); } break; @@ -407,19 +408,19 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) if ((msg.subFunc % 2) == 1) { buildString.append("Seed request for security level: " + QString::number(msg.subFunc) + "\n"); - if (msg.data.length()> 1) + if (dataLen > 1) { buildString.append("Data payload: "); - for (int j = 1; j < msg.data.length(); j++) buildString.append(Utility::formatHexNum(msg.data[j]) + " "); + for (int j = 1; j < dataLen; j++) buildString.append(Utility::formatHexNum(data[j]) + " "); } } else { buildString.append("Key sending for security level: " + QString::number(msg.subFunc - 1)); - if (msg.data.length()> 1) //and it sure as hell should be! + if (dataLen > 1) //and it sure as hell should be! { buildString.append("KEY: "); - for (int j = 1; j < msg.data.length(); j++) buildString.append(Utility::formatHexNum(msg.data[j]) + " "); + for (int j = 1; j < dataLen; j++) buildString.append(Utility::formatHexNum(data[j]) + " "); } } break; @@ -427,10 +428,10 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) if ((msg.subFunc % 2) == 1) { buildString.append("Seed response for security level: " + QString::number(msg.subFunc) + "\n"); - if (msg.data.length()> 1) //be kinda pointless if it weren't + if (dataLen > 1) //be kinda pointless if it weren't { buildString.append("SEED: "); - for (int j = 1; j < msg.data.length(); j++) buildString.append(Utility::formatHexNum(msg.data[j]) + " "); + for (int j = 1; j < dataLen; j++) buildString.append(Utility::formatHexNum(data[j]) + " "); } } else @@ -441,33 +442,33 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) break; case UDS_SERVICES::READ_BY_ID: //parameter is groups of two bytes, each of which specify an ID to read - if (msg.data.length() > 2) + if (dataLen > 2) { uint32_t id; - for (int i = 1; i < msg.data.length(); i = i + 2) + for (int i = 1; i < dataLen; i = i + 2) { - id = (msg.data[i] * 256) + msg.data[i+1]; + id = (data[i] * 256) + data[i+1]; buildString.append("\nID to read: " + Utility::formatHexNum(id)); } } break; case UDS_SERVICES::READ_BY_ID + 0x40: //reply buildString.append("Reply is non-standard and so no decoding is done. The format is (ID) followed by how ever much data that ID returns, followed by more ID/data pairs if applicable.\nPayload: "); - for (int i = 1; i < msg.data.length(); i++) + for (int i = 1; i < dataLen; i++) { - buildString.append(Utility::formatHexNum(msg.data[i]) + " "); + buildString.append(Utility::formatHexNum(data[i]) + " "); } break; case UDS_SERVICES::READ_BY_ADDR: //subfunc byte specifies address and length format, then address, then size dataSize = msg.subFunc >> 4; addrSize = msg.subFunc & 0xF; - if (msg.data.length() > (dataSize + addrSize)) + if (dataLen > (dataSize + addrSize)) { buildString.append("Address: 0x"); - for (int i = 0; i < addrSize; i++) buildString.append(QString::number(msg.data[1+i], 16).toUpper().rightJustified(2,'0')); + for (int i = 0; i < addrSize; i++) buildString.append(QString::number(data[1+i], 16).toUpper().rightJustified(2,'0')); buildString.append("\nSize: 0x"); - for (int i = 0; i < dataSize; i++) buildString.append(QString::number(msg.data[1+i+addrSize], 16).toUpper().rightJustified(2,'0')); + for (int i = 0; i < dataSize; i++) buildString.append(QString::number(data[1+i+addrSize], 16).toUpper().rightJustified(2,'0')); } else { @@ -476,44 +477,44 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) break; case UDS_SERVICES::READ_BY_ADDR + 0x40: buildString.append("Reply is a raw packet of data of the size requested.\nPayload: "); - for (int i = 1; i < msg.data.length(); i++) + for (int i = 1; i < dataLen; i++) { - buildString.append(Utility::formatHexNum(msg.data[i]) + " "); + buildString.append(Utility::formatHexNum(data[i]) + " "); } break; case UDS_SERVICES::WRITE_BY_ID: break; case UDS_SERVICES::ROUTINE_CTRL: buildString.append("Routine Control: " + getLongDesc(UDS_ROUTINE_SUB, msg.subFunc)); - if (msg.data.length() > 2) + if (dataLen > 2) { int routineID; - routineID = (msg.data[1] * 256 + msg.data[2]); + routineID = (data[1] * 256 + data[2]); buildString.append("\nRoutine ID: " + Utility::formatHexNum(routineID)); } - if (msg.data.length() > 3) + if (dataLen > 3) { buildString.append("\nParameter bytes to routine: "); - for (int i = 4; i < msg.data.length(); i++) + for (int i = 4; i < dataLen; i++) { - buildString.append(Utility::formatHexNum(msg.data[i]) + " "); + buildString.append(Utility::formatHexNum(data[i]) + " "); } } break; case UDS_SERVICES::ROUTINE_CTRL + 0x40: buildString.append("Routine Control: " + getLongDesc(UDS_ROUTINE_SUB, msg.subFunc)); - if (msg.data.length() > 2) + if (dataLen > 2) { int routineID; - routineID = (msg.data[1] * 256 + msg.data[2]); + routineID = (data[1] * 256 + data[2]); buildString.append("\nRoutine ID: " + Utility::formatHexNum(routineID)); } - if (msg.data.length() > 3) + if (dataLen > 3) { buildString.append("\nBytes returned by routine: "); - for (int i = 4; i < msg.data.length(); i++) + for (int i = 4; i < dataLen; i++) { - buildString.append(Utility::formatHexNum(msg.data[i]) + " "); + buildString.append(Utility::formatHexNum(data[i]) + " "); } } break; diff --git a/canframemodel.cpp b/canframemodel.cpp index 8a70ecc..167f822 100644 --- a/canframemodel.cpp +++ b/canframemodel.cpp @@ -347,7 +347,6 @@ void CANFrameModel::recalcOverwrite() QVariant CANFrameModel::data(const QModelIndex &index, int role) const { - int dLen; QString tempString; CANFrame thisFrame; static bool rowFlip = false; @@ -361,6 +360,9 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const thisFrame = filteredFrames.at(index.row()); + unsigned char *data = reinterpret_cast(thisFrame.payload().data()); + int dataLen = thisFrame.payload().count(); + if (role == Qt::BackgroundColorRole) { if (dbcHandler != nullptr && interpretFrames) @@ -422,7 +424,7 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const case Column::Bus: return QString::number(thisFrame.bus); case Column::Length: - return QString::number(thisFrame.payload().length()); + return QString::number(dataLen); case Column::ASCII: if (thisFrame.frameId() >= 0x7FFFFFF0ull) { @@ -430,32 +432,29 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const tempString.append(QString::number(thisFrame.frameId() & 0x7)); return tempString; } - dLen = thisFrame.payload().length(); - if (!thisFrame.frameType() != QCanBusFrame::RemoteRequestFrame) { - if (dLen < 0) dLen = 0; - if (dLen > 8) dLen = 8; - for (int i = 0; i < dLen; i++) + if (thisFrame.frameType() != QCanBusFrame::RemoteRequestFrame) { + if (dataLen < 0) dataLen = 0; + //if (dLen > 8) dLen = 8; + for (int i = 0; i < dataLen; i++) { - quint8 byt = thisFrame.payload()[i]; + char byt = thisFrame.payload()[i]; //0x20 through 0x7E are printable characters. Outside of that range they aren't. So use dots instead if (byt < 0x20) byt = 0x2E; //dot character if (byt > 0x7E) byt = 0x2E; - tempString.append(QString::fromUtf8((char *)&byt, 1)); + tempString.append(QString::fromUtf8(&byt, 1)); } } return tempString; case Column::Data: - dLen = thisFrame.payload().length(); - if (dLen < 0) dLen = 0; - if (dLen > 8) dLen = 8; + if (dataLen < 0) dataLen = 0; //if (useHexMode) tempString.append("0x "); if (thisFrame.frameType() == QCanBusFrame::RemoteRequestFrame) { return tempString; } - for (int i = 0; i < dLen; i++) + for (int i = 0; i < dataLen; i++) { - if (useHexMode) tempString.append( QString::number((unsigned char)thisFrame.payload()[i], 16).toUpper().rightJustified(2, '0')); - else tempString.append(QString::number(thisFrame.payload()[i], 10)); + if (useHexMode) tempString.append( QString::number(data[i], 16).toUpper().rightJustified(2, '0')); + else tempString.append(QString::number(data[i], 10)); tempString.append(" "); } //now, if we're supposed to interpret the data and the DBC handler is loaded then use it @@ -740,7 +739,7 @@ void CANFrameModel::insertFrames(const QVector &newFrames) int CANFrameModel::getIndexFromTimeID(unsigned int ID, double timestamp) { int bestIndex = -1; - uint64_t intTimeStamp = timestamp * 1000000l; + int64_t intTimeStamp = static_cast (timestamp * 1000000l); for (int i = 0; i < frames.count(); i++) { if ((frames[i].frameId() == ID)) diff --git a/canframemodel.h b/canframemodel.h index d01f03b..51762a1 100644 --- a/canframemodel.h +++ b/canframemodel.h @@ -88,7 +88,7 @@ private: bool timeSeconds; bool useSystemTime; bool needFilterRefresh; - uint64_t timeOffset; + int64_t timeOffset; int lastUpdateNumFrames; uint32_t preallocSize; bool sortDirAsc; diff --git a/connections/canconnectionmodel.cpp b/connections/canconnectionmodel.cpp index cbecb40..35a0c1e 100644 --- a/connections/canconnectionmodel.cpp +++ b/connections/canconnectionmodel.cpp @@ -70,10 +70,10 @@ QVariant CANConnectionModel::data(const QModelIndex &index, int role) const //qDebug() << "Row: " << index.row(); CANConnection *conn_p = getAtIdx(index.row()); - bool ret; + if (!conn_p) return QVariant(); - bool isSocketCAN = (conn_p->getType() == CANCon::SERIALBUS) ? true: false; + //bool isSocketCAN = (conn_p->getType() == CANCon::SERIALBUS) ? true: false; if (role == Qt::DisplayRole) { @@ -147,6 +147,8 @@ CANConnection* CANConnectionModel::getAtIdx(int pIdx) const void CANConnectionModel::refresh(int pIndex) { + Q_UNUSED(pIndex) + beginResetModel(); endResetModel(); /* diff --git a/connections/connectionwindow.cpp b/connections/connectionwindow.cpp index 56dff96..3a5fabf 100644 --- a/connections/connectionwindow.cpp +++ b/connections/connectionwindow.cpp @@ -312,8 +312,8 @@ void ConnectionWindow::populateBusDetails(int offset) } else { - bool ret; - int numBuses; + //bool ret; + //int numBuses; CANConnection* conn_p = connModel->getAtIdx(selIdx); CANBus bus; @@ -365,7 +365,7 @@ void ConnectionWindow::currentRowChanged(const QModelIndex ¤t, const QMode } else { - bool ret; + //bool ret; ui->groupBus->setEnabled(true); int numBuses; diff --git a/connections/gvretserial.cpp b/connections/gvretserial.cpp index 11708a2..d84a875 100644 --- a/connections/gvretserial.cpp +++ b/connections/gvretserial.cpp @@ -192,14 +192,14 @@ void GVRetSerial::piSetBusSettings(int pBusIdx, CANBus bus) sendDebug("Got signal to update bauds. 1: " + QString::number((can0Baud & 0xFFFFFFF)) + " 2: " + QString::number((can1Baud & 0xFFFFFFF))); buffer[0] = (char)0xF1; //start of a command over serial buffer[1] = 5; //setup canbus - buffer[2] = (unsigned char)(can0Baud & 0xFF); //four bytes of ID LSB first - buffer[3] = (unsigned char)(can0Baud >> 8); - buffer[4] = (unsigned char)(can0Baud >> 16); - buffer[5] = (unsigned char)(can0Baud >> 24); - buffer[6] = (unsigned char)(can1Baud & 0xFF); //four bytes of ID LSB first - buffer[7] = (unsigned char)(can1Baud >> 8); - buffer[8] = (unsigned char)(can1Baud >> 16); - buffer[9] = (unsigned char)(can1Baud >> 24); + buffer[2] = (char)(can0Baud & 0xFF); //four bytes of ID LSB first + buffer[3] = (char)(can0Baud >> 8); + buffer[4] = (char)(can0Baud >> 16); + buffer[5] = (char)(can0Baud >> 24); + buffer[6] = (char)(can1Baud & 0xFF); //four bytes of ID LSB first + buffer[7] = (char)(can1Baud >> 8); + buffer[8] = (char)(can1Baud >> 16); + buffer[9] = (char)(can1Baud >> 24); buffer[10] = 0; sendToSerial(buffer); } @@ -210,18 +210,18 @@ void GVRetSerial::piSetBusSettings(int pBusIdx, CANBus bus) sendDebug("Got signal to update extended bus speeds SWCAN: " + QString::number(swcanBaud) + " LIN1: " + QString::number(lin1Baud) + " LIN2: " + QString::number(lin2Baud)); buffer[0] = (char)0xF1; //start of a command over serial buffer[1] = 14; //setup extended buses - buffer[2] = (unsigned char)(swcanBaud & 0xFF); //four bytes of ID LSB first - buffer[3] = (unsigned char)(swcanBaud >> 8); - buffer[4] = (unsigned char)(swcanBaud >> 16); - buffer[5] = (unsigned char)(swcanBaud >> 24); - buffer[6] = (unsigned char)(lin1Baud & 0xFF); //four bytes of ID LSB first - buffer[7] = (unsigned char)(lin1Baud >> 8); - buffer[8] = (unsigned char)(lin1Baud >> 16); - buffer[9] = (unsigned char)(lin1Baud >> 24); - buffer[10] = (unsigned char)(lin2Baud & 0xFF); //four bytes of ID LSB first - buffer[11] = (unsigned char)(lin2Baud >> 8); - buffer[12] = (unsigned char)(lin2Baud >> 16); - buffer[13] = (unsigned char)(lin2Baud >> 24); + buffer[2] = (char)(swcanBaud & 0xFF); //four bytes of ID LSB first + buffer[3] = (char)(swcanBaud >> 8); + buffer[4] = (char)(swcanBaud >> 16); + buffer[5] = (char)(swcanBaud >> 24); + buffer[6] = (char)(lin1Baud & 0xFF); //four bytes of ID LSB first + buffer[7] = (char)(lin1Baud >> 8); + buffer[8] = (char)(lin1Baud >> 16); + buffer[9] = (char)(lin1Baud >> 24); + buffer[10] = (char)(lin2Baud & 0xFF); //four bytes of ID LSB first + buffer[11] = (char)(lin2Baud >> 8); + buffer[12] = (char)(lin2Baud >> 16); + buffer[13] = (char)(lin2Baud >> 24); buffer[14] = 0; sendToSerial(buffer); } @@ -231,8 +231,8 @@ void GVRetSerial::piSetBusSettings(int pBusIdx, CANBus bus) bool GVRetSerial::piSendFrame(const CANFrame& frame) { QByteArray buffer; - unsigned int c; - int ID; + int c; + quint32 ID; //qDebug() << "Sending out GVRET frame with id " << frame.ID << " on bus " << frame.bus; @@ -250,16 +250,16 @@ bool GVRetSerial::piSendFrame(const CANFrame& frame) return true; } ID = frame.frameId(); - if (frame.hasExtendedFrameFormat()) ID |= 1 << 31; + if (frame.hasExtendedFrameFormat()) ID |= 1u << 31; - buffer[0] = (unsigned char)0xF1; //start of a command over serial + buffer[0] = (char)0xF1; //start of a command over serial buffer[1] = 0; //command ID for sending a CANBUS frame - buffer[2] = (unsigned char)(ID & 0xFF); //four bytes of ID LSB first - buffer[3] = (unsigned char)(ID >> 8); - buffer[4] = (unsigned char)(ID >> 16); - buffer[5] = (unsigned char)(ID >> 24); - buffer[6] = (unsigned char)((frame.bus) & 3); - buffer[7] = (unsigned char)frame.payload().length(); + buffer[2] = (char)(ID & 0xFF); //four bytes of ID LSB first + buffer[3] = (char)(ID >> 8); + buffer[4] = (char)(ID >> 16); + buffer[5] = (char)(ID >> 24); + buffer[6] = (char)((frame.bus) & 3); + buffer[7] = (char)frame.payload().length(); for (c = 0; c < frame.payload().length(); c++) { buffer[8 + c] = frame.payload()[c]; diff --git a/dbc/dbc_classes.cpp b/dbc/dbc_classes.cpp index 003ea52..c0f11ac 100644 --- a/dbc/dbc_classes.cpp +++ b/dbc/dbc_classes.cpp @@ -47,7 +47,7 @@ bool DBC_SIGNAL::processAsText(const CANFrame &frame, QString &outString, bool o QString buildString; int startByte = startBit / 8; int bytes = signalSize / 8; - for (int x = 0; x < bytes; x++) buildString.append((char)frame.payload()[startByte + x]); + for (int x = 0; x < bytes; x++) buildString.append(frame.payload().data()[startByte + x]); outString = buildString; cachedValue = outString; return true; diff --git a/filterutility.cpp b/filterutility.cpp index a085993..b577c3d 100644 --- a/filterutility.cpp +++ b/filterutility.cpp @@ -22,22 +22,22 @@ QString FilterUtility::getId( QListWidgetItem * item ) return getId(item->text()); } -uint32_t FilterUtility::getGMLanArbitrationId(uint32_t id) +uint32_t FilterUtility::getGMLanArbitrationId(int32_t id) { return (id >> 13) & 0x1FFF; } -uint32_t FilterUtility::getGMLanPriorityBits(uint32_t id) +uint32_t FilterUtility::getGMLanPriorityBits(int32_t id) { return (id >> 26) & 0x7; } -uint32_t FilterUtility::getGMLanSenderId(uint32_t id) +uint32_t FilterUtility::getGMLanSenderId(int32_t id) { return id & 0x1FFF; } -QListWidgetItem * FilterUtility::createCheckableFilterItem(uint32_t id, bool checked, QListWidget* parent) +QListWidgetItem * FilterUtility::createCheckableFilterItem(int32_t id, bool checked, QListWidget* parent) { QListWidgetItem * thisItem = createFilterItem(id,parent); thisItem->setFlags(thisItem->flags() | Qt::ItemIsUserCheckable); @@ -48,7 +48,7 @@ QListWidgetItem * FilterUtility::createCheckableFilterItem(uint32_t id, bool che return thisItem; } -QListWidgetItem * FilterUtility::createFilterItem(uint32_t id, QListWidget* parent) +QListWidgetItem * FilterUtility::createFilterItem(int32_t id, QListWidget* parent) { QSettings settings; DBCHandler * dbcHandler = DBCHandler::getReference(); @@ -77,4 +77,4 @@ QListWidgetItem * FilterUtility::createFilterItem(uint32_t id, QListWidget* pare thisItem->setText(filterItemName); return thisItem; -} \ No newline at end of file +} diff --git a/filterutility.h b/filterutility.h index 3e0eeaa..e346c31 100644 --- a/filterutility.h +++ b/filterutility.h @@ -8,16 +8,16 @@ class FilterUtility { public: - static QListWidgetItem * createFilterItem(uint32_t id, QListWidget* parent=NULL); // if parent is given, add item automatically to listwidget - static QListWidgetItem * createCheckableFilterItem(uint32_t id, bool checked, QListWidget* parent=NULL); + static QListWidgetItem * createFilterItem(int32_t id, QListWidget* parent=NULL); // if parent is given, add item automatically to listwidget + static QListWidgetItem * createCheckableFilterItem(int32_t id, bool checked, QListWidget* parent=NULL); static uint32_t getIdAsInt( QListWidgetItem * item ); static QString getId( QListWidgetItem * item ); static QString getId( QString itemText ); - static uint32_t getGMLanArbitrationId(uint32_t id); - static uint32_t getGMLanSenderId(uint32_t id); - static uint32_t getGMLanPriorityBits(uint32_t id); + static uint32_t getGMLanArbitrationId(int32_t id); + static uint32_t getGMLanSenderId(int32_t id); + static uint32_t getGMLanPriorityBits(int32_t id); }; -#endif // DBCLOADSAVEWINDOW_H \ No newline at end of file +#endif // DBCLOADSAVEWINDOW_H diff --git a/firmwareuploaderwindow.cpp b/firmwareuploaderwindow.cpp index a94a55f..6480e25 100644 --- a/firmwareuploaderwindow.cpp +++ b/firmwareuploaderwindow.cpp @@ -77,17 +77,20 @@ void FirmwareUploaderWindow::updatedFrames(int numFrames) void FirmwareUploaderWindow::gotTargettedFrame(CANFrame frame) { + unsigned char *data = reinterpret_cast(frame.payload().data()); + int dataLen = frame.payload().count(); + qDebug() << "FUW: Got targetted frame with id " << frame.frameId(); - if (frame.frameId() == (uint32_t)(baseAddress + 0x10)) { + if (frame.frameId() == (uint32_t)(baseAddress + 0x10) && (dataLen == 8) ) { qDebug() << "Start firmware reply"; - if (((char)frame.payload()[0] == (char)0xAD) && ((char)frame.payload()[1] == (char)0xDE)) + if ((data[0] == 0xAD) && (data[1] == 0xDE)) { - if (((char)frame.payload()[2] == (char)0xAF) && ((char)frame.payload()[3] == (char)0xDE)) + if ((data[2] == 0xAF) && (data[3] == 0xDE)) { qDebug() << "There's dead beef here"; - if (((char)frame.payload()[4] == (char)(token & 0xFF)) && ((char)frame.payload()[5] == (char)((token >> 8) & 0xFF))) + if ( (data[4] == (token & 0xFF)) && (data[5] == ((token >> 8) & 0xFF) ) ) { - if (((char)frame.payload()[6] == (char)((token >> 16) & 0xFF)) && ((char)frame.payload()[7] == (char)((token >> 24) & 0xFF))) + if ((data[6] == ((token >> 16) & 0xFF)) && (data[7] == ((token >> 24) & 0xFF))) { qDebug() << "starting firmware process"; //MainWindow::getReference()->setTargettedID(baseAddress + 0x20); @@ -101,7 +104,7 @@ void FirmwareUploaderWindow::gotTargettedFrame(CANFrame frame) if (frame.frameId() == (uint32_t)(baseAddress + 0x20)) { qDebug() << "Firmware reception success reply"; - int seq = frame.payload()[0] + (256 * frame.payload()[1]); + int seq = data[0] + (256 * data[1]); if (seq == currentSendingPosition) { currentSendingPosition++; @@ -143,7 +146,7 @@ void FirmwareUploaderWindow::sendFirmwareChunk() output->payload()[3] = firmwareData[firmwareLocation++]; output->payload()[4] = firmwareData[firmwareLocation++]; output->payload()[5] = firmwareData[firmwareLocation++]; - for (int i = 0; i < 6; i++) xorByte = xorByte ^ output->payload()[i]; + for (int i = 0; i < 6; i++) xorByte = xorByte ^ static_cast(output->payload()[i]); output->payload()[6] = xorByte; output->setPayload(bytes); sendCANFrame(output); diff --git a/framefileio.cpp b/framefileio.cpp index 760c062..741d8b1 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -809,7 +809,7 @@ bool FrameFileIO::loadCANHackerFile(QString filename, QVector* frames) { if (tokens[d + 3] != "") { - bytes[d] = static_cast(tokens[d + 3].toInt(nullptr, 16)); + bytes[d] = static_cast(tokens[d + 3].toInt(nullptr, 16)); } else bytes[d] = 0; } @@ -927,7 +927,7 @@ bool FrameFileIO::loadCANOpenFile(QString filename, QVector* frames) { if (dataTok[d] != "") { - bytes[d] = static_cast(dataTok[d].simplified().toInt(nullptr, 16)); + bytes[d] = static_cast(dataTok[d].simplified().toInt(nullptr, 16)); } else bytes[d] = 0; } @@ -947,6 +947,10 @@ bool FrameFileIO::saveCRTDFile(QString filename, const QVector* frames QFile *outFile = new QFile(filename); int lineCounter = 0; + unsigned char *data; + int dataLen; + const CANFrame *frame; + if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) { delete outFile; @@ -966,24 +970,28 @@ bool FrameFileIO::saveCRTDFile(QString filename, const QVector* frames lineCounter = 0; } - outFile->write(QString::number(frames->at(c).timeStamp().microSeconds() / 1000000.0, 'f', 6).toUtf8()); + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); + + outFile->write(QString::number(frame->timeStamp().microSeconds() / 1000000.0, 'f', 6).toUtf8()); outFile->putChar(' '); - outFile->write(QString::number(frames->at(c).bus + 1).toUtf8()); - if (frames->at(c).isReceived) outFile->putChar('R'); + outFile->write(QString::number(frame->bus + 1).toUtf8()); + if (frame->isReceived) outFile->putChar('R'); else outFile->putChar('T'); - if (frames->at(c).hasExtendedFrameFormat()) + if (frame->hasExtendedFrameFormat()) { outFile->write("29 "); } else outFile->write("11 "); - outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); + outFile->write(QString::number(frame->frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); outFile->putChar(' '); - for (int temp = 0; temp < frames->at(c).payload().length(); temp++) + for (int temp = 0; temp < dataLen; temp++) { - outFile->write(QString::number(frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number(data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(' '); } @@ -1136,7 +1144,7 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) { if (tokens[d] != "") { - bytes[d] = static_cast(tokens[d].toInt(nullptr, 16)); + bytes[d] = static_cast(tokens[d].toInt(nullptr, 16)); } else bytes[d] = 0; } @@ -1169,7 +1177,7 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) { if (tokens[d] != "") { - bytes[d] = static_cast(tokens[d].toInt(nullptr, 16)); + bytes[d] = static_cast(tokens[d].toInt(nullptr, 16)); } else bytes[d] = 0; } @@ -1313,7 +1321,7 @@ bool FrameFileIO::loadCanalyzerASC(QString filename, QVector* frames) { if (tokens.count() > d) { - bytes[d - 6] = static_cast(tokens[d].toInt(nullptr, 16)); + bytes[d - 6] = static_cast(tokens[d].toInt(nullptr, 16)); } else //expected byte wasn't there to read. Set it zero and set error flag { @@ -1336,7 +1344,12 @@ bool FrameFileIO::saveCanalyzerASC(QString filename, const QVector* fr { QFile *outFile = new QFile(filename); int lineCounter = 0; - uint64_t offsetTime = frames->at(0).timeStamp().microSeconds(); + int64_t offsetTime = frames->at(0).timeStamp().microSeconds(); + + unsigned char *data; + int dataLen; + const CANFrame *frame; + for (int c = 0; c < frames->count(); c++) { if (frames->at(c).timeStamp().microSeconds() < offsetTime) offsetTime = frames->at(c).timeStamp().microSeconds(); @@ -1369,24 +1382,28 @@ bool FrameFileIO::saveCanalyzerASC(QString filename, const QVector* fr lineCounter = 0; } - uint64_t timeStamp = (frames->at(c).timeStamp().microSeconds() - offsetTime) / 1000000ull; + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); + + uint64_t timeStamp = (frame->timeStamp().microSeconds() - offsetTime) / 1000000ull; int tsLen = QString::number(timeStamp).length(); int precision = 6; //vector seems to keep 10 bytes at the start of the line for the timestamp. It should never exceed this //and there should never be a precision over 6 digits after the decimal if (tsLen > 3) precision = 9 - tsLen; - outFile->write(QString::number((frames->at(c).timeStamp().microSeconds() - offsetTime) / 1000000.0, 'f', precision).rightJustified(10, ' ').toUtf8()); + outFile->write(QString::number((frame->timeStamp().microSeconds() - offsetTime) / 1000000.0, 'f', precision).rightJustified(10, ' ').toUtf8()); outFile->putChar(' '); - outFile->write(QString::number(frames->at(c).bus + 1).toUtf8()); + outFile->write(QString::number(frame->bus + 1).toUtf8()); outFile->write(" "); if (frames->at(c).hasExtendedFrameFormat()) { - outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); + outFile->write(QString::number(frame->frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); outFile->write("x"); } else { - outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(3, '0').toUtf8()); + outFile->write(QString::number(frame->frameId(), 16).toUpper().rightJustified(3, '0').toUtf8()); outFile->write(" "); } outFile->write(" "); @@ -1397,12 +1414,12 @@ bool FrameFileIO::saveCanalyzerASC(QString filename, const QVector* fr if (frames->at(c).frameType() == QCanBusFrame::RemoteRequestFrame) outFile->write("r "); else outFile->write("d "); - outFile->write(QString::number(frames->at(c).payload().length()).toUtf8()); + outFile->write(QString::number(dataLen).toUtf8()); outFile->write(" "); - for (int temp = 0; temp < frames->at(c).payload().length(); temp++) + for (int temp = 0; temp < dataLen; temp++) { - outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number(data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->write(" "); } @@ -1563,7 +1580,7 @@ bool FrameFileIO::loadNativeCSVFile(QString filename, QVector* frames) QByteArray bytes(lng, 0); for (int c = 0; c < 8; c++) bytes[c] = 0; for (int d = 0; d < thisFrame.payload().length(); d++) - bytes[d] = static_cast(tokens[5 + d].toInt(nullptr, 16)); + bytes[d] = static_cast(tokens[5 + d].toInt(nullptr, 16)); thisFrame.setPayload(bytes); } else if (fileVersion == 2) @@ -1578,7 +1595,7 @@ bool FrameFileIO::loadNativeCSVFile(QString filename, QVector* frames) QByteArray bytes(lng, 0); for (int c = 0; c < 8; c++) bytes[c] = 0; for (int d = 0; d < thisFrame.payload().length(); d++) - bytes[d] = static_cast(tokens[6 + d].toInt(nullptr, 16)); + bytes[d] = static_cast(tokens[6 + d].toInt(nullptr, 16)); thisFrame.setPayload(bytes); } @@ -1597,6 +1614,10 @@ bool FrameFileIO::saveNativeCSVFile(QString filename, const QVector* f QFile *outFile = new QFile(filename); int lineCounter = 0; + unsigned char *data; + int dataLen; + const CANFrame *frame; + if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) { delete outFile; @@ -1615,28 +1636,32 @@ bool FrameFileIO::saveNativeCSVFile(QString filename, const QVector* f lineCounter = 0; } - outFile->write(QString::number(frames->at(c).timeStamp().microSeconds()).toUtf8()); + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); + + outFile->write(QString::number(frame->timeStamp().microSeconds()).toUtf8()); outFile->putChar(44); - outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); + outFile->write(QString::number(frame->frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); outFile->putChar(44); - if (frames->at(c).hasExtendedFrameFormat()) outFile->write("true,"); + if (frame->hasExtendedFrameFormat()) outFile->write("true,"); else outFile->write("false,"); - if (frames->at(c).isReceived) outFile->write("Rx,"); + if (frame->isReceived) outFile->write("Rx,"); else outFile->write("Tx,"); - outFile->write(QString::number(frames->at(c).bus).toUtf8()); + outFile->write(QString::number(frame->bus).toUtf8()); outFile->putChar(44); - outFile->write(QString::number(frames->at(c).payload().length()).toUtf8()); + outFile->write(QString::number(dataLen).toUtf8()); outFile->putChar(44); for (int temp = 0; temp < 8; temp++) { - if (temp < frames->at(c).payload().length()) - outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + if (temp < dataLen) + outFile->write(QString::number(data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); else outFile->write("00"); outFile->putChar(44); @@ -1694,32 +1719,41 @@ bool FrameFileIO::closeContinuousNative() bool FrameFileIO::writeContinuousNative(const QVector* frames, int beginningFrame) { + + unsigned char *data; + int dataLen; + const CANFrame *frame; + if (!continuousFile.isOpen()) return false; qDebug() << "Bgn: " << beginningFrame << " Count: " << frames->count(); for (int c = beginningFrame; c < frames->count(); c++) { - continuousFile.write(QString::number(frames->at(c).timeStamp().microSeconds()).toUtf8()); + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); + + continuousFile.write(QString::number(frame->timeStamp().microSeconds()).toUtf8()); continuousFile.putChar(44); - continuousFile.write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); + continuousFile.write(QString::number(frame->frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); continuousFile.putChar(44); - if (frames->at(c).hasExtendedFrameFormat()) continuousFile.write("true,"); + if (frame->hasExtendedFrameFormat()) continuousFile.write("true,"); else continuousFile.write("false,"); - if (frames->at(c).isReceived) continuousFile.write("Rx,"); + if (frame->isReceived) continuousFile.write("Rx,"); else continuousFile.write("Tx,"); - continuousFile.write(QString::number(frames->at(c).bus).toUtf8()); + continuousFile.write(QString::number(frame->bus).toUtf8()); continuousFile.putChar(44); - continuousFile.write(QString::number(frames->at(c).payload().length()).toUtf8()); + continuousFile.write(QString::number(dataLen).toUtf8()); continuousFile.putChar(44); for (int temp = 0; temp < 8; temp++) { - if (temp < frames->at(c).payload().length()) - continuousFile.write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + if (temp < dataLen) + continuousFile.write(QString::number(data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); else continuousFile.write("00"); continuousFile.putChar(44); @@ -1829,7 +1863,7 @@ bool FrameFileIO::loadGenericCSVFile(QString filename, QVector* frames thisFrame.setFrameType(QCanBusFrame::DataFrame); QList dataTok = tokens[1].split(' '); QByteArray bytes(dataTok.length(), 0); - for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast(dataTok[d].toInt(nullptr, 16)); + for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast(dataTok[d].toInt(nullptr, 16)); thisFrame.setPayload(bytes); frames->append(thisFrame); } @@ -1846,6 +1880,10 @@ bool FrameFileIO::saveGenericCSVFile(QString filename, const QVector* QFile *outFile = new QFile(filename); int lineCounter = 0; + unsigned char *data; + int dataLen; + const CANFrame *frame; + if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) { delete outFile; @@ -1864,12 +1902,16 @@ bool FrameFileIO::saveGenericCSVFile(QString filename, const QVector* lineCounter = 0; } - outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); + + outFile->write(QString::number(frame->frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); outFile->putChar(44); - for (int temp = 0; temp < frames->at(c).payload().length(); temp++) + for (int temp = 0; temp < dataLen; temp++) { - outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number(data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(' '); } @@ -2025,7 +2067,7 @@ bool FrameFileIO::loadLogFile(QString filename, QVector* frames) QByteArray bytes(lng, 0); if (thisFrame.frameType() != QCanBusFrame::RemoteRequestFrame) { for (int d = 0; d < thisFrame.payload().length(); d++) - bytes[d] = static_cast(tokens[d + 6].toInt(nullptr, 16)); + bytes[d] = static_cast(tokens[d + 6].toInt(nullptr, 16)); } thisFrame.setPayload(bytes); frames->append(thisFrame); @@ -2044,6 +2086,10 @@ bool FrameFileIO::saveLogFile(QString filename, const QVector* frames) QDateTime timestamp, tempStamp; int lineCounter = 0; + unsigned char *data; + int dataLen; + const CANFrame *frame; + //timestamp = QDateTime::currentDateTime(); if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) @@ -2077,28 +2123,32 @@ bool FrameFileIO::saveLogFile(QString filename, const QVector* frames) lineCounter = 0; } - tempStamp = QDateTime::fromMSecsSinceEpoch(frames->at(c).timeStamp().microSeconds() / 1000); + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); + + tempStamp = QDateTime::fromMSecsSinceEpoch(frame->timeStamp().microSeconds() / 1000); outFile->write(tempStamp.toString("hh:mm:ss:zzz").toUtf8()); - if (frames->at(c).isReceived) outFile->write(" Rx "); + if (frame->isReceived) outFile->write(" Rx "); else outFile->write(" Tx "); // busmaster channel start at 1 - outFile->write(QString::number(frames->at(c).bus+1).toUtf8() + " "); + outFile->write(QString::number(frame->bus + 1).toUtf8() + " "); outFile->write("0x"); - if (frames->at(c).hasExtendedFrameFormat() && frames->at(c).frameId() > 0x7FF) { - outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); + if (frame->hasExtendedFrameFormat() && frame->frameId() > 0x7FF) { + outFile->write(QString::number(frame->frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); } else { - outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(3, '0').toUtf8()); + outFile->write(QString::number(frame->frameId(), 16).toUpper().rightJustified(3, '0').toUtf8()); } - if (frames->at(c).hasExtendedFrameFormat()) outFile->write(" x"); + if (frame->hasExtendedFrameFormat()) outFile->write(" x"); else outFile->write(" s"); - if (frames->at(c).frameType() == QCanBusFrame::RemoteRequestFrame) outFile->write("r "); + if (frame->frameType() == QCanBusFrame::RemoteRequestFrame) outFile->write("r "); else outFile->write(" "); - outFile->write(QString::number(frames->at(c).payload().length()).toUtf8() + " "); + outFile->write(QString::number(dataLen).toUtf8() + " "); - if (frames->at(c).frameType() != QCanBusFrame::RemoteRequestFrame) { - for (int temp = 0; temp < frames->at(c).payload().length(); temp++) + if (frame->frameType() != QCanBusFrame::RemoteRequestFrame) { + for (int temp = 0; temp < dataLen; temp++) { - outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number(data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(' '); } } @@ -2214,7 +2264,7 @@ bool FrameFileIO::loadIXXATFile(QString filename, QVector* frames) QStringList dataToks = Utility::unQuote(tokens[4]).simplified().split(' '); QByteArray bytes(dataToks.length(), 0); if (thisFrame.payload().length() > 8) return false; - for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast(dataToks[d].toInt(nullptr, 16)); + for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast(dataToks[d].toInt(nullptr, 16)); thisFrame.setPayload(bytes); frames->append(thisFrame); } @@ -2232,6 +2282,10 @@ bool FrameFileIO::saveIXXATFile(QString filename, const QVector* frame QDateTime timestamp, tempStamp; int lineCounter = 0; + unsigned char *data; + int dataLen; + const CANFrame *frame; + timestamp = QDateTime::currentDateTime(); if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) @@ -2257,17 +2311,22 @@ bool FrameFileIO::saveIXXATFile(QString filename, const QVector* frame qApp->processEvents(); lineCounter = 0; } - tempStamp = QDateTime::fromMSecsSinceEpoch(frames->at(c).timeStamp().microSeconds() / 1000); + + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); + + tempStamp = QDateTime::fromMSecsSinceEpoch(frame->timeStamp().microSeconds() / 1000); outFile->write("\"" + tempStamp.toString("h:m:s.").toUtf8() + tempStamp.toString("z").rightJustified(3, '0').toUtf8() + "\""); - outFile->write(",\"" + QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8() + "\""); - if (frames->at(c).hasExtendedFrameFormat()) outFile->write(",\"Ext\""); + outFile->write(",\"" + QString::number(frame->frameId(), 16).toUpper().rightJustified(8, '0').toUtf8() + "\""); + if (frame->hasExtendedFrameFormat()) outFile->write(",\"Ext\""); else outFile->write(",\"Std\""); outFile->write(",\"\",\""); - for (int temp = 0; temp < frames->at(c).payload().length(); temp++) + for (int temp = 0; temp < dataLen; temp++) { - outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number(data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(' '); } @@ -2334,7 +2393,7 @@ bool FrameFileIO::loadCANDOFile(QString filename, QVector* frames) int lineCounter = 0; QByteArray data; int timeOffset = 0; - uint64_t lastTimeStamp = 0; + int64_t lastTimeStamp = 0; bool foundErrors = false; thisFrame.setFrameType(QCanBusFrame::DataFrame); @@ -2359,14 +2418,15 @@ bool FrameFileIO::loadCANDOFile(QString filename, QVector* frames) } data = inFile->read(12); + unsigned char *uData = reinterpret_cast(data.data()); thisFrame.bus = 0; thisFrame.isReceived = true; thisFrame.setExtendedFrameFormat(false); //format is incapable of extended frames thisFrame.setFrameType(QCanBusFrame::DataFrame); qint64 tempStamp; - tempStamp = 1000000ul * ((unsigned char)data[0] >> 2); - tempStamp += (((data[0] & 3) << 8) + (unsigned char)data[1]) * 1000; + tempStamp = 1000000ul * (uData[0] >> 2); + tempStamp += (((uData[0] & 3) << 8) + uData[1]) * 1000; tempStamp += timeOffset; if (tempStamp < lastTimeStamp) { @@ -2374,12 +2434,12 @@ bool FrameFileIO::loadCANDOFile(QString filename, QVector* frames) } lastTimeStamp = tempStamp; thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, tempStamp)); - thisFrame.setFrameId((((unsigned char)data[3] & 0x0F) * 256 + (unsigned char)data[2]) & 0x7FF); - QByteArray bytes((unsigned char)data[3] >> 4, 0); + thisFrame.setFrameId(((uData[3] & 0x0F) * 256 + uData[2]) & 0x7FF); + QByteArray bytes(uData[3] >> 4, 0); if (thisFrame.payload().length() <= 8 && thisFrame.frameId() <= 0x7FF) { - for (unsigned int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = (unsigned char)data[4 + d]; + for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = data[4 + d]; thisFrame.setPayload(bytes); frames->append(thisFrame); } @@ -2397,7 +2457,12 @@ bool FrameFileIO::saveCANDOFile(QString filename, const QVector* frame int lineCounter = 0; QByteArray data; CANFrame thisFrame; - int ms, id; + int id; + qint64 ms; + + unsigned char *inData; + int inDataLen; + const CANFrame *frame; if (!outFile->open(QIODevice::WriteOnly)) { @@ -2425,18 +2490,22 @@ bool FrameFileIO::saveCANDOFile(QString filename, const QVector* frame qApp->processEvents(); lineCounter = 0; } + + frame = &frames->at(c); + inData = reinterpret_cast(frame->payload().data()); + inDataLen = frame->payload().count(); + for (int j = 0; j < 8; j++) data[4 + j] = (char)0xFF; - thisFrame = frames->at(c); - if (!thisFrame.hasExtendedFrameFormat()) + if (!frame->hasExtendedFrameFormat()) { - ms = (thisFrame.timeStamp().microSeconds() / 1000); - id = thisFrame.frameId() & 0x7FF; + ms = (frame->timeStamp().microSeconds() / 1000); + id = frame->frameId() & 0x7FF; data[0] = (((ms / 1000) % 60) << 2) + ((ms % 1000) >> 8); data[1] = (char)(ms & 0xFF); data[2] = (char)(id & 0xFF); - data[3] = (char)((id >> 8) + (thisFrame.payload().length() << 4)); - for (unsigned int d = 0; d < thisFrame.payload().length(); d++) data[4 + d] = (char)thisFrame.payload()[d]; + data[3] = (char)((id >> 8) + (inDataLen << 4)); + for (int d = 0; d < inDataLen; d++) data[4 + d] = (char)inData[d]; outFile->write(data); } } @@ -2553,14 +2622,14 @@ bool FrameFileIO::loadMicrochipFile(QString filename, QVector* frames) if (tokens[1].at(0) == 'R') thisFrame.isReceived = true; else thisFrame.isReceived = false; thisFrame.setFrameType(QCanBusFrame::DataFrame); - thisFrame.setFrameId(Utility::ParseStringToNum(tokens[2])); + thisFrame.setFrameId(static_cast( Utility::ParseStringToNum(tokens[2])) ); if (thisFrame.frameId() <= 0x7FF) thisFrame.setExtendedFrameFormat(false); else thisFrame.setExtendedFrameFormat(true); thisFrame.bus = 0; - QByteArray bytes(tokens[3].toUInt(), 0); + QByteArray bytes(tokens[3].toInt(), 0); if (thisFrame.payload().length() > 8) thisFrame.payload().resize(8); - if (thisFrame.payload().length() + 4 > (unsigned int) tokens.length()) thisFrame.payload().resize( tokens.length() - 4 ); - for (unsigned int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = (unsigned char)Utility::ParseStringToNum(tokens[4 + d]); + if (thisFrame.payload().length() + 4 > tokens.length()) thisFrame.payload().resize( tokens.length() - 4 ); + for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast( Utility::ParseStringToNum(tokens[4 + d]) ); thisFrame.setPayload(bytes); frames->append(thisFrame); } @@ -2591,6 +2660,10 @@ bool FrameFileIO::saveMicrochipFile(QString filename, const QVector* f QDateTime timestamp, tempStamp; int lineCounter = 0; + unsigned char *data; + int dataLen; + const CANFrame *frame; + timestamp = QDateTime::currentDateTime(); if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) @@ -2617,15 +2690,19 @@ bool FrameFileIO::saveMicrochipFile(QString filename, const QVector* f lineCounter = 0; } - outFile->write(QString::number((int)(frames->at(c).timeStamp().microSeconds() / 1000)).toUtf8()); - if (frames->at(c).isReceived) outFile->write(";RX;"); - else outFile->write(";TX;"); - outFile->write("0x" + QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8() + ";"); - outFile->write(QString::number(frames->at(c).payload().length()).toUtf8() + ";"); + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); - for (unsigned int temp = 0; temp < frames->at(c).payload().length(); temp++) + outFile->write(QString::number((frame->timeStamp().microSeconds() / 1000)).toUtf8()); + if (frame->isReceived) outFile->write(";RX;"); + else outFile->write(";TX;"); + outFile->write("0x" + QString::number(frame->frameId(), 16).toUpper().rightJustified(8, '0').toUtf8() + ";"); + outFile->write(QString::number(dataLen).toUtf8() + ";"); + + for (int temp = 0; temp < dataLen; temp++) { - outFile->write("0x" + QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write("0x" + QString::number(data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(';'); } @@ -2773,11 +2850,11 @@ bool FrameFileIO::loadTraceFile(QString filename, QVector* frames) else thisFrame.setExtendedFrameFormat(true); thisFrame.bus = 0; thisFrame.setFrameType(QCanBusFrame::DataFrame); - QByteArray bytes(tokens[3].toUInt(), 0); + QByteArray bytes(tokens[3].toInt(), 0); if (thisFrame.payload().length() > 8) thisFrame.payload().resize(8); QList dataToks = tokens[4].split(' '); - if (thisFrame.payload().length() > (unsigned int) dataToks.length()) thisFrame.payload().resize( static_cast(dataToks.length()) ); - for (unsigned int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = (unsigned char)dataToks[d].toInt(nullptr, 16); + if (thisFrame.payload().length() > dataToks.length()) thisFrame.payload().resize(dataToks.length()); + for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast(dataToks[d].toInt(nullptr, 16)); thisFrame.setPayload(bytes); frames->append(thisFrame); } @@ -2795,9 +2872,13 @@ bool FrameFileIO::saveTraceFile(QString filename, const QVector * fram QFile *outFile = new QFile(filename); QDateTime timestamp; int lineCounter = 0; - uint64_t tempTime; + int64_t tempTime; int tempTimePiece; + unsigned char *data; + int dataLen; + const CANFrame *frame; + timestamp = QDateTime::currentDateTime(); if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) @@ -2836,38 +2917,41 @@ bool FrameFileIO::saveTraceFile(QString filename, const QVector * fram //lineCounter = 0; } + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); //1F D3 3F FF 08 FF E0 CB outFile->write(QString::number(lineCounter).rightJustified(10, ' ').toUtf8()); outFile->write("\t"); - tempTime = frames->at(c).timeStamp().microSeconds(); - tempTimePiece = tempTime / 1000000ul / 60 / 60; - tempTime -= tempTimePiece * 1000000ul * 60 * 60; + tempTime = frame->timeStamp().microSeconds(); + tempTimePiece = static_cast(tempTime / 1000000l / 60 / 60); + tempTime -= tempTimePiece * 1000000l * 60 * 60; outFile->write(QString::number(tempTimePiece).rightJustified(2, '0').toUtf8()); outFile->write(":"); - tempTimePiece = tempTime / 1000000ul / 60; - tempTime -= tempTimePiece * 1000000ul * 60; + tempTimePiece = static_cast(tempTime / 1000000l / 60); + tempTime -= tempTimePiece * 1000000l * 60; outFile->write(QString::number(tempTimePiece).rightJustified(2, '0').toUtf8()); outFile->write(":"); - tempTimePiece = tempTime / 1000000ul; - tempTime -= tempTimePiece * 1000000ul; + tempTimePiece = static_cast(tempTime / 1000000l); + tempTime -= tempTimePiece * 1000000l; outFile->write(QString::number(tempTimePiece).rightJustified(2, '0').toUtf8()); outFile->write(":"); - tempTimePiece = tempTime / 100; + tempTimePiece = static_cast(tempTime / 100); outFile->write(QString::number(tempTimePiece).rightJustified(4, '0').toUtf8()); outFile->write("\t"); - outFile->write(QString::number(frames->at(c).frameId(), 16).toUpper().rightJustified(8, '0').toUtf8() + "\t"); + outFile->write(QString::number(frame->frameId(), 16).toUpper().rightJustified(8, '0').toUtf8() + "\t"); - outFile->write(QString::number(frames->at(c).payload().length()).toUtf8() + "\t"); + outFile->write(QString::number(dataLen).toUtf8() + "\t"); - for (unsigned int temp = 0; temp < frames->at(c).payload().length(); temp++) + for (int temp = 0; temp < dataLen; temp++) { - outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + outFile->write(QString::number(data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); outFile->putChar(' '); } @@ -2886,6 +2970,10 @@ bool FrameFileIO::saveCanDumpFile(QString filename, const QVector * fr int lineCounter = 0; double tempTime; + unsigned char *data; + int dataLen; + const CANFrame *frame; + timestamp = QDateTime::currentDateTime(); if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) @@ -2902,27 +2990,31 @@ bool FrameFileIO::saveCanDumpFile(QString filename, const QVector * fr qApp->processEvents(); } + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); + outFile->write("("); - tempTime = frames->at(c).timeStamp().microSeconds() / 1000000.0; + tempTime = frame->timeStamp().microSeconds() / 1000000.0; outFile->write(QString::number(tempTime,'f', 6).rightJustified(17, '0').toUtf8()); outFile->write(") vcan0 "); - if (frames->at(c).hasExtendedFrameFormat()) { - outFile->write(QString::number(frames->at(c).frameId(), 16).rightJustified(8,'0').toUpper().toUtf8()); + if (frame->hasExtendedFrameFormat()) { + outFile->write(QString::number(frame->frameId(), 16).rightJustified(8,'0').toUpper().toUtf8()); } else { - outFile->write(QString::number(frames->at(c).frameId(), 16).rightJustified(3,'0').toUpper().toUtf8()); + outFile->write(QString::number(frame->frameId(), 16).rightJustified(3,'0').toUpper().toUtf8()); } outFile->write("#"); - if (frames->at(c).frameType() == QCanBusFrame::RemoteRequestFrame) { + if (frame->frameType() == QCanBusFrame::RemoteRequestFrame) { outFile->write("R"); - outFile->write(QString::number(frames->at(c).payload().length()).toUtf8()); + outFile->write(QString::number(dataLen).toUtf8()); } else { - for (unsigned int temp = 0; temp < frames->at(c).payload().length(); temp++) + for (int temp = 0; temp < dataLen; temp++) { - outFile->write(QString::number((unsigned char)frames->at(c).payload()[temp], 16).rightJustified(2,'0').toUpper().toUtf8()); + outFile->write(QString::number(data[temp], 16).rightJustified(2,'0').toUpper().toUtf8()); } } @@ -3102,14 +3194,14 @@ bool FrameFileIO::loadCanDumpFile(QString filename, QVector* frames) { //(1551774790.942758) can1 7A8 [8] F4 DC D1 83 0E 02 00 00 // 0 1 2 3 4 5 6 7 8 9 10 11 - thisFrame.setFrameId(tokens[2].toULong(nullptr, 16)); + thisFrame.setFrameId(tokens[2].toLong(nullptr, 16)); if (thisFrame.frameId() > 0x7FF) thisFrame.setExtendedFrameFormat(true); else thisFrame.setExtendedFrameFormat(false); thisFrame.setFrameType(QCanBusFrame::DataFrame); QByteArray bytes(tokens[3].at(1) - '0', 0); - for (uint32_t c = 0; c < thisFrame.payload().length(); c++) + for (int c = 0; c < thisFrame.payload().length(); c++) { - bytes[c] = static_cast(tokens[4 + c].toInt(nullptr, 16)); + bytes[c] = static_cast(tokens[4 + c].toInt(nullptr, 16)); } thisFrame.setPayload(bytes); } @@ -3170,7 +3262,6 @@ bool FrameFileIO::isLawicelFile(QString filename) QByteArray line; int lineCounter = 0; bool isMatch = false; - bool ret; if (!inFile->open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -3245,7 +3336,7 @@ bool FrameFileIO::loadLawicelFile(QString filename, QVector* frames) QByteArray bytes(line.length() / 2, 0); for (int d = 0; d < bytes.length(); d++) { - bytes[d] = static_cast(line.mid(d * 2, 2).toInt(nullptr, 16)); + bytes[d] = static_cast(line.mid(d * 2, 2).toInt(nullptr, 16)); } thisFrame.setPayload(bytes); frames->append(thisFrame); @@ -3508,6 +3599,10 @@ bool FrameFileIO::saveCabanaFile(QString filename, const QVector* fram QFile *outFile = new QFile(filename); int lineCounter = 0; + unsigned char *data; + int dataLen; + const CANFrame *frame; + if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) { delete outFile; @@ -3526,23 +3621,27 @@ bool FrameFileIO::saveCabanaFile(QString filename, const QVector* fram lineCounter = 0; } - double tempTimeStamp = frames->at(c).timeStamp().microSeconds(); + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); + + double tempTimeStamp = frame->timeStamp().microSeconds(); tempTimeStamp /= 1000000; outFile->write(QString::number(tempTimeStamp, 'f').toUtf8()); outFile->write(".0"); outFile->putChar(44); - outFile->write(QString::number(frames->at(c).frameId(), 10).toUpper().toUtf8()); + outFile->write(QString::number(frame->frameId(), 10).toUpper().toUtf8()); outFile->putChar(44); - outFile->write(QString::number(frames->at(c).bus).toUtf8()); + outFile->write(QString::number(frame->bus).toUtf8()); outFile->putChar(44); - for (unsigned int temp = 0; temp < 8; temp++) + for (int temp = 0; temp < 8; temp++) { - if (temp < frames->at(c).payload().length()) - outFile->write(QString::number(frames->at(c).payload()[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); + if (temp < dataLen) + outFile->write(QString::number(data[temp], 16).toUpper().rightJustified(2, '0').toUtf8()); else outFile->write("00"); } diff --git a/frameplaybackwindow.cpp b/frameplaybackwindow.cpp index 5f9ff76..85fd517 100644 --- a/frameplaybackwindow.cpp +++ b/frameplaybackwindow.cpp @@ -297,7 +297,7 @@ void FramePlaybackWindow::refreshIDList() QHash::Iterator filterIter; for (filterIter = currentSeqItem->idFilters.begin(); filterIter != currentSeqItem->idFilters.end(); ++filterIter) { - QListWidgetItem* listItem = FilterUtility::createCheckableFilterItem(filterIter.key(), filterIter.value(), ui->listID); + /*QListWidgetItem* listItem = */ FilterUtility::createCheckableFilterItem(filterIter.key(), filterIter.value(), ui->listID); } //default is to sort in ascending order ui->listID->sortItems(); @@ -479,7 +479,7 @@ void FramePlaybackWindow::btnLoadFile() if (FrameFileIO::loadFrameFile(filename, &item.data)) { - qSort(item.data); //sort by timestamp to be sure it's in order + std::sort(item.data.begin(), item.data.end()); //sort by timestamp to be sure it's in order QStringList fileList = filename.split('/'); item.filename = fileList[fileList.length() - 1]; item.currentLoopCount = 0; @@ -513,7 +513,7 @@ void FramePlaybackWindow::btnLoadLive() item.currentLoopCount = 0; item.maxLoops = 1; item.data = QVector(*modelFrames); //create a copy of the current frames from the main view - qSort(item.data); //be sure it's all in time based order + std::sort(item.data.begin(), item.data.end()); //be sure it's all in time based order fillIDHash(item); if (ui->tblSequence->currentRow() == -1) { diff --git a/framesenderwindow.cpp b/framesenderwindow.cpp index 1a8a4a7..d107428 100644 --- a/framesenderwindow.cpp +++ b/framesenderwindow.cpp @@ -185,7 +185,7 @@ void FrameSenderWindow::processIncomingFrame(CANFrame *frame) qDebug() << "Frame ID: " << frame->frameId(); if (thisTrigger->ID > 0 && (uint32_t)thisTrigger->ID == frame->frameId()) { - if ((uint32_t)thisTrigger->bus == frame->bus || thisTrigger->bus == -1) + if (thisTrigger->bus == frame->bus || thisTrigger->bus == -1) { if (thisTrigger->currCount < thisTrigger->maxCount) { @@ -525,16 +525,16 @@ int FrameSenderWindow::fetchOperand(int idx, ModifierOperand op) } else if (op.ID == -2) //fetch data from a data byte within the output frame { - if (op.notOper) return ~sendingData.at(idx).payload()[op.databyte]; - else return sendingData.at(idx).payload()[op.databyte]; + if (op.notOper) return ~((unsigned char)sendingData.at(idx).payload()[op.databyte]); + else return (unsigned char)sendingData.at(idx).payload()[op.databyte]; } else //look up external data byte { tempFrame = lookupFrame(op.ID, op.bus); if (tempFrame != nullptr) { - if (op.notOper) return ~tempFrame->payload()[op.databyte]; - else return tempFrame->payload()[op.databyte]; + if (op.notOper) return ~((unsigned char)tempFrame->payload()[op.databyte]); + else return (unsigned char)tempFrame->payload()[op.databyte]; } else return 0; } @@ -550,7 +550,7 @@ CANFrame* FrameSenderWindow::lookupFrame(int ID, int bus) { if (!frameCache.contains(ID)) return nullptr; - if (bus == -1 || frameCache[ID].bus == (unsigned int)bus) return &frameCache[ID]; + if (bus == -1 || frameCache[ID].bus == bus) return &frameCache[ID]; return nullptr; } @@ -786,17 +786,21 @@ ModifierOperationType FrameSenderWindow::parseOperation(QString op) void FrameSenderWindow::updateGridRow(int idx) { qDebug() << "updateGridRow"; + inhibitChanged = true; FrameSendData *temp = &sendingData[idx]; int gridLine = idx; QString dataString; QTableWidgetItem *item = ui->tableSender->item(gridLine, 9); + unsigned char *data = reinterpret_cast(temp->payload().data()); + int dataLen = temp->payload().length(); + if (item == nullptr) item = new QTableWidgetItem(); item->setText(QString::number(temp->count)); if (temp->frameType() != QCanBusFrame::RemoteRequestFrame) { - for (unsigned int i = 0; i < temp->payload().length(); i++) + for (int i = 0; i < dataLen; i++) { - dataString.append(Utility::formatNumber(temp->payload()[i])); + dataString.append(Utility::formatNumber(data[i])); dataString.append(" "); } ui->tableSender->item(gridLine, 6)->setText(dataString); diff --git a/mainwindow.cpp b/mainwindow.cpp index 0e9fc71..678e603 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -444,7 +444,7 @@ void MainWindow::updateFilterList() QMap::const_iterator filterIter; for (filterIter = filters->begin(); filterIter != filters->end(); ++filterIter) { - QListWidgetItem *thisItem = FilterUtility::createCheckableFilterItem(filterIter.key(), filterIter.value(), ui->listFilters); + /*QListWidgetItem *thisItem = */FilterUtility::createCheckableFilterItem(filterIter.key(), filterIter.value(), ui->listFilters); } inhibitFilterUpdate = false; } @@ -729,6 +729,10 @@ void MainWindow::saveDecodedTextFile(QString filename) QFile *outFile = new QFile(filename); const QVector *frames = model->getFilteredListReference(); + unsigned char *data; + int dataLen; + const CANFrame *frame; + if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) return; /* @@ -738,20 +742,23 @@ Data Bytes: 88 10 00 13 BB 00 06 00 */ for (int c = 0; c < frames->count(); c++) { - CANFrame thisFrame = frames->at(c); + frame = &frames->at(c); + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().count(); + QString builderString; - builderString += tr("Time: ") + QString::number((thisFrame.timeStamp().microSeconds() / 1000000.0), 'f', 6); - builderString += tr(" ID: ") + Utility::formatCANID(thisFrame.frameId(), thisFrame.hasExtendedFrameFormat()); - if (thisFrame.hasExtendedFrameFormat()) builderString += tr(" Ext "); + builderString += tr("Time: ") + QString::number((frame->timeStamp().microSeconds() / 1000000.0), 'f', 6); + builderString += tr(" ID: ") + Utility::formatCANID(frame->frameId(), frame->hasExtendedFrameFormat()); + if (frame->hasExtendedFrameFormat()) builderString += tr(" Ext "); else builderString += tr(" Std "); - builderString += tr("Bus: ") + QString::number(thisFrame.bus); - builderString += " Len: " + QString::number(thisFrame.payload().length()) + "\n"; + builderString += tr("Bus: ") + QString::number(frame->bus); + builderString += " Len: " + QString::number(dataLen) + "\n"; outFile->write(builderString.toUtf8()); builderString = tr("Data Bytes: "); - for (unsigned int temp = 0; temp < thisFrame.payload().length(); temp++) + for (int temp = 0; temp < dataLen; temp++) { - builderString += Utility::formatNumber(thisFrame.payload()[temp]) + " "; + builderString += Utility::formatNumber(data[temp]) + " "; } builderString += "\n"; outFile->write(builderString.toUtf8()); @@ -759,14 +766,14 @@ Data Bytes: 88 10 00 13 BB 00 06 00 builderString = ""; if (dbcHandler != nullptr) { - DBC_MESSAGE *msg = dbcHandler->findMessage(thisFrame); + DBC_MESSAGE *msg = dbcHandler->findMessage(*frame); if (msg != nullptr) { for (int j = 0; j < msg->sigHandler->getCount(); j++) { QString temp; - if (msg->sigHandler->findSignalByIdx(j)->processAsText(thisFrame, temp)) + if (msg->sigHandler->findSignalByIdx(j)->processAsText(*frame, temp)) { builderString.append("\t" + temp); builderString.append("\n"); diff --git a/re/filecomparatorwindow.cpp b/re/filecomparatorwindow.cpp index e565de7..c85cc8c 100644 --- a/re/filecomparatorwindow.cpp +++ b/re/filecomparatorwindow.cpp @@ -123,6 +123,8 @@ void FileComparatorWindow::calculateDetails() QTreeWidgetItem *interestedOnlyBase, *referenceOnlyBase = nullptr, *sharedBase, *bitmapBaseInterested, *bitmapBaseReference = nullptr; QTreeWidgetItem *valuesBase, *detail, *sharedItem, *valuesInterested, *valuesReference = nullptr; uint64_t tmp; + unsigned char *data; + int dataLen; bool uniqueInterested = ui->ckUniqueToInterested->isChecked(); @@ -152,12 +154,15 @@ void FileComparatorWindow::calculateDetails() for (int x = 0; x < interestedFrames.count(); x++) { CANFrame frame = interestedFrames.at(x); + data = reinterpret_cast(frame.payload().data()); + dataLen = frame.payload().count(); + if (interestedIDs.contains(frame.frameId())) //if we saw this ID before then add to the QList in there { - for (unsigned int y = 0; y < frame.payload().length(); y++) + for (unsigned int y = 0; y < dataLen; y++) { - interestedIDs[frame.frameId()].values[y][frame.payload()[y]]++; - tmp = frame.payload()[y]; + interestedIDs[frame.frameId()].values[y][data[y]]++; + tmp = data[y]; tmp = tmp << (8 * y); interestedIDs[frame.frameId()].bitmap |= tmp; //qDebug() << "bitmap: " << QString::number(interestedIDs[frame.frameId()].bitmap, 16); @@ -167,7 +172,7 @@ void FileComparatorWindow::calculateDetails() { FrameData *newData = new FrameData(); newData->ID = frame.frameId(); - newData->dataLen = frame.payload().length(); + newData->dataLen = dataLen; //it would be possible to implement a constructor for FrameData //that sets the bitmap and values to zero. That would be cleaner and better. newData->bitmap = 0; @@ -179,10 +184,10 @@ void FileComparatorWindow::calculateDetails() } } //memset(newData->values, 0, 256 * 8); - for (unsigned int y = 0; y < frame.payload().length(); y++) + for (unsigned int y = 0; y < dataLen; y++) { - newData->values[y][frame.payload()[y]] = 1; - tmp = frame.payload()[y]; + newData->values[y][data[y]] = 1; + tmp = data[y]; tmp = tmp << (8 * y); newData->bitmap |= tmp; //qDebug() << "bitmap: " << QString::number(newData->bitmap, 16); @@ -196,12 +201,15 @@ void FileComparatorWindow::calculateDetails() for (int x = 0; x < referenceFrames.count(); x++) { CANFrame frame = referenceFrames.at(x); + data = reinterpret_cast(frame.payload().data()); + dataLen = frame.payload().count(); + if (referenceIDs.contains(frame.frameId())) //if we saw this ID before then add to the QList in there { - for (unsigned int y = 0; y < frame.payload().length(); y++) + for (unsigned int y = 0; y < dataLen; y++) { - referenceIDs[frame.frameId()].values[y][frame.payload()[y]]++; - tmp = frame.payload()[y]; + referenceIDs[frame.frameId()].values[y][data[y]]++; + tmp = data[y]; tmp = tmp << (8 * y); referenceIDs[frame.frameId()].bitmap |= tmp; //qDebug() << "bitmap: " << QString::number(referenceIDs[frame.frameId()].bitmap, 16); @@ -211,7 +219,7 @@ void FileComparatorWindow::calculateDetails() { FrameData *newData = new FrameData(); newData->ID = frame.frameId(); - newData->dataLen = frame.payload().length(); + newData->dataLen = dataLen; newData->bitmap = 0; for (int x = 0; x < 8; x++) { @@ -221,10 +229,10 @@ void FileComparatorWindow::calculateDetails() } } //memset(newData->values, 0, 256 * 8); - for (unsigned int y = 0; y < frame.payload().length(); y++) + for (unsigned int y = 0; y < dataLen; y++) { - newData->values[y][frame.payload()[y]] = 1; - tmp = frame.payload()[y]; + newData->values[y][data[y]] = 1; + tmp = data[y]; tmp = tmp << (8 * y); newData->bitmap |= tmp; //qDebug() << "bitmap: " << QString::number(newData->bitmap, 16); diff --git a/re/flowviewwindow.cpp b/re/flowviewwindow.cpp index 8261aef..c247290 100644 --- a/re/flowviewwindow.cpp +++ b/re/flowviewwindow.cpp @@ -257,7 +257,7 @@ void FlowViewWindow::plottableDoubleClick(QCPAbstractPlottable* plottable, QMous void FlowViewWindow::gotCenterTimeID(int32_t ID, double timestamp) { - uint64_t t_stamp; + int64_t t_stamp; t_stamp = timestamp * 1000000l; @@ -397,8 +397,10 @@ void FlowViewWindow::updatedFrames(int numFrames) { QVectornewX[8]; QVectornewY[8]; + unsigned char *data; + int dataLen = 0; - CANFrame thisFrame; + const CANFrame *thisFrame; if (numFrames == -1) //all frames deleted. Kill the display { ui->listFrameID->clear(); @@ -433,35 +435,37 @@ void FlowViewWindow::updatedFrames(int numFrames) bool needRefresh = false; for (int i = modelFrames->count() - numFrames; i < modelFrames->count(); i++) { - thisFrame = modelFrames->at(i); + thisFrame = &modelFrames->at(i); + data = reinterpret_cast(thisFrame->payload().data()); + dataLen = thisFrame->payload().length(); - if (!foundID.contains(thisFrame.frameId())) + if (!foundID.contains(thisFrame->frameId())) { - foundID.append(thisFrame.frameId()); - FilterUtility::createFilterItem(thisFrame.frameId(), ui->listFrameID); + foundID.append(thisFrame->frameId()); + FilterUtility::createFilterItem(thisFrame->frameId(), ui->listFrameID); } - if (thisFrame.frameId() == refID) + if (thisFrame->frameId() == refID) { - frameCache.append(thisFrame); + frameCache.append(*thisFrame); - for (int k = 0; k < 8; k++) + for (int k = 0; k < dataLen; k++) { if (ui->cbTimeGraph->isChecked()) { if (secondsMode){ - newX[k].append((double)(thisFrame.timeStamp().microSeconds()) / 1000000.0); + newX[k].append((double)(thisFrame->timeStamp().microSeconds()) / 1000000.0); } else { - newX[k].append(thisFrame.timeStamp().microSeconds()); + newX[k].append(thisFrame->timeStamp().microSeconds()); } } else { newX[k].append(x[k].count()); } - newY[k].append(thisFrame.payload()[k]); + newY[k].append(data[k]); needRefresh = true; } } @@ -475,7 +479,7 @@ void FlowViewWindow::updatedFrames(int numFrames) } if (needRefresh) { - for (int k = 0; k < 8; k++) + for (int k = 0; k < dataLen; k++) { if (graphRef[k] && graphRef[k]->data()) graphRef[k]->addData(newX[k], newY[k]); @@ -498,6 +502,9 @@ void FlowViewWindow::createGraph(int byteNum) { int tempVal; float minval=1000000, maxval = -100000; + unsigned char *data; + int dataLen = 0; + const CANFrame *frame; qDebug() << "Create Graph " << byteNum; @@ -512,16 +519,20 @@ void FlowViewWindow::createGraph(int byteNum) for (int j = 0; j < numEntries; j++) { - tempVal = frameCache[j].payload()[byteNum]; + frame = &frameCache[j]; + data = reinterpret_cast(frame->payload().data()); + dataLen = frame->payload().length(); + + tempVal = data[byteNum]; if (graphByTime) { if (secondsMode){ - x[byteNum][j] = (double)(frameCache[j].timeStamp().microSeconds()) / 1000000.0; + x[byteNum][j] = (double)(frame->timeStamp().microSeconds()) / 1000000.0; } else { - x[byteNum][j] = frameCache[j].timeStamp().microSeconds(); + x[byteNum][j] = frame->timeStamp().microSeconds(); } } else @@ -758,7 +769,8 @@ void FlowViewWindow::updatePosition(bool forward) uint8_t cngByte; for (int i = 0; i < 8; i++) { - cngByte = currBytes[i] ^ frameCache.at(currentPosition).payload()[i]; + unsigned char thisByte = static_cast(frameCache.at(currentPosition).payload().data()[i]); + cngByte = currBytes[i] ^ thisByte; changedBits |= (uint64_t)cngByte << (8ull * i); } diff --git a/re/flowviewwindow.h b/re/flowviewwindow.h index fa07799..a18e5f3 100644 --- a/re/flowviewwindow.h +++ b/re/flowviewwindow.h @@ -44,7 +44,7 @@ signals: private: Ui::FlowViewWindow *ui; - QList foundID; + QList foundID; QList frameCache; const QVector *modelFrames; unsigned char refBytes[8]; diff --git a/re/frameinfowindow.cpp b/re/frameinfowindow.cpp index e200a8b..db954b4 100644 --- a/re/frameinfowindow.cpp +++ b/re/frameinfowindow.cpp @@ -138,7 +138,7 @@ FrameInfoWindow::~FrameInfoWindow() void FrameInfoWindow::closeEvent(QCloseEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event) writeSettings(); } @@ -260,10 +260,10 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) uint8_t referenceBits[8]; QTreeWidgetItem *baseNode, *dataBase, *histBase, *tempItem; - targettedID = static_cast(Utility::ParseStringToNum(newID)); - if (modelFrames->count() == 0) return; + targettedID = static_cast(Utility::ParseStringToNum(newID)); + qDebug() << "Started update details window with id " << targettedID; avgInterval = 0; @@ -278,6 +278,9 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) if (thisFrame.frameId() == static_cast(targettedID)) frameCache.append(thisFrame); } + unsigned char *data = reinterpret_cast(frameCache.at(0).payload().data()); + int dataLen = frameCache.at(0).payload().length(); + ui->treeDetails->clear(); if (frameCache.count() == 0) return; @@ -364,10 +367,13 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) } for (int j = 0; j < 64; j++) bitfieldHistogram[j] = 0; - for (int c = 0; c < 8; c++) + data = reinterpret_cast(frameCache.at(0).payload().data()); + dataLen = frameCache.at(0).payload().length(); + + for (int c = 0; c < dataLen; c++) { changedBits[c] = 0; - referenceBits[c] = frameCache.at(0).payload()[c]; + referenceBits[c] = data[c]; //qDebug() << referenceBits[c]; } @@ -377,10 +383,13 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) //then find all data points for (int j = 0; j < frameCache.count(); j++) { + data = reinterpret_cast(frameCache.at(j).payload().data()); + dataLen = frameCache.at(j).payload().length(); + byteGraphX.append(j); - for (uint32_t bytcnt = 0; bytcnt < frameCache[j].payload().length(); bytcnt++) + for (uint32_t bytcnt = 0; bytcnt < dataLen; bytcnt++) { - byteGraphY[bytcnt].append(frameCache[j].payload()[bytcnt]); + byteGraphY[bytcnt].append(data[bytcnt]); } if (j != 0) @@ -398,12 +407,12 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) if (thisInterval < minInterval) minInterval = thisInterval; avgInterval += thisInterval; } - thisLen = frameCache.at(j).payload().length(); + thisLen = dataLen; if (thisLen > maxLen) maxLen = thisLen; if (thisLen < minLen) minLen = thisLen; for (int c = 0; c < thisLen; c++) { - unsigned char dat = frameCache.at(j).payload()[c]; + unsigned char dat = data[c]; if (minData[c] > dat) minData[c] = dat; if (maxData[c] < dat) maxData[c] = dat; dataHistogram[dat][c]++; //add one to count for this diff --git a/re/isotp_interpreterwindow.cpp b/re/isotp_interpreterwindow.cpp index 5fe264b..d73e5d6 100644 --- a/re/isotp_interpreterwindow.cpp +++ b/re/isotp_interpreterwindow.cpp @@ -81,7 +81,7 @@ void ISOTP_InterpreterWindow::showEvent(QShowEvent* event) void ISOTP_InterpreterWindow::closeEvent(QCloseEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event) removeEventFilter(this); writeSettings(); } @@ -194,7 +194,7 @@ void ISOTP_InterpreterWindow::updatedFrames(int numFrames) void ISOTP_InterpreterWindow::headerClicked(int logicalIndex) { ui->tableIsoFrames->setSortingEnabled(false); - ui->tableIsoFrames->sortByColumn(logicalIndex); + ui->tableIsoFrames->sortByColumn(logicalIndex, Qt::SortOrder::AscendingOrder); } void ISOTP_InterpreterWindow::showDetailView() @@ -208,17 +208,21 @@ void ISOTP_InterpreterWindow::showDetailView() msg = &messages[rowNum]; - if (msg->len != msg->data.length()) + unsigned char *data = reinterpret_cast(msg->payload().data()); + int dataLen = msg->payload().length(); + + if (msg->reportedLength != dataLen) { buildString.append("Message didn't have the correct number of bytes.\rExpected " - + QString::number(msg->len) + " got " - + QString::number(msg->data.length()) + "\r\r"); + + QString::number(msg->reportedLength) + " got " + + QString::number(dataLen) + "\r\r"); } buildString.append(tr("Raw Payload: ")); - for (int i = 0; i < messages[rowNum].data.count(); i++) + + for (int i = 0; i < dataLen; i++) { - buildString.append(Utility::formatNumber((char)messages[rowNum].data[i])); + buildString.append(Utility::formatNumber(data[i])); buildString.append(" "); } buildString.append("\r\r"); @@ -262,17 +266,20 @@ void ISOTP_InterpreterWindow::newISOMessage(ISOTP_MESSAGE msg) int rowNum; QString tempString; - if ((msg.len != msg.data.count()) && !ui->cbShowIncomplete->isChecked()) return; + unsigned char *data = reinterpret_cast(msg.payload().data()); + int dataLen = msg.payload().length(); - if (idFilters.find(msg.ID) == idFilters.end()) + if ((msg.reportedLength != dataLen) && !ui->cbShowIncomplete->isChecked()) return; + + if (idFilters.find(msg.frameId()) == idFilters.end()) { - idFilters.insert(msg.ID, true); + idFilters.insert(msg.frameId(), true); - QListWidgetItem* listItem = new QListWidgetItem(Utility::formatCANID(msg.ID, msg.extended), ui->listFilter); + QListWidgetItem* listItem = new QListWidgetItem(Utility::formatCANID(msg.frameId(), msg.hasExtendedFrameFormat()), ui->listFilter); listItem->setFlags(listItem->flags() | Qt::ItemIsUserCheckable); // set checkable flag listItem->setCheckState(Qt::Checked); } - if (!idFilters[msg.ID]) return; + if (!idFilters[msg.frameId()]) return; messages.append(msg); rowNum = ui->tableIsoFrames->rowCount(); @@ -280,22 +287,21 @@ void ISOTP_InterpreterWindow::newISOMessage(ISOTP_MESSAGE msg) ui->tableIsoFrames->insertRow(rowNum); QTableWidgetItem *item = new QTableWidgetItem; - item->setData(Qt::EditRole, Utility::formatTimestamp(msg.timestamp)); + item->setData(Qt::EditRole, Utility::formatTimestamp(msg.timeStamp().microSeconds())); //ui->tableIsoFrames->setItem(rowNum, 0, (double)msg.timestamp, Utility::formatTimestamp(msg.timestamp))); ui->tableIsoFrames->setItem(rowNum, 0, item); - ui->tableIsoFrames->setItem(rowNum, 1, new QTableWidgetItem(QString::number(msg.ID, 16))); + ui->tableIsoFrames->setItem(rowNum, 1, new QTableWidgetItem(QString::number(msg.frameId(), 16))); ui->tableIsoFrames->setItem(rowNum, 2, new QTableWidgetItem(QString::number(msg.bus))); if (msg.isReceived) ui->tableIsoFrames->setItem(rowNum, 3, new QTableWidgetItem("Rx")); else ui->tableIsoFrames->setItem(rowNum, 3, new QTableWidgetItem("Tx")); - ui->tableIsoFrames->setItem(rowNum, 4, new QTableWidgetItem(QString::number(msg.len))); + ui->tableIsoFrames->setItem(rowNum, 4, new QTableWidgetItem(QString::number(msg.payload().length()))); - for (int i = 0; i < msg.data.count(); i++) + for (int i = 0; i < dataLen; i++) { - tempString.append(Utility::formatNumber((char)msg.data[i])); + tempString.append(Utility::formatNumber(data[i])); tempString.append(" "); } ui->tableIsoFrames->setItem(rowNum, 5, new QTableWidgetItem(tempString)); - } diff --git a/re/sniffer/snifferitem.cpp b/re/sniffer/snifferitem.cpp index 8e102eb..54bb1b6 100644 --- a/re/sniffer/snifferitem.cpp +++ b/re/sniffer/snifferitem.cpp @@ -6,18 +6,23 @@ SnifferItem::SnifferItem(const CANFrame& pFrame, quint32 seq): mID(pFrame.frameId()) { - for (int i = 0; i < 8; i++) { - mNotch[i] =0; + unsigned char *data = reinterpret_cast(pFrame.payload().data()); + int dataLen = pFrame.payload().length(); + + for (int i = 0; i < 0; i++) { + mNotch[i] = 0; mMarker.data[i] = 0; mMarker.dataTimestamp[i] = 0; - mCurrent.data[i] = pFrame.payload()[i]; + if (i < dataLen) mCurrent.data[i] = data[i]; + else mCurrent.data[i] = 0; mCurrent.dataTimestamp[i] = seq; } mLastMarker = mMarker; - mCurrent.len = pFrame.payload().length(); + mCurrent.len = dataLen; + /* that's dirty */ update(pFrame, seq, false); - update(pFrame, seq, false); + update(pFrame, seq, false); //anyone know why we're doing this twice?! } @@ -112,20 +117,23 @@ void SnifferItem::update(const CANFrame& pFrame, quint32 timeSeq, bool mute) mLastTime = mCurrentTime; mCurrSeqVal = timeSeq; + unsigned char *data = reinterpret_cast(pFrame.payload().data()); + int dataLen = pFrame.payload().length(); + /* copy new value */ - for (int i = 0; i < 8; i++) + for (int i = 0; i < dataLen; i++) { - maskedData = pFrame.payload()[i]; + maskedData = data[i]; if (mute) maskedData &= ~mNotch[i]; maskedCurr = mCurrent.data[i]; if (mute) maskedCurr &= ~mNotch[i]; if (maskedCurr != maskedData) { - mCurrent.data[i] = pFrame.payload()[i]; + mCurrent.data[i] = data[i]; mCurrent.dataTimestamp[i] = timeSeq; } } - mCurrent.len = pFrame.payload().length(); + mCurrent.len = dataLen; mCurrentTime = pFrame.timeStamp().microSeconds(); /* update marker */ diff --git a/re/udsscanwindow.cpp b/re/udsscanwindow.cpp index 081cfa7..ecb4623 100644 --- a/re/udsscanwindow.cpp +++ b/re/udsscanwindow.cpp @@ -237,8 +237,8 @@ void UDSScanWindow::scanUDS() for (id = startID; id <= endID; id++) { - test.ID = id; - test.data.clear(); + test.setFrameId( id ); + test.payload().clear(); if (ui->ckTester->isChecked()) { @@ -354,17 +354,19 @@ void UDSScanWindow::gotUDSReply(UDS_MESSAGE msg) int offset = ui->spinReplyOffset->value(); UDS_MESSAGE sentFrame; bool gotReply = false; + unsigned char *data = reinterpret_cast(msg.payload().data()); + int dataLen = msg.payload().length(); int numSending = sendingFrames.length(); if (numSending == 0) return; if (currIdx >= numSending) return; sentFrame = sendingFrames[currIdx]; - id = msg.ID; + id = msg.frameId(); - qDebug() << "UDS message ID " << QString::number(msg.ID,16) << " service: " << QString::number(msg.service, 16) << " subfunc: " << QString::number(msg.subFunc, 16); + qDebug() << "UDS message ID " << QString::number(msg.frameId(),16) << " service: " << QString::number(msg.service, 16) << " subfunc: " << QString::number(msg.subFunc, 16); - if ((id == (uint32_t)(sentFrame.ID + offset)) || ui->cbAllowAdaptiveOffset->isChecked()) + if ((id == (uint32_t)(sentFrame.frameId() + offset)) || ui->cbAllowAdaptiveOffset->isChecked()) { serviceShortName = udsHandler->getServiceShortDesc(sentFrame.service); if (serviceShortName.length() < 3) serviceShortName = QString::number(sentFrame.service, 16); @@ -374,10 +376,10 @@ void UDSScanWindow::gotUDSReply(UDS_MESSAGE msg) QTreeWidgetItem *nodePositive = new QTreeWidgetItem(); QString reply = "POSITIVE "; - for (int i = 0; i < msg.data.length(); i++) + for (int i = 0; i < dataLen; i++) { reply.append(" "); - reply.append(Utility::formatHexNum(msg.data[i])); + reply.append(Utility::formatHexNum(data[i])); } nodePositive->setText(0, reply); nodePositive->setForeground(0, QBrush(Qt::darkGreen)); @@ -387,12 +389,12 @@ void UDSScanWindow::gotUDSReply(UDS_MESSAGE msg) } else if ( msg.isErrorReply && (msg.service == sendingFrames[currIdx].service) ) { - if (msg.data.length()) + if (dataLen) { setupNodes(id); QTreeWidgetItem *nodeNegative = new QTreeWidgetItem(); qDebug() << ui->spinNumBytes->value(); - nodeNegative->setText(0, "NEGATIVE - " + udsHandler->getNegativeResponseShort(msg.data[0])); + nodeNegative->setText(0, "NEGATIVE - " + udsHandler->getNegativeResponseShort(data[0])); nodeNegative->setForeground(0, QBrush(Qt::darkRed)); nodeSubFunc->addChild(nodeNegative); nodeSubFunc->setForeground(0, QBrush(Qt::darkRed)); @@ -413,10 +415,10 @@ void UDSScanWindow::setupNodes(uint32_t replyID) if (serviceShortName.length() < 3) serviceShortName = QString::number(sendingFrames[currIdx].service, 16); QTreeWidgetItem *replyNode = nullptr; - if (!nodeID || nodeID->text(0) != Utility::formatHexNum(sendingFrames[currIdx].ID)) + if (!nodeID || nodeID->text(0) != Utility::formatHexNum(sendingFrames[currIdx].frameId())) { nodeID = new QTreeWidgetItem(); - nodeID->setText(0, Utility::formatHexNum(sendingFrames[currIdx].ID)); + nodeID->setText(0, Utility::formatHexNum(sendingFrames[currIdx].frameId())); ui->treeResults->addTopLevelItem(nodeID); nodeService = nullptr; } diff --git a/scriptcontainer.cpp b/scriptcontainer.cpp index c3ef718..f3fa836 100644 --- a/scriptcontainer.cpp +++ b/scriptcontainer.cpp @@ -236,7 +236,7 @@ void CANScriptHelper::sendFrame(QJSValue bus, QJSValue id, QJSValue length, QJSV if (!data.isArray()) qDebug() << "data isn't an array"; - for (unsigned int i = 0; i < bytes.length(); i++) + for (int i = 0; i < bytes.length(); i++) { bytes[i] = (uint8_t)data.property(i).toInt(); } @@ -254,15 +254,19 @@ void CANScriptHelper::gotTargettedFrame(const CANFrame &frame) { if (!gotFrameFunction.isCallable()) return; //nothing to do if we can't even call the function //qDebug() << "Got frame in script interface"; + + unsigned char *data = reinterpret_cast(frame.payload().data()); + int dataLen = frame.payload().length(); + for (int i = 0; i < filters.length(); i++) { if (filters[i].checkFilter(frame.frameId(), frame.bus)) { QJSValueList args; args << frame.bus << frame.frameId() << frame.payload().length(); - QJSValue dataBytes = scriptEngine->newArray(frame.payload().length()); + QJSValue dataBytes = scriptEngine->newArray(dataLen); - for (unsigned int j = 0; j < frame.payload().length(); j++) dataBytes.setProperty(j, QJSValue((unsigned char)frame.payload()[j])); + for (int j = 0; j < dataLen; j++) dataBytes.setProperty(j, QJSValue(data[j])); args.append(dataBytes); gotFrameFunction.call(args); return; //as soon as one filter matches we jump out @@ -299,26 +303,29 @@ void ISOTPScriptHelper::setFilter(QJSValue id, QJSValue mask, QJSValue bus) handler->addFilter(busVal, idVal, maskVal); } -void ISOTPScriptHelper::sendISOTP(QJSValue bus, QJSValue id, QJSValue length, QJSValue data) +void ISOTPScriptHelper::sendISOTP(QJSValue bus, QJSValue id, QJSValue length, QJSValue dataBytes) { ISOTP_MESSAGE msg; - msg.extended = false; - msg.ID = id.toInt(); - msg.len = length.toUInt(); + msg.setExtendedFrameFormat(false); + msg.setFrameId(id.toUInt()); + msg.payload().resize(length.toInt()); - if (!data.isArray()) qDebug() << "data isn't an array"; + unsigned char *data = reinterpret_cast(msg.payload().data()); + int dataLen = msg.payload().length(); - for (int i = 0; i < msg.len; i++) + if (!dataBytes.isArray()) qDebug() << "data isn't an array"; + + for (int i = 0; i < dataLen; i++) { - msg.data[i] = static_cast(data.property(i).toInt()); + data[i] = static_cast(dataBytes.property(i).toInt()); } msg.bus = bus.toInt(); - if (msg.ID > 0x7FF) msg.extended = true; + if (msg.frameId() > 0x7FF) msg.setExtendedFrameFormat(true); qDebug() << "sending isotp message from script"; - handler->sendISOTPFrame(msg.bus, msg.ID, msg.data); + handler->sendISOTPFrame(msg.bus, msg.frameId(), msg.payload()); } void ISOTPScriptHelper::setRxCallback(QJSValue cb) @@ -333,10 +340,10 @@ void ISOTPScriptHelper::newISOMessage(ISOTP_MESSAGE msg) //qDebug() << "Got frame in script interface"; QJSValueList args; - args << msg.bus << msg.ID << msg.len; - QJSValue dataBytes = scriptEngine->newArray(static_cast(msg.len)); + args << msg.bus << msg.frameId() << msg.payload().length(); + QJSValue dataBytes = scriptEngine->newArray(static_cast(msg.payload().length())); - for (int j = 0; j < msg.len; j++) dataBytes.setProperty(static_cast(j), QJSValue(msg.data[j])); + for (int j = 0; j < msg.payload().length(); j++) dataBytes.setProperty(static_cast(j), QJSValue((unsigned char)msg.payload()[j])); args.append(dataBytes); gotFrameFunction.call(args); } @@ -373,25 +380,23 @@ void UDSScriptHelper::setFilter(QJSValue id, QJSValue mask, QJSValue bus) void UDSScriptHelper::sendUDS(QJSValue bus, QJSValue id, QJSValue service, QJSValue sublen, QJSValue subFunc, QJSValue length, QJSValue data) { UDS_MESSAGE msg; - msg.extended = false; - msg.ID = id.toUInt(); - msg.len = length.toUInt(); + msg.setExtendedFrameFormat(false); + msg.setFrameId( id.toUInt() ); + msg.payload().resize(length.toUInt()); msg.service = service.toUInt(); msg.subFuncLen = sublen.toUInt(); msg.subFunc = subFunc.toUInt(); if (!data.isArray()) qDebug() << "data isn't an array"; - msg.data.reserve(msg.len); - - for (int i = 0; i < msg.len; i++) + for (int i = 0; i < msg.payload().length(); i++) { - msg.data.append(static_cast(data.property(static_cast(i)).toInt())); + msg.payload().append(static_cast(data.property(static_cast(i)).toInt())); } msg.bus = bus.toInt(); - if (msg.ID > 0x7FF) msg.extended = true; + if (msg.frameId() > 0x7FF) msg.setExtendedFrameFormat( true ); qDebug() << "sending UDS message from script"; @@ -406,15 +411,15 @@ void UDSScriptHelper::setRxCallback(QJSValue cb) void UDSScriptHelper::newUDSMessage(UDS_MESSAGE msg) { //qDebug() << "udsScriptHelper got a UDS message"; - qDebug() << "UDS script helper. Msg data len: " << msg.len; + qDebug() << "UDS script helper. Msg data len: " << msg.payload().length(); if (!gotFrameFunction.isCallable()) return; //nothing to do if we can't even call the function qDebug() << "Got frame in script interface"; QJSValueList args; - args << msg.bus << msg.ID << msg.service << msg.subFunc << msg.len; - QJSValue dataBytes = scriptEngine->newArray(static_cast(msg.len)); + args << msg.bus << msg.frameId() << msg.service << msg.subFunc << msg.payload().length(); + QJSValue dataBytes = scriptEngine->newArray(static_cast(msg.payload().length())); - for (int j = 0; j < msg.data.length(); j++) dataBytes.setProperty(static_cast(j), QJSValue(msg.data[j])); + for (int j = 0; j < msg.payload().length(); j++) dataBytes.setProperty(static_cast(j), QJSValue((unsigned char)msg.payload()[j])); args.append(dataBytes); gotFrameFunction.call(args); } diff --git a/scriptingwindow.cpp b/scriptingwindow.cpp index fef3ca7..46ffa2b 100644 --- a/scriptingwindow.cpp +++ b/scriptingwindow.cpp @@ -18,7 +18,6 @@ ScriptingWindow::ScriptingWindow(const QVector *frames, QWidget *paren editor = new JSEdit(); editor->setFrameShape(JSEdit::NoFrame); editor->setWordWrapMode(QTextOption::NoWrap); - editor->setTabStopWidth(4); editor->setEnabled(false); editor->setFont(QFont("Monospace", 12)); editor->show(); diff --git a/utility.h b/utility.h index d4f7f62..8160aa7 100644 --- a/utility.h +++ b/utility.h @@ -97,15 +97,6 @@ public: else return formatHexNum(value); } - static QString formatNumber(char value) - { - if (decimalMode) - { - return QString::number((unsigned char)value, 10); - } - else return formatHexNum((unsigned char)value); - } - static QString formatCANID(uint64_t id, bool extended) { if (decimalMode) return QString::number(id, 10); From 48d7dfa97c8ea1b21f36d9875096e6771974fc92 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 12 Feb 2020 19:56:43 -0500 Subject: [PATCH 05/58] Add ability to load Tesla ODIN JSON files. Where will you get such a file? State secret. --- dbc/dbchandler.cpp | 182 ++++++++++++++++++++++++++++++++++++++ dbc/dbchandler.h | 1 + dbc/dbcloadsavewindow.cpp | 23 +++++ dbc/dbcloadsavewindow.h | 1 + dbc/dbcmaineditor.cpp | 7 +- ui/dbcloadsavewindow.ui | 7 ++ 6 files changed, 219 insertions(+), 2 deletions(-) diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index d22c023..4532372 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include #include "utility.h" #include "connections/canconmanager.h" @@ -1376,6 +1379,7 @@ int DBCHandler::createBlankFile() //add our custom attributes to the new file so that we know they're already there. attr.attrType = MESSAGE; attr.defaultValue = QApplication::palette().color(QPalette::Base).name(); + qDebug() << attr.defaultValue; attr.enumVals.clear(); attr.lower = 0; attr.upper = 0; @@ -1385,6 +1389,7 @@ int DBCHandler::createBlankFile() attr.attrType = MESSAGE; attr.defaultValue = QApplication::palette().color(QPalette::WindowText).name(); + qDebug() << attr.defaultValue; attr.enumVals.clear(); attr.lower = 0; attr.upper = 0; @@ -1401,6 +1406,11 @@ int DBCHandler::createBlankFile() attr.valType = QINT; newFile.dbc_attributes.append(attr); + DBC_NODE falseNode; + falseNode.name = "Vector__XXX"; + falseNode.comment = "Default node if none specified"; + newFile.dbc_nodes.append(falseNode); + loadedFiles.append(newFile); return loadedFiles.count(); } @@ -1440,6 +1450,178 @@ DBCFile* DBCHandler::loadDBCFile(int idx) return nullptr; } +DBCFile* DBCHandler::loadJSONFile(int idx) +{ + if (idx > -1 && idx < loadedFiles.count()) removeDBCFile(idx); + + QString filename; + QFileDialog dialog; + QSettings settings; + DBCFile *thisFile; + DBC_MESSAGE *pMsg; + + QStringList filters; + filters.append(QString(tr("JSON File (*.json *.JSON)"))); + + dialog.setDirectory(settings.value("DBC/LoadSaveDirectory", dialog.directory().path()).toString()); + dialog.setFileMode(QFileDialog::ExistingFile); + dialog.setNameFilters(filters); + dialog.setViewMode(QFileDialog::Detail); + + if (dialog.exec() == QDialog::Accepted) + { + filename = dialog.selectedFiles()[0]; + //right now there is only one file type that can be loaded here so just do it. + createBlankFile(); + thisFile = &loadedFiles.last(); + QFile *inFile = new QFile(filename); + if (!inFile->open(QIODevice::ReadOnly | QIODevice::Text)) + { + qDebug() << "Could not open JSON file for reading."; + delete inFile; + return nullptr; + } + QByteArray wholeFileData = inFile->readAll(); + inFile->close(); + delete inFile; + //qDebug() << "Data Length: " << wholeFileData.length(); + QJsonDocument jsonDoc = QJsonDocument::fromJson(wholeFileData); + if (jsonDoc.isNull()) + { + qDebug() << "Couldn't load the json file, shit the bed, don't know why I did it."; + return nullptr; + } + qDebug() << "Loaded JSON"; + + QJsonObject jsonObj = jsonDoc.object(); + QJsonObject jsonMessages = jsonObj["messages"].toObject(); + + QJsonObject::iterator iter; + for (iter = jsonMessages.begin(); iter != jsonMessages.end(); iter++) + { + qDebug() << iter.key(); + DBC_MESSAGE msg; + msg.ID = static_cast(iter->toObject().find("origin_id").value().toInt()); + msg.name = QString(iter.key().toUtf8()); + msg.len = static_cast(iter->toObject().find("length_bytes").value().toInt()); + msg.sender = thisFile->findNodeByIdx(0); + QString nodeName = iter->toObject().find("originNode").value().toString(); + msg.sender = thisFile->findNodeByName(nodeName); + msg.bgColor = QColor(); + msg.fgColor = QColor(); + if (!msg.sender && nodeName.length() > 1) + { + DBC_NODE node; + node.name = nodeName; + thisFile->dbc_nodes.append(node); + msg.sender = thisFile->findNodeByName(nodeName); + } + if (nodeName.length() < 2) msg.sender = thisFile->findNodeByIdx(0); + thisFile->messageHandler->addMessage(msg); + pMsg = thisFile->messageHandler->findMsgByID(msg.ID); + if (!pMsg) + { + qDebug() << "pMsg was null... I have no idea how that is possible. DEBUG ME"; + return nullptr; + } + + QJsonObject jsonSigs = iter->toObject().find("signals").value().toObject(); + QJsonObject::iterator sigIter; + for (sigIter = jsonSigs.begin(); sigIter != jsonSigs.end(); sigIter++) + { + qDebug() << sigIter.key(); + DBC_SIGNAL sig; + QJsonObject sigObj = sigIter->toObject(); + if (sigObj.isEmpty()) + { + qDebug() << "EMPTY!?"; + } + sig.name = QString(sigIter.key().toUtf8()); + sig.factor = sigObj.find("scale").value().toDouble(); + sig.bias = sigObj.find("offset").value().toDouble(); + sig.max = sigObj.find("max").value().toInt(); + sig.min = sigObj.find("min").value().toInt(); + sig.startBit = sigObj.find("start_position").value().toInt(); + sig.unitName = sigObj.find("units").value().toString(); + sig.signalSize = sigObj.find("width").value().toInt(); + sig.isMultiplexed = false; + sig.isMultiplexor = false; + sig.parentMessage = pMsg; + if (!sigObj.find("mux_id")->isUndefined()) + { + QJsonValue muxVal = sigObj.find("mux_id").value(); + sig.multiplexValue = muxVal.toInt(); + sig.isMultiplexed = true; + } + else + { + sig.multiplexValue = 0; + } + QJsonValue muxerVal = sigObj.find("is_muxer").value(); + if (!muxerVal.isNull()) + { + if (muxerVal.toBool()) + { + sig.isMultiplexor = true; + } + } + + QJsonObject valuesObj = sigObj.find("value_description")->toObject(); + if (!valuesObj.isEmpty()) + { + QJsonObject::iterator valIter; + for (valIter = valuesObj.begin(); valIter != valuesObj.end(); valIter++) + { + DBC_VAL_ENUM_ENTRY valEnum; + valEnum.value = valIter.value().toInt(); + valEnum.descript = valIter.key().toUtf8(); + sig.valList.append(valEnum); + } + } + + QJsonArray rxArray = sigObj.find("receivers")->toArray(); + if (rxArray.size() < 1) sig.receiver = thisFile->findNodeByIdx(0); + else + { + qDebug() << rxArray[0].toString(); + sig.receiver = thisFile->findNodeByName(rxArray[0].toString()); + if (!sig.receiver && rxArray[0].toString().length() > 1) + { + DBC_NODE node; + node.name = rxArray[0].toString(); + thisFile->dbc_nodes.append(node); + sig.receiver = thisFile->findNodeByName(rxArray[0].toString()); + } + if (!sig.receiver) sig.receiver = thisFile->findNodeByIdx(0); + } + if (!sigObj.find("endianness").value().toString().compare("BIG")) + { + sig.intelByteOrder = false; + } + else sig.intelByteOrder = true; + + if (!sigObj.find("signedness").value().toString().compare("UNSIGNED")) + { + sig.valType = DBC_SIG_VAL_TYPE::UNSIGNED_INT; + } + else sig.valType = DBC_SIG_VAL_TYPE::SIGNED_INT; + + pMsg->sigHandler->addSignal(sig); + if (sig.isMultiplexor) //if this signal was the multiplexor then store that info + { + DBC_SIGNAL *pSig = pMsg->sigHandler->findSignalByName(sig.name); + if (pSig) pMsg->multiplexorSignal = pSig; + } + } + } + + settings.setValue("DBC/LoadSaveDirectory", dialog.directory().path()); + return thisFile; + } + + return nullptr; +} + void DBCHandler::removeDBCFile(int idx) { if (loadedFiles.count() == 0) return; diff --git a/dbc/dbchandler.h b/dbc/dbchandler.h index ebe48e1..c767a7d 100644 --- a/dbc/dbchandler.h +++ b/dbc/dbchandler.h @@ -101,6 +101,7 @@ public: DBCFile* getFileByIdx(int idx); DBCFile* getFileByName(QString name); int createBlankFile(); + DBCFile* loadJSONFile(int); static DBCHandler *getReference(); private: diff --git a/dbc/dbcloadsavewindow.cpp b/dbc/dbcloadsavewindow.cpp index 327252b..91bee2f 100644 --- a/dbc/dbcloadsavewindow.cpp +++ b/dbc/dbcloadsavewindow.cpp @@ -35,6 +35,7 @@ DBCLoadSaveWindow::DBCLoadSaveWindow(const QVector *frames, QWidget *p connect(ui->btnNewDBC, &QAbstractButton::clicked, this, &DBCLoadSaveWindow::newFile); connect(ui->tableFiles, &QTableWidget::cellChanged, this, &DBCLoadSaveWindow::cellChanged); connect(ui->tableFiles, &QTableWidget::cellDoubleClicked, this, &DBCLoadSaveWindow::cellDoubleClicked); + connect(ui->btnLoadJSON, &QAbstractButton::clicked, this, &DBCLoadSaveWindow::loadJSON); editorWindow = new DBCMainEditor(frames, this); currentlyEditingFile = nullptr; @@ -101,6 +102,28 @@ void DBCLoadSaveWindow::loadFile() } } +void DBCLoadSaveWindow::loadJSON() +{ + DBCFile *file = dbcHandler->loadJSONFile(-1); + if(file) { + int idx = ui->tableFiles->rowCount(); + ui->tableFiles->insertRow(ui->tableFiles->rowCount()); + ui->tableFiles->setItem(idx, 0, new QTableWidgetItem(file->getFullFilename())); + ui->tableFiles->setItem(idx, 1, new QTableWidgetItem("-1")); + DBC_ATTRIBUTE *attr = file->findAttributeByName("isj1939dbc"); + QTableWidgetItem *item = new QTableWidgetItem(""); + ui->tableFiles->setItem(idx, 2, item); + if (attr && attr->defaultValue > 0) + { + item->setCheckState(Qt::Checked); + } + else + { + item->setCheckState(Qt::Unchecked); + } + } +} + void DBCLoadSaveWindow::saveFile() { int idx = ui->tableFiles->currentRow(); diff --git a/dbc/dbcloadsavewindow.h b/dbc/dbcloadsavewindow.h index d724fc1..18625cc 100644 --- a/dbc/dbcloadsavewindow.h +++ b/dbc/dbcloadsavewindow.h @@ -20,6 +20,7 @@ public: private slots: void loadFile(); + void loadJSON(); void saveFile(); void removeFile(); void moveUp(); diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index e8d9af4..aea0f9b 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -522,9 +522,12 @@ void DBCMainEditor::refreshMessagesTable(const DBC_NODE *node) QTableWidgetItem *msgLen = new QTableWidgetItem(QString::number(msg->len)); QTableWidgetItem *msgSignals = new QTableWidgetItem(QString::number(msg->sigHandler->getCount())); QTableWidgetItem *fgColor = new QTableWidgetItem(""); - fgColor->setBackgroundColor(msg->fgColor); + if (msg->fgColor.isValid()) fgColor->setBackgroundColor(msg->fgColor); + else fgColor->setBackgroundColor(QColor(dbcFile->findAttributeByName("GenMsgForegroundColor")->defaultValue.toString())); QTableWidgetItem *bgColor = new QTableWidgetItem(""); - bgColor->setBackgroundColor(msg->bgColor); + if (msg->bgColor.isValid()) bgColor->setBackgroundColor(msg->bgColor); + else bgColor->setBackgroundColor(QColor(dbcFile->findAttributeByName("GenMsgBackgroundColor")->defaultValue.toString())); + QTableWidgetItem *msgComment = new QTableWidgetItem(msg->comment); rowIdx = ui->MessagesTable->rowCount(); diff --git a/ui/dbcloadsavewindow.ui b/ui/dbcloadsavewindow.ui index d32abf1..6d936ff 100644 --- a/ui/dbcloadsavewindow.ui +++ b/ui/dbcloadsavewindow.ui @@ -24,6 +24,13 @@ + + + + Load Tesla JSON (shhhh....) + + + From 7f8411f47fafb246b4aa6d075829437226dd5ce4 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 12 Feb 2020 21:16:02 -0500 Subject: [PATCH 06/58] Fixes to hopefully make signal colors look proper. --- dbc/dbchandler.cpp | 4 ++-- dbc/dbcmaineditor.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index 4532372..d56d18b 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -1507,8 +1507,8 @@ DBCFile* DBCHandler::loadJSONFile(int idx) msg.sender = thisFile->findNodeByIdx(0); QString nodeName = iter->toObject().find("originNode").value().toString(); msg.sender = thisFile->findNodeByName(nodeName); - msg.bgColor = QColor(); - msg.fgColor = QColor(); + msg.bgColor = QColor(thisFile->findAttributeByName("GenMsgBackgroundColor")->defaultValue.toString()); + msg.fgColor = QColor(thisFile->findAttributeByName("GenMsgForegroundColor")->defaultValue.toString()); if (!msg.sender && nodeName.length() > 1) { DBC_NODE node; diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index aea0f9b..ccf9a1b 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -288,8 +288,8 @@ void DBCMainEditor::onCellChangedMessage(int row,int col) newMsg.name = ""; newMsg.sender = node; newMsg.len = 0; - newMsg.fgColor = ui->MessagesTable->item(row, 4)->backgroundColor(); - newMsg.bgColor = ui->MessagesTable->item(row, 5)->backgroundColor(); + newMsg.fgColor = QColor(dbcFile->findAttributeByName("GenMsgForegroundColor")->defaultValue.toString()); + newMsg.bgColor = QColor(dbcFile->findAttributeByName("GenMsgBackgroundColor")->defaultValue.toString()); for (int i = 0; i < referenceFrames->length(); i++) { From d550fa79a0f1f15562737465fcf40704c2d7f2c8 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Thu, 13 Feb 2020 22:01:06 -0500 Subject: [PATCH 07/58] Changed a lot of pointer references to const (it's more correct and fixes a weird problem), fixed a problem with JSON to DBC, a few other fixes --- bus_protocols/isotp_handler.cpp | 15 ++++----- bus_protocols/uds_handler.cpp | 6 ++-- canframemodel.cpp | 3 +- connections/canconnection.h | 7 ++++ connections/serialbusconnection.cpp | 6 ++-- dbc/dbchandler.cpp | 2 +- firmwareuploaderwindow.cpp | 2 +- framefileio.cpp | 50 ++++++++++++++--------------- framesenderwindow.cpp | 2 +- mainwindow.cpp | 4 +-- re/filecomparatorwindow.cpp | 6 ++-- re/flowviewwindow.cpp | 8 ++--- re/frameinfowindow.cpp | 6 ++-- re/isotp_interpreterwindow.cpp | 4 +-- re/sniffer/snifferitem.cpp | 4 +-- re/udsscanwindow.cpp | 5 ++- scriptcontainer.cpp | 5 ++- utility.h | 3 ++ 18 files changed, 76 insertions(+), 62 deletions(-) diff --git a/bus_protocols/isotp_handler.cpp b/bus_protocols/isotp_handler.cpp index d56477c..4293699 100644 --- a/bus_protocols/isotp_handler.cpp +++ b/bus_protocols/isotp_handler.cpp @@ -59,12 +59,13 @@ void ISOTP_HANDLER::sendISOTPFrame(int bus, int ID, QByteArray data) lastSenderID = ID; lastSenderBus = bus; + frame.bus = bus; + frame.setFrameId(ID); + if (ID > 0x7FF) frame.setExtendedFrameFormat(true); + else frame.setExtendedFrameFormat(false); + if (data.length() < 8) { - frame.bus = bus; - frame.setFrameId(ID); - if (ID > 0x7FF) frame.setExtendedFrameFormat(true); - else frame.setExtendedFrameFormat(false); QByteArray bytes(8,0); bytes.resize(8); bytes[0] = data.length(); @@ -74,10 +75,6 @@ void ISOTP_HANDLER::sendISOTPFrame(int bus, int ID, QByteArray data) } else //need to send a multi-part ISO_TP message - Respects timing and frame number based flow control { - frame.bus = bus; - frame.setFrameId(ID); - if (ID > 0x7FF) frame.setExtendedFrameFormat(true); - else frame.setExtendedFrameFormat(false); QByteArray bytes(8, 0); bytes[0] = 0x10 + (data.length() / 256); bytes[1] = data.length() & 0xFF; @@ -159,7 +156,7 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) //int offset; ISOTP_MESSAGE msg; ISOTP_MESSAGE *pMsg; - unsigned char *data = reinterpret_cast(frame.payload().data()); + const unsigned char *data = reinterpret_cast(frame.payload().constData()); //int dataLen = frame.payload().count(); frameType = 0; diff --git a/bus_protocols/uds_handler.cpp b/bus_protocols/uds_handler.cpp index e0fb085..29bbb47 100644 --- a/bus_protocols/uds_handler.cpp +++ b/bus_protocols/uds_handler.cpp @@ -177,7 +177,7 @@ UDS_HANDLER::~UDS_HANDLER() void UDS_HANDLER::gotISOTPFrame(ISOTP_MESSAGE msg) { qDebug() << "UDS handler got ISOTP frame"; - unsigned char *data = reinterpret_cast(msg.payload().data()); + const unsigned char *data = reinterpret_cast(msg.payload().constData()); int dataLen = msg.payload().count(); UDS_MESSAGE udsMsg; udsMsg.bus = msg.bus; @@ -258,6 +258,8 @@ void UDS_HANDLER::sendUDSFrame(const UDS_MESSAGE &msg) data.append(msg.payload()); isoHandler->sendISOTPFrame(msg.bus, msg.frameId(), data); + //qDebug() << "Data sending: " << data; + qDebug() << "Sent UDS service: " << getServiceShortDesc(msg.service) << " on bus " << msg.bus; } @@ -327,7 +329,7 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) bool isResponse = true; int dataSize; int addrSize; - unsigned char *data = reinterpret_cast(msg.payload().data()); + const unsigned char *data = reinterpret_cast(msg.payload().constData()); int dataLen = msg.payload().length(); if (msg.isErrorReply) diff --git a/canframemodel.cpp b/canframemodel.cpp index 167f822..486a1da 100644 --- a/canframemodel.cpp +++ b/canframemodel.cpp @@ -360,7 +360,7 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const thisFrame = filteredFrames.at(index.row()); - unsigned char *data = reinterpret_cast(thisFrame.payload().data()); + const unsigned char *data = reinterpret_cast(thisFrame.payload().constData()); int dataLen = thisFrame.payload().count(); if (role == Qt::BackgroundColorRole) @@ -551,6 +551,7 @@ void CANFrameModel::addFrame(const CANFrame& frame, bool autoRefresh = false) mutex.lock(); CANFrame tempFrame; tempFrame = frame; + tempFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, tempFrame.timeStamp().microSeconds() - timeOffset)); lastUpdateNumFrames++; diff --git a/connections/canconnection.h b/connections/canconnection.h index a450134..38d1160 100644 --- a/connections/canconnection.h +++ b/connections/canconnection.h @@ -77,6 +77,12 @@ public: */ CANCon::status getStatus(); + /** + * @brief setConsoleOutput + * @param state - set whether to send debugging info to the console or not + */ + void setConsoleOutput(bool state); + signals: /*not implemented yet */ @@ -194,6 +200,7 @@ public slots: protected: int mNumBuses; //protected to allow connected device to figure out how many buses are available QVector mBusData; + bool mConsoleOutput; //send debugging info to the console? //determine if the passed frame is part of a filter or not. void checkTargettedFrame(CANFrame &frame); diff --git a/connections/serialbusconnection.cpp b/connections/serialbusconnection.cpp index 72e1e90..bda07f8 100644 --- a/connections/serialbusconnection.cpp +++ b/connections/serialbusconnection.cpp @@ -119,6 +119,8 @@ bool SerialBusConnection::piSendFrame(const CANFrame& pFrame) return false; if (!mDev_p) return false; + qDebug() << "Data bytes at lowlevel: " << pFrame.payload().toBase64(); + return mDev_p->writeFrame(pFrame); } @@ -179,7 +181,7 @@ void SerialBusConnection::framesReceived() continue; /* check frame */ - if (recFrame.payload().length() <= 8) { + //if (recFrame.payload().length() <= 8) { CANFrame* frame_p = getQueue().get(); if(frame_p) { frame_p->setPayload(recFrame.payload()); @@ -241,7 +243,7 @@ void SerialBusConnection::framesReceived() /* enqueue frame */ getQueue().queue(); - } + //} #if 0 else qDebug() << "can't get a frame, ERROR"; diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index b2bc9ef..0393e36 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -1556,7 +1556,7 @@ DBCFile* DBCHandler::loadJSONFile(int idx) { qDebug() << iter.key(); DBC_MESSAGE msg; - msg.ID = static_cast(iter->toObject().find("origin_id").value().toInt()); + msg.ID = static_cast(iter->toObject().find("message_id").value().toInt()); msg.name = QString(iter.key().toUtf8()); msg.len = static_cast(iter->toObject().find("length_bytes").value().toInt()); msg.sender = thisFile->findNodeByIdx(0); diff --git a/firmwareuploaderwindow.cpp b/firmwareuploaderwindow.cpp index 6480e25..44571d3 100644 --- a/firmwareuploaderwindow.cpp +++ b/firmwareuploaderwindow.cpp @@ -77,7 +77,7 @@ void FirmwareUploaderWindow::updatedFrames(int numFrames) void FirmwareUploaderWindow::gotTargettedFrame(CANFrame frame) { - unsigned char *data = reinterpret_cast(frame.payload().data()); + const unsigned char *data = reinterpret_cast(frame.payload().constData()); int dataLen = frame.payload().count(); qDebug() << "FUW: Got targetted frame with id " << frame.frameId(); diff --git a/framefileio.cpp b/framefileio.cpp index 741d8b1..c5a8439 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -947,7 +947,7 @@ bool FrameFileIO::saveCRTDFile(QString filename, const QVector* frames QFile *outFile = new QFile(filename); int lineCounter = 0; - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -971,7 +971,7 @@ bool FrameFileIO::saveCRTDFile(QString filename, const QVector* frames } frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); outFile->write(QString::number(frame->timeStamp().microSeconds() / 1000000.0, 'f', 6).toUtf8()); @@ -1346,7 +1346,7 @@ bool FrameFileIO::saveCanalyzerASC(QString filename, const QVector* fr int lineCounter = 0; int64_t offsetTime = frames->at(0).timeStamp().microSeconds(); - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -1383,7 +1383,7 @@ bool FrameFileIO::saveCanalyzerASC(QString filename, const QVector* fr } frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); uint64_t timeStamp = (frame->timeStamp().microSeconds() - offsetTime) / 1000000ull; @@ -1614,7 +1614,7 @@ bool FrameFileIO::saveNativeCSVFile(QString filename, const QVector* f QFile *outFile = new QFile(filename); int lineCounter = 0; - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -1637,7 +1637,7 @@ bool FrameFileIO::saveNativeCSVFile(QString filename, const QVector* f } frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); outFile->write(QString::number(frame->timeStamp().microSeconds()).toUtf8()); @@ -1720,7 +1720,7 @@ bool FrameFileIO::closeContinuousNative() bool FrameFileIO::writeContinuousNative(const QVector* frames, int beginningFrame) { - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -1729,7 +1729,7 @@ bool FrameFileIO::writeContinuousNative(const QVector* frames, int beg for (int c = beginningFrame; c < frames->count(); c++) { frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); continuousFile.write(QString::number(frame->timeStamp().microSeconds()).toUtf8()); @@ -1880,7 +1880,7 @@ bool FrameFileIO::saveGenericCSVFile(QString filename, const QVector* QFile *outFile = new QFile(filename); int lineCounter = 0; - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -1903,7 +1903,7 @@ bool FrameFileIO::saveGenericCSVFile(QString filename, const QVector* } frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); outFile->write(QString::number(frame->frameId(), 16).toUpper().rightJustified(8, '0').toUtf8()); @@ -2086,7 +2086,7 @@ bool FrameFileIO::saveLogFile(QString filename, const QVector* frames) QDateTime timestamp, tempStamp; int lineCounter = 0; - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -2124,7 +2124,7 @@ bool FrameFileIO::saveLogFile(QString filename, const QVector* frames) } frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); tempStamp = QDateTime::fromMSecsSinceEpoch(frame->timeStamp().microSeconds() / 1000); @@ -2282,7 +2282,7 @@ bool FrameFileIO::saveIXXATFile(QString filename, const QVector* frame QDateTime timestamp, tempStamp; int lineCounter = 0; - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -2313,7 +2313,7 @@ bool FrameFileIO::saveIXXATFile(QString filename, const QVector* frame } frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); tempStamp = QDateTime::fromMSecsSinceEpoch(frame->timeStamp().microSeconds() / 1000); @@ -2418,7 +2418,7 @@ bool FrameFileIO::loadCANDOFile(QString filename, QVector* frames) } data = inFile->read(12); - unsigned char *uData = reinterpret_cast(data.data()); + const unsigned char *uData = reinterpret_cast(data.constData()); thisFrame.bus = 0; thisFrame.isReceived = true; @@ -2460,7 +2460,7 @@ bool FrameFileIO::saveCANDOFile(QString filename, const QVector* frame int id; qint64 ms; - unsigned char *inData; + const unsigned char *inData; int inDataLen; const CANFrame *frame; @@ -2492,7 +2492,7 @@ bool FrameFileIO::saveCANDOFile(QString filename, const QVector* frame } frame = &frames->at(c); - inData = reinterpret_cast(frame->payload().data()); + inData = reinterpret_cast(frame->payload().constData()); inDataLen = frame->payload().count(); for (int j = 0; j < 8; j++) data[4 + j] = (char)0xFF; @@ -2660,7 +2660,7 @@ bool FrameFileIO::saveMicrochipFile(QString filename, const QVector* f QDateTime timestamp, tempStamp; int lineCounter = 0; - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -2691,7 +2691,7 @@ bool FrameFileIO::saveMicrochipFile(QString filename, const QVector* f } frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); outFile->write(QString::number((frame->timeStamp().microSeconds() / 1000)).toUtf8()); @@ -2875,7 +2875,7 @@ bool FrameFileIO::saveTraceFile(QString filename, const QVector * fram int64_t tempTime; int tempTimePiece; - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -2918,7 +2918,7 @@ bool FrameFileIO::saveTraceFile(QString filename, const QVector * fram } frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); //1F D3 3F FF 08 FF E0 CB @@ -2970,7 +2970,7 @@ bool FrameFileIO::saveCanDumpFile(QString filename, const QVector * fr int lineCounter = 0; double tempTime; - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -2991,7 +2991,7 @@ bool FrameFileIO::saveCanDumpFile(QString filename, const QVector * fr } frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); outFile->write("("); @@ -3599,7 +3599,7 @@ bool FrameFileIO::saveCabanaFile(QString filename, const QVector* fram QFile *outFile = new QFile(filename); int lineCounter = 0; - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -3622,7 +3622,7 @@ bool FrameFileIO::saveCabanaFile(QString filename, const QVector* fram } frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); double tempTimeStamp = frame->timeStamp().microSeconds(); diff --git a/framesenderwindow.cpp b/framesenderwindow.cpp index d107428..2d17b36 100644 --- a/framesenderwindow.cpp +++ b/framesenderwindow.cpp @@ -792,7 +792,7 @@ void FrameSenderWindow::updateGridRow(int idx) int gridLine = idx; QString dataString; QTableWidgetItem *item = ui->tableSender->item(gridLine, 9); - unsigned char *data = reinterpret_cast(temp->payload().data()); + const unsigned char *data = reinterpret_cast(temp->payload().constData()); int dataLen = temp->payload().length(); if (item == nullptr) item = new QTableWidgetItem(); diff --git a/mainwindow.cpp b/mainwindow.cpp index 678e603..99bab53 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -729,7 +729,7 @@ void MainWindow::saveDecodedTextFile(QString filename) QFile *outFile = new QFile(filename); const QVector *frames = model->getFilteredListReference(); - unsigned char *data; + const unsigned char *data; int dataLen; const CANFrame *frame; @@ -743,7 +743,7 @@ Data Bytes: 88 10 00 13 BB 00 06 00 for (int c = 0; c < frames->count(); c++) { frame = &frames->at(c); - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().count(); QString builderString; diff --git a/re/filecomparatorwindow.cpp b/re/filecomparatorwindow.cpp index c85cc8c..8525470 100644 --- a/re/filecomparatorwindow.cpp +++ b/re/filecomparatorwindow.cpp @@ -123,7 +123,7 @@ void FileComparatorWindow::calculateDetails() QTreeWidgetItem *interestedOnlyBase, *referenceOnlyBase = nullptr, *sharedBase, *bitmapBaseInterested, *bitmapBaseReference = nullptr; QTreeWidgetItem *valuesBase, *detail, *sharedItem, *valuesInterested, *valuesReference = nullptr; uint64_t tmp; - unsigned char *data; + const unsigned char *data; int dataLen; bool uniqueInterested = ui->ckUniqueToInterested->isChecked(); @@ -154,7 +154,7 @@ void FileComparatorWindow::calculateDetails() for (int x = 0; x < interestedFrames.count(); x++) { CANFrame frame = interestedFrames.at(x); - data = reinterpret_cast(frame.payload().data()); + data = reinterpret_cast(frame.payload().constData()); dataLen = frame.payload().count(); if (interestedIDs.contains(frame.frameId())) //if we saw this ID before then add to the QList in there @@ -201,7 +201,7 @@ void FileComparatorWindow::calculateDetails() for (int x = 0; x < referenceFrames.count(); x++) { CANFrame frame = referenceFrames.at(x); - data = reinterpret_cast(frame.payload().data()); + data = reinterpret_cast(frame.payload().constData()); dataLen = frame.payload().count(); if (referenceIDs.contains(frame.frameId())) //if we saw this ID before then add to the QList in there diff --git a/re/flowviewwindow.cpp b/re/flowviewwindow.cpp index c247290..3cab711 100644 --- a/re/flowviewwindow.cpp +++ b/re/flowviewwindow.cpp @@ -397,7 +397,7 @@ void FlowViewWindow::updatedFrames(int numFrames) { QVectornewX[8]; QVectornewY[8]; - unsigned char *data; + const unsigned char *data; int dataLen = 0; const CANFrame *thisFrame; @@ -436,7 +436,7 @@ void FlowViewWindow::updatedFrames(int numFrames) for (int i = modelFrames->count() - numFrames; i < modelFrames->count(); i++) { thisFrame = &modelFrames->at(i); - data = reinterpret_cast(thisFrame->payload().data()); + data = reinterpret_cast(thisFrame->payload().constData()); dataLen = thisFrame->payload().length(); if (!foundID.contains(thisFrame->frameId())) @@ -502,7 +502,7 @@ void FlowViewWindow::createGraph(int byteNum) { int tempVal; float minval=1000000, maxval = -100000; - unsigned char *data; + const unsigned char *data; int dataLen = 0; const CANFrame *frame; @@ -520,7 +520,7 @@ void FlowViewWindow::createGraph(int byteNum) for (int j = 0; j < numEntries; j++) { frame = &frameCache[j]; - data = reinterpret_cast(frame->payload().data()); + data = reinterpret_cast(frame->payload().constData()); dataLen = frame->payload().length(); tempVal = data[byteNum]; diff --git a/re/frameinfowindow.cpp b/re/frameinfowindow.cpp index db954b4..fa5e8a9 100644 --- a/re/frameinfowindow.cpp +++ b/re/frameinfowindow.cpp @@ -278,7 +278,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) if (thisFrame.frameId() == static_cast(targettedID)) frameCache.append(thisFrame); } - unsigned char *data = reinterpret_cast(frameCache.at(0).payload().data()); + const unsigned char *data = reinterpret_cast(frameCache.at(0).payload().constData()); int dataLen = frameCache.at(0).payload().length(); ui->treeDetails->clear(); @@ -367,7 +367,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) } for (int j = 0; j < 64; j++) bitfieldHistogram[j] = 0; - data = reinterpret_cast(frameCache.at(0).payload().data()); + data = reinterpret_cast(frameCache.at(0).payload().constData()); dataLen = frameCache.at(0).payload().length(); for (int c = 0; c < dataLen; c++) @@ -383,7 +383,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) //then find all data points for (int j = 0; j < frameCache.count(); j++) { - data = reinterpret_cast(frameCache.at(j).payload().data()); + data = reinterpret_cast(frameCache.at(j).payload().constData()); dataLen = frameCache.at(j).payload().length(); byteGraphX.append(j); diff --git a/re/isotp_interpreterwindow.cpp b/re/isotp_interpreterwindow.cpp index d73e5d6..f18eafd 100644 --- a/re/isotp_interpreterwindow.cpp +++ b/re/isotp_interpreterwindow.cpp @@ -208,7 +208,7 @@ void ISOTP_InterpreterWindow::showDetailView() msg = &messages[rowNum]; - unsigned char *data = reinterpret_cast(msg->payload().data()); + const unsigned char *data = reinterpret_cast(msg->payload().constData()); int dataLen = msg->payload().length(); if (msg->reportedLength != dataLen) @@ -266,7 +266,7 @@ void ISOTP_InterpreterWindow::newISOMessage(ISOTP_MESSAGE msg) int rowNum; QString tempString; - unsigned char *data = reinterpret_cast(msg.payload().data()); + const unsigned char *data = reinterpret_cast(msg.payload().constData()); int dataLen = msg.payload().length(); if ((msg.reportedLength != dataLen) && !ui->cbShowIncomplete->isChecked()) return; diff --git a/re/sniffer/snifferitem.cpp b/re/sniffer/snifferitem.cpp index 54bb1b6..1194ee9 100644 --- a/re/sniffer/snifferitem.cpp +++ b/re/sniffer/snifferitem.cpp @@ -6,7 +6,7 @@ SnifferItem::SnifferItem(const CANFrame& pFrame, quint32 seq): mID(pFrame.frameId()) { - unsigned char *data = reinterpret_cast(pFrame.payload().data()); + const unsigned char *data = reinterpret_cast(pFrame.payload().constData()); int dataLen = pFrame.payload().length(); for (int i = 0; i < 0; i++) { @@ -117,7 +117,7 @@ void SnifferItem::update(const CANFrame& pFrame, quint32 timeSeq, bool mute) mLastTime = mCurrentTime; mCurrSeqVal = timeSeq; - unsigned char *data = reinterpret_cast(pFrame.payload().data()); + const unsigned char *data = reinterpret_cast(pFrame.payload().constData()); int dataLen = pFrame.payload().length(); /* copy new value */ diff --git a/re/udsscanwindow.cpp b/re/udsscanwindow.cpp index ecb4623..cf060d8 100644 --- a/re/udsscanwindow.cpp +++ b/re/udsscanwindow.cpp @@ -354,7 +354,7 @@ void UDSScanWindow::gotUDSReply(UDS_MESSAGE msg) int offset = ui->spinReplyOffset->value(); UDS_MESSAGE sentFrame; bool gotReply = false; - unsigned char *data = reinterpret_cast(msg.payload().data()); + const unsigned char *data = reinterpret_cast(msg.payload().constData()); int dataLen = msg.payload().length(); int numSending = sendingFrames.length(); @@ -488,6 +488,9 @@ void UDSScanWindow::sendNextMsg() else { waitTimer->stop(); + udsHandler->setReception(false); + udsHandler->setProcessAllIDs(false); + udsHandler->setFlowCtrl(false); ui->btnScan->setText("Start Scan"); currentlyRunning = false; } diff --git a/scriptcontainer.cpp b/scriptcontainer.cpp index f3fa836..30969be 100644 --- a/scriptcontainer.cpp +++ b/scriptcontainer.cpp @@ -255,7 +255,7 @@ void CANScriptHelper::gotTargettedFrame(const CANFrame &frame) if (!gotFrameFunction.isCallable()) return; //nothing to do if we can't even call the function //qDebug() << "Got frame in script interface"; - unsigned char *data = reinterpret_cast(frame.payload().data()); + const unsigned char *data = reinterpret_cast(frame.payload().constData()); int dataLen = frame.payload().length(); for (int i = 0; i < filters.length(); i++) @@ -310,14 +310,13 @@ void ISOTPScriptHelper::sendISOTP(QJSValue bus, QJSValue id, QJSValue length, QJ msg.setFrameId(id.toUInt()); msg.payload().resize(length.toInt()); - unsigned char *data = reinterpret_cast(msg.payload().data()); int dataLen = msg.payload().length(); if (!dataBytes.isArray()) qDebug() << "data isn't an array"; for (int i = 0; i < dataLen; i++) { - data[i] = static_cast(dataBytes.property(i).toInt()); + msg.payload()[i] = static_cast(dataBytes.property(i).toInt()); } msg.bus = bus.toInt(); diff --git a/utility.h b/utility.h index 8160aa7..090b0a3 100644 --- a/utility.h +++ b/utility.h @@ -185,6 +185,9 @@ public: int64_t result = 0; int bit; + int maxBytes = (startBit + sigSize) / 8; + if (data.size() < maxBytes) return 0; + if (littleEndian) { bit = startBit; From e4a3854af48db5c5fb2165c1e41566528d221496 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sat, 15 Feb 2020 19:52:18 -0500 Subject: [PATCH 08/58] Many little changes to get rid of compile-time warnings. There now should be no warnings when compiling on LINUX. YMMV elsewhere. --- bus_protocols/isotp_handler.cpp | 4 +-- bus_protocols/isotp_handler.h | 4 +-- bus_protocols/uds_handler.cpp | 4 --- connections/canbus.h | 3 ++- connections/canconmanager.cpp | 2 +- connections/canconnection.cpp | 2 +- connections/gvretserial.cpp | 6 ++--- connections/gvretserial.h | 2 +- dbc/dbchandler.cpp | 6 ++--- dbc/dbcloadsavewindow.cpp | 4 ++- dbc/dbcmaineditor.cpp | 32 ++++++++++++------------ jsedit.cpp | 4 +-- mainwindow.cpp | 16 ++++++------ mainwindow.h | 2 +- motorcontrollerconfigwindow.cpp | 9 +++---- re/filecomparatorwindow.cpp | 26 +++++++++---------- re/filecomparatorwindow.h | 2 +- re/flowviewwindow.cpp | 6 ++--- re/flowviewwindow.h | 2 +- re/frameinfowindow.cpp | 44 ++++++++++++++++----------------- re/graphingwindow.h | 2 +- 21 files changed, 89 insertions(+), 93 deletions(-) diff --git a/bus_protocols/isotp_handler.cpp b/bus_protocols/isotp_handler.cpp index 4293699..583cdc5 100644 --- a/bus_protocols/isotp_handler.cpp +++ b/bus_protocols/isotp_handler.cpp @@ -356,7 +356,7 @@ void ISOTP_HANDLER::setProcessAll(bool state) processAll = state; } -void ISOTP_HANDLER::addFilter(uint32_t pBusId, uint32_t ID, uint32_t mask) +void ISOTP_HANDLER::addFilter(int pBusId, uint32_t ID, uint32_t mask) { CANFilter filt; filt.ID = ID; @@ -366,7 +366,7 @@ void ISOTP_HANDLER::addFilter(uint32_t pBusId, uint32_t ID, uint32_t mask) filters.append(filt); } -void ISOTP_HANDLER::removeFilter(uint32_t pBusId, uint32_t ID, uint32_t mask) +void ISOTP_HANDLER::removeFilter(int pBusId, uint32_t ID, uint32_t mask) { for (int i = 0; i < filters.count(); i++) { diff --git a/bus_protocols/isotp_handler.h b/bus_protocols/isotp_handler.h index 72ada5e..f45e0ba 100644 --- a/bus_protocols/isotp_handler.h +++ b/bus_protocols/isotp_handler.h @@ -22,8 +22,8 @@ public: void sendISOTPFrame(int bus, int ID, QByteArray data); void setProcessAll(bool state); void setFlowCtrl(bool state); - void addFilter(uint32_t pBusId, uint32_t ID, uint32_t mask); - void removeFilter(uint32_t pBusId, uint32_t ID, uint32_t mask); + void addFilter(int pBusId, uint32_t ID, uint32_t mask); + void removeFilter(int pBusId, uint32_t ID, uint32_t mask); void clearAllFilters(); public slots: diff --git a/bus_protocols/uds_handler.cpp b/bus_protocols/uds_handler.cpp index 29bbb47..d218c5d 100644 --- a/bus_protocols/uds_handler.cpp +++ b/bus_protocols/uds_handler.cpp @@ -326,7 +326,6 @@ QString UDS_HANDLER::getNegativeResponseLong(int respCode) QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) { QString buildString; - bool isResponse = true; int dataSize; int addrSize; const unsigned char *data = reinterpret_cast(msg.payload().constData()); @@ -334,18 +333,15 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) if (msg.isErrorReply) { - isResponse = true; buildString.append("UDS ERROR Response\n"); buildString.append("Service: " + getServiceLongDesc(msg.service) + "\n"); } else if (msg.service < 0x3F || (msg.service > 0x7F && msg.service < 0xAF)) { - isResponse = false; buildString.append("UDS Request\n"); buildString.append("Service: " + getServiceLongDesc(msg.service) + "\n"); } else { - isResponse = true; buildString.append("UDS Positive Response\n"); buildString.append("Service: " + getServiceLongDesc(msg.service - 0x40) + "\n"); } diff --git a/connections/canbus.h b/connections/canbus.h index fb2d382..b127059 100644 --- a/connections/canbus.h +++ b/connections/canbus.h @@ -9,7 +9,8 @@ public: CANBus(); CANBus(const CANBus&); bool operator==(const CANBus&) const; - virtual ~CANBus(){}; + CANBus& operator=(const CANBus& other) = default; + //virtual ~CANBus(){} int speed; bool listenOnly; diff --git a/connections/canconmanager.cpp b/connections/canconmanager.cpp index 9ae1f11..a1e0cab 100644 --- a/connections/canconmanager.cpp +++ b/connections/canconmanager.cpp @@ -214,7 +214,7 @@ bool CANConManager::sendFrame(const CANFrame& pFrame) foreach (CANConnection* conn, mConns) { //check if this CAN connection is supposed to handle the requested bus - if (pFrame.bus < (uint32_t)(busBase + conn->getNumBuses())) + if (pFrame.bus < (busBase + conn->getNumBuses())) { workingFrame.bus -= busBase; workingFrame.isReceived = false; diff --git a/connections/canconnection.cpp b/connections/canconnection.cpp index 27850ff..ef6e176 100644 --- a/connections/canconnection.cpp +++ b/connections/canconnection.cpp @@ -8,8 +8,8 @@ CANConnection::CANConnection(QString pPort, int pNumBuses, int pQueueLen, bool pUseThread) : - mQueue(), mNumBuses(pNumBuses), + mQueue(), mPort(pPort), mDriver(pDriver), mType(pType), diff --git a/connections/gvretserial.cpp b/connections/gvretserial.cpp index d84a875..4275f0f 100644 --- a/connections/gvretserial.cpp +++ b/connections/gvretserial.cpp @@ -9,9 +9,9 @@ #include "gvretserial.h" GVRetSerial::GVRetSerial(QString portName, bool useTcp) : - CANConnection(portName, "gvret", CANCon::GVRET_SERIAL, 3, 4000, true), - useTcp(useTcp), - mTimer(this) /*NB: set this as parent of timer to manage it from working thread */ + CANConnection(portName, "gvret", CANCon::GVRET_SERIAL, 3, 4000, true), + mTimer(this), /*NB: set this as parent of timer to manage it from working thread */ + useTcp(useTcp) { sendDebug("GVRetSerial()"); diff --git a/connections/gvretserial.h b/connections/gvretserial.h index 3df1350..4dc5e47 100644 --- a/connections/gvretserial.h +++ b/connections/gvretserial.h @@ -91,7 +91,7 @@ protected: QUdpSocket *udpClient; int framesRapid; STATE rx_state; - uint32_t rx_step; + int rx_step; CANFrame buildFrame; qint64 buildTimestamp; quint32 buildId; diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index 0393e36..15b2207 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -46,7 +46,7 @@ bool DBCSignalHandler::addSignal(DBC_SIGNAL &sig) bool DBCSignalHandler::removeSignal(DBC_SIGNAL *sig) { - Q_UNUSED(sig); + Q_UNUSED(sig) //if (sigs.removeAll(*sig) > 0) return true; return false; } @@ -1698,7 +1698,7 @@ void DBCHandler::swapFiles(int pos1, int pos2) if (pos2 < 0) return; if (pos2 >= loadedFiles.count()) return; - loadedFiles.swap(pos1, pos2); + loadedFiles.swapItemsAt(pos1, pos2); } /* @@ -1711,7 +1711,7 @@ DBC_MESSAGE* DBCHandler::findMessage(const CANFrame &frame) { for(int i = 0; i < loadedFiles.count(); i++) { - if (loadedFiles[i].getAssocBus() == -1 || frame.bus == (unsigned int)loadedFiles[i].getAssocBus()) + if (loadedFiles[i].getAssocBus() == -1 || frame.bus == loadedFiles[i].getAssocBus()) { DBC_MESSAGE* msg = loadedFiles[i].messageHandler->findMsgByID(frame.frameId()); if (msg != nullptr) return msg; diff --git a/dbc/dbcloadsavewindow.cpp b/dbc/dbcloadsavewindow.cpp index 0c9b400..5cdedd9 100644 --- a/dbc/dbcloadsavewindow.cpp +++ b/dbc/dbcloadsavewindow.cpp @@ -270,6 +270,8 @@ void DBCLoadSaveWindow::editFile() void DBCLoadSaveWindow::matchingCriteriaChanged(int index) { + Q_UNUSED(index) + if (inhibitCellProcessing) return; // We don't know which combobox changed, so we just update all of them for (int row=0; rowtableFiles->rowCount(); row++) @@ -312,7 +314,7 @@ void DBCLoadSaveWindow::cellChanged(int row, int col) { DBCFile *file = dbcHandler->getFileByIdx(row); int bus = ui->tableFiles->item(row, col)->text().toInt(); - int numBuses = CANConManager::getInstance()->getNumBuses(); + //int numBuses = CANConManager::getInstance()->getNumBuses(); if (bus > -2) { file->setAssocBus(bus); diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index 8687ce6..df2e906 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -102,7 +102,7 @@ void DBCMainEditor::setFileIdx(int idx) void DBCMainEditor::closeEvent(QCloseEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event) writeSettings(); sigEditor->close(); } @@ -197,7 +197,7 @@ void DBCMainEditor::onCellChangedNode(int row,int col) if (dbcFile->findNodeByName(newName) != nullptr) //duplicates an existing node! { QMessageBox msg; - msg.setParent(0); + msg.setParent(nullptr); msg.setText("An existing node with that name already exists! Aborting!"); msg.exec(); return; @@ -275,7 +275,7 @@ void DBCMainEditor::onCellChangedMessage(int row,int col) if (msg != nullptr) { QMessageBox msg; - msg.setParent(0); + msg.setParent(nullptr); msg.setText("An existing msg with that ID already exists! Aborting!"); msg.exec(); @@ -296,7 +296,7 @@ void DBCMainEditor::onCellChangedMessage(int row,int col) { if ((uint) referenceFrames->at(i).frameId() == msgID) { - newMsg.len = referenceFrames->at(i).payload().length(); + newMsg.len = static_cast(referenceFrames->at(i).payload().length()); break; } } @@ -395,14 +395,14 @@ void DBCMainEditor::onCellClickedMessage(int row, int col) QTableWidgetItem* firstCol = ui->MessagesTable->item(row, 0); bool ret = false; DBC_MESSAGE *msg; - int msgID; + uint32_t msgID; if (col == 3) //3 is the signals field. If clicked we go to the signals dialog { QTableWidgetItem* msg = ui->MessagesTable->item(row, 0); if(msg) { QString idString = msg->text(); - DBC_MESSAGE *message = dbcFile->messageHandler->findMsgByID(Utility::ParseStringToNum(idString)); + DBC_MESSAGE *message = dbcFile->messageHandler->findMsgByID(static_cast(Utility::ParseStringToNum(idString))); sigEditor->setMessageRef(message); sigEditor->setFileIdx(fileIdx); sigEditor->setWindowModality(Qt::WindowModal); @@ -416,8 +416,8 @@ void DBCMainEditor::onCellClickedMessage(int row, int col) } if (col == 4) { - QColor newColor = QColorDialog::getColor(thisItem->backgroundColor()); - thisItem->setBackgroundColor(newColor); + QColor newColor = QColorDialog::getColor(thisItem->background().color()); + thisItem->setBackground(newColor); if(!firstCol) return; @@ -443,8 +443,8 @@ void DBCMainEditor::onCellClickedMessage(int row, int col) } if (col == 5) { - QColor newColor = QColorDialog::getColor(thisItem->backgroundColor()); - thisItem->setBackgroundColor(newColor); + QColor newColor = QColorDialog::getColor(thisItem->background().color()); + thisItem->setBackground(newColor); if(!firstCol) return; @@ -523,11 +523,11 @@ void DBCMainEditor::refreshMessagesTable(const DBC_NODE *node) QTableWidgetItem *msgLen = new QTableWidgetItem(QString::number(msg->len)); QTableWidgetItem *msgSignals = new QTableWidgetItem(QString::number(msg->sigHandler->getCount())); QTableWidgetItem *fgColor = new QTableWidgetItem(""); - if (msg->fgColor.isValid()) fgColor->setBackgroundColor(msg->fgColor); - else fgColor->setBackgroundColor(QColor(dbcFile->findAttributeByName("GenMsgForegroundColor")->defaultValue.toString())); + if (msg->fgColor.isValid()) fgColor->setBackground(msg->fgColor); + else fgColor->setBackground(QColor(dbcFile->findAttributeByName("GenMsgForegroundColor")->defaultValue.toString())); QTableWidgetItem *bgColor = new QTableWidgetItem(""); - if (msg->bgColor.isValid()) bgColor->setBackgroundColor(msg->bgColor); - else bgColor->setBackgroundColor(QColor(dbcFile->findAttributeByName("GenMsgBackgroundColor")->defaultValue.toString())); + if (msg->bgColor.isValid()) bgColor->setBackground(msg->bgColor); + else bgColor->setBackground(QColor(dbcFile->findAttributeByName("GenMsgBackgroundColor")->defaultValue.toString())); QTableWidgetItem *msgComment = new QTableWidgetItem(msg->comment); @@ -558,8 +558,8 @@ void DBCMainEditor::insertBlankRow() for(int i=1 ; i < ui->MessagesTable->columnCount(); i++) { QTableWidgetItem *item = new QTableWidgetItem(""); - if (i == 4) item->setBackgroundColor(QApplication::palette().color(QPalette::WindowText)); //foreground color - if (i == 5) item->setBackgroundColor(QApplication::palette().color(QPalette::Base)); + if (i == 4) item->setBackground(QApplication::palette().color(QPalette::WindowText)); //foreground color + if (i == 5) item->setBackground(QApplication::palette().color(QPalette::Base)); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->MessagesTable->setItem(rowIdx, i, item); } diff --git a/jsedit.cpp b/jsedit.cpp index 96c64cb..aff5f81 100644 --- a/jsedit.cpp +++ b/jsedit.cpp @@ -997,11 +997,11 @@ void JSEdit::updateSidebar() int maxLines = blockCount(); for (int number = 10; number < maxLines; number *= 10) ++digits; - sw += fontMetrics().width('w') * digits; + sw += fontMetrics().horizontalAdvance('w') * digits; } if (d->codeFolding) { int fh = fontMetrics().lineSpacing(); - int fw = fontMetrics().width('w'); + int fw = fontMetrics().horizontalAdvance('w'); d->sidebar->foldIndicatorWidth = qMax(fw, fh); sw += d->sidebar->foldIndicatorWidth; } diff --git a/mainwindow.cpp b/mainwindow.cpp index 99bab53..08a096e 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -849,14 +849,14 @@ void MainWindow::showGraphingWindow() { if (!graphingWindow) { graphingWindow = new GraphingWindow(model->getListReference()); - connect(graphingWindow, SIGNAL(sendCenterTimeID(int32_t,double)), this, SLOT(gotCenterTimeID(int32_t,double))); - connect(this, SIGNAL(sendCenterTimeID(int32_t,double)), graphingWindow, SLOT(gotCenterTimeID(int32_t,double))); + connect(graphingWindow, SIGNAL(sendCenterTimeID(uint32_t,double)), this, SLOT(gotCenterTimeID(int32_t,double))); + connect(this, SIGNAL(sendCenterTimeID(uint32_t,double)), graphingWindow, SLOT(gotCenterTimeID(int32_t,double))); } if (flowViewWindow) //connect the two external windows together { - connect(graphingWindow, SIGNAL(sendCenterTimeID(int32_t,double)), flowViewWindow, SLOT(gotCenterTimeID(int32_t,double))); - connect(flowViewWindow, SIGNAL(sendCenterTimeID(int32_t,double)), graphingWindow, SLOT(gotCenterTimeID(int32_t,double))); + connect(graphingWindow, SIGNAL(sendCenterTimeID(uint32_t,double)), flowViewWindow, SLOT(gotCenterTimeID(int32_t,double))); + connect(flowViewWindow, SIGNAL(sendCenterTimeID(uint32_t,double)), graphingWindow, SLOT(gotCenterTimeID(int32_t,double))); } graphingWindow->show(); } @@ -1026,14 +1026,14 @@ void MainWindow::showFlowViewWindow() flowViewWindow = new FlowViewWindow(model->getListReference()); else flowViewWindow = new FlowViewWindow(model->getFilteredListReference()); - connect(flowViewWindow, SIGNAL(sendCenterTimeID(int32_t,double)), this, SLOT(gotCenterTimeID(int32_t,double))); - connect(this, SIGNAL(sendCenterTimeID(int32_t,double)), flowViewWindow, SLOT(gotCenterTimeID(int32_t,double))); + connect(flowViewWindow, SIGNAL(sendCenterTimeID(uint32_t,double)), this, SLOT(gotCenterTimeID(int32_t,double))); + connect(this, SIGNAL(sendCenterTimeID(uint32_t,double)), flowViewWindow, SLOT(gotCenterTimeID(int32_t,double))); } if (graphingWindow) { - connect(graphingWindow, SIGNAL(sendCenterTimeID(int32_t,double)), flowViewWindow, SLOT(gotCenterTimeID(int32_t,double))); - connect(flowViewWindow, SIGNAL(sendCenterTimeID(int32_t,double)), graphingWindow, SLOT(gotCenterTimeID(int32_t,double))); + connect(graphingWindow, SIGNAL(sendCenterTimeID(uint32_t,double)), flowViewWindow, SLOT(gotCenterTimeID(int32_t,double))); + connect(flowViewWindow, SIGNAL(sendCenterTimeID(uint32_t,double)), graphingWindow, SLOT(gotCenterTimeID(int32_t,double))); } flowViewWindow->show(); diff --git a/mainwindow.h b/mainwindow.h index eec38a8..e780f88 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -115,7 +115,7 @@ signals: void framesUpdated(int numFrames); //something has updated the frame list (send at gui update frequency) void frameUpdateRapid(int numFrames); void settingsUpdated(); - void sendCenterTimeID(int32_t ID, double timestamp); + void sendCenterTimeID(uint32_t ID, double timestamp); private: Ui::MainWindow *ui; diff --git a/motorcontrollerconfigwindow.cpp b/motorcontrollerconfigwindow.cpp index 1fe9dae..15b8d0d 100644 --- a/motorcontrollerconfigwindow.cpp +++ b/motorcontrollerconfigwindow.cpp @@ -51,9 +51,8 @@ MotorControllerConfigWindow::~MotorControllerConfigWindow() void MotorControllerConfigWindow::updatedFrames(int numFrames) { CANFrame thisFrame; - int id; - int param; - QTableWidgetItem *item; + uint32_t id; + QTableWidgetItem *item = nullptr; if (numFrames == -1) //all frames deleted { @@ -74,12 +73,12 @@ void MotorControllerConfigWindow::updatedFrames(int numFrames) { if ((char)thisFrame.payload()[2] == 0) { - int paramID = thisFrame.payload()[0] + (thisFrame.payload()[1] * 256); + uint32_t paramID = static_cast(thisFrame.payload()[0] + (thisFrame.payload()[1] * 256)); for (int i = 0; i < params.length(); i++) { if (params[i].paramID == paramID) { - params[i].value = thisFrame.payload()[4] + (thisFrame.payload()[5] * 256); + params[i].value = static_cast(thisFrame.payload()[4] + (thisFrame.payload()[5] * 256)); if (params[i].paramType == ASCII) item = new QTableWidgetItem(); //QString::fromUtf8((char *)params[i].value, 2)); if (params[i].paramType == HEX) item = new QTableWidgetItem(Utility::formatHexNum(params[i].value)); if (params[i].paramType == DEC) diff --git a/re/filecomparatorwindow.cpp b/re/filecomparatorwindow.cpp index 8525470..a159146 100644 --- a/re/filecomparatorwindow.cpp +++ b/re/filecomparatorwindow.cpp @@ -36,7 +36,7 @@ void FileComparatorWindow::showEvent(QShowEvent *) void FileComparatorWindow::closeEvent(QCloseEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event) writeSettings(); } @@ -118,8 +118,8 @@ void FileComparatorWindow::clearReference() void FileComparatorWindow::calculateDetails() { - QMap interestedIDs; - QMap referenceIDs; + QMap interestedIDs; + QMap referenceIDs; QTreeWidgetItem *interestedOnlyBase, *referenceOnlyBase = nullptr, *sharedBase, *bitmapBaseInterested, *bitmapBaseReference = nullptr; QTreeWidgetItem *valuesBase, *detail, *sharedItem, *valuesInterested, *valuesReference = nullptr; uint64_t tmp; @@ -131,7 +131,7 @@ void FileComparatorWindow::calculateDetails() QProgressDialog progress(this); progress.setWindowModality(Qt::WindowModal); progress.setLabelText("Calculating differences"); - progress.setCancelButton(0); + progress.setCancelButton(nullptr); progress.setRange(0,0); progress.setMinimumDuration(0); progress.show(); @@ -159,7 +159,7 @@ void FileComparatorWindow::calculateDetails() if (interestedIDs.contains(frame.frameId())) //if we saw this ID before then add to the QList in there { - for (unsigned int y = 0; y < dataLen; y++) + for (int y = 0; y < dataLen; y++) { interestedIDs[frame.frameId()].values[y][data[y]]++; tmp = data[y]; @@ -184,7 +184,7 @@ void FileComparatorWindow::calculateDetails() } } //memset(newData->values, 0, 256 * 8); - for (unsigned int y = 0; y < dataLen; y++) + for (int y = 0; y < dataLen; y++) { newData->values[y][data[y]] = 1; tmp = data[y]; @@ -206,7 +206,7 @@ void FileComparatorWindow::calculateDetails() if (referenceIDs.contains(frame.frameId())) //if we saw this ID before then add to the QList in there { - for (unsigned int y = 0; y < dataLen; y++) + for (int y = 0; y < dataLen; y++) { referenceIDs[frame.frameId()].values[y][data[y]]++; tmp = data[y]; @@ -229,7 +229,7 @@ void FileComparatorWindow::calculateDetails() } } //memset(newData->values, 0, 256 * 8); - for (unsigned int y = 0; y < dataLen; y++) + for (int y = 0; y < dataLen; y++) { newData->values[y][data[y]] = 1; tmp = data[y]; @@ -246,7 +246,7 @@ void FileComparatorWindow::calculateDetails() //now we iterate through the IDs within both files and see which are unique to one file and which //are shared bool interestedHadUnique = false; - QMap::iterator i; + QMap::iterator i; int framesCounter = 0; for (i = interestedIDs.begin(); i != interestedIDs.end(); ++i) { @@ -257,7 +257,7 @@ void FileComparatorWindow::calculateDetails() qApp->processEvents(); } - int keyone = i.key(); + uint32_t keyone = i.key(); if (!referenceIDs.contains(keyone)) { valuesBase = new QTreeWidgetItem(); @@ -324,7 +324,7 @@ void FileComparatorWindow::calculateDetails() for (int j = 0; j < 256; j++) { detail = new QTreeWidgetItem(); - detail->setText(0, Utility::formatHexNum(j)); + detail->setText(0, Utility::formatHexNum(static_cast(j))); if ((interested.values[i][j] > 0) && (reference.values[i][j] == 0) ) { valuesInterested->addChild(detail); @@ -344,10 +344,10 @@ void FileComparatorWindow::calculateDetails() if (!uniqueInterested) { - QMap::iterator itwo; + QMap::iterator itwo; for (itwo = referenceIDs.begin(); itwo != referenceIDs.end(); ++itwo) { - int keytwo = itwo.key(); + unsigned int keytwo = itwo.key(); if (!interestedIDs.contains(keytwo)) { valuesBase = new QTreeWidgetItem(); diff --git a/re/filecomparatorwindow.h b/re/filecomparatorwindow.h index d348255..61cef8c 100644 --- a/re/filecomparatorwindow.h +++ b/re/filecomparatorwindow.h @@ -14,7 +14,7 @@ class FileComparatorWindow; struct FrameData { - int ID; + uint32_t ID; int dataLen; uint64_t bitmap; int values[8][256]; //first index is the data byte, second is # of times we saw that value diff --git a/re/flowviewwindow.cpp b/re/flowviewwindow.cpp index 3cab711..9d8f3de 100644 --- a/re/flowviewwindow.cpp +++ b/re/flowviewwindow.cpp @@ -501,9 +501,8 @@ void FlowViewWindow::removeAllGraphs() void FlowViewWindow::createGraph(int byteNum) { int tempVal; - float minval=1000000, maxval = -100000; + double minval = 1000000.0, maxval = -100000.0; const unsigned char *data; - int dataLen = 0; const CANFrame *frame; qDebug() << "Create Graph " << byteNum; @@ -521,14 +520,13 @@ void FlowViewWindow::createGraph(int byteNum) { frame = &frameCache[j]; data = reinterpret_cast(frame->payload().constData()); - dataLen = frame->payload().length(); tempVal = data[byteNum]; if (graphByTime) { if (secondsMode){ - x[byteNum][j] = (double)(frame->timeStamp().microSeconds()) / 1000000.0; + x[byteNum][j] = frame->timeStamp().microSeconds() / 1000000.0; } else { diff --git a/re/flowviewwindow.h b/re/flowviewwindow.h index a18e5f3..258c614 100644 --- a/re/flowviewwindow.h +++ b/re/flowviewwindow.h @@ -40,7 +40,7 @@ private slots: void gotCellClick(int x, int y); signals: - void sendCenterTimeID(int32_t ID, double timestamp); + void sendCenterTimeID(uint32_t ID, double timestamp); private: Ui::FlowViewWindow *ui; diff --git a/re/frameinfowindow.cpp b/re/frameinfowindow.cpp index fa5e8a9..656549a 100644 --- a/re/frameinfowindow.cpp +++ b/re/frameinfowindow.cpp @@ -244,10 +244,10 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) { int targettedID; int minLen, maxLen, thisLen; - uint64_t avgInterval; - uint64_t minInterval; - uint64_t maxInterval; - uint64_t thisInterval; + int64_t avgInterval; + int64_t minInterval; + int64_t maxInterval; + int64_t thisInterval; int minData[8]; int maxData[8]; int dataHistogram[256][8]; @@ -377,8 +377,8 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) //qDebug() << referenceBits[c]; } - std::vector sortedIntervals; - uint64_t intervalSum = 0; + std::vector sortedIntervals; + int64_t intervalSum = 0; //then find all data points for (int j = 0; j < frameCache.count(); j++) @@ -387,7 +387,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) dataLen = frameCache.at(j).payload().length(); byteGraphX.append(j); - for (uint32_t bytcnt = 0; bytcnt < dataLen; bytcnt++) + for (int bytcnt = 0; bytcnt < dataLen; bytcnt++) { byteGraphY[bytcnt].append(data[bytcnt]); } @@ -429,7 +429,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) } std::sort(sortedIntervals.begin(), sortedIntervals.end()); - uint64_t intervalStdDiv = 0, intervalPctl5 = 0, intervalPctl95 = 0, intervalMean = 0, intervalVariance = 0; + int64_t intervalStdDiv = 0, intervalPctl5 = 0, intervalPctl95 = 0, intervalMean = 0, intervalVariance = 0; int maxTimeCounter = -1; if (sortedIntervals.size() > 0) @@ -441,17 +441,17 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) } intervalVariance /= sortedIntervals.size(); - intervalStdDiv = sqrt(intervalVariance); + intervalStdDiv = static_cast(sqrt(intervalVariance)); - intervalPctl5 = sortedIntervals[floor(0.05 * sortedIntervals.size())]; - intervalPctl95 = sortedIntervals[floor(0.95 * sortedIntervals.size())]; + intervalPctl5 = sortedIntervals[static_cast(floor(0.05 * sortedIntervals.size()))]; + intervalPctl95 = sortedIntervals[static_cast(floor(0.95 * sortedIntervals.size()))]; - uint64_t step = ceil((maxInterval - minInterval) / numIntervalHistBars); + uint64_t step = static_cast(ceil((maxInterval - minInterval) / numIntervalHistBars)); qDebug() << "Step: " << step << " minInt: " << minInterval << " maxInt: " << maxInterval; - int index = 0; + unsigned int index = 0; int counter = 0; for(int l = 0; l <= numIntervalHistBars; l++) { - uint64_t currentMax = maxInterval - ((numIntervalHistBars - l) * step); // avoid missing the biggest value due to rounding errors + int64_t currentMax = maxInterval - ((numIntervalHistBars - l) * step); // avoid missing the biggest value due to rounding errors qDebug() << "CurrentMax: " << currentMax; while(index < sortedIntervals.size()) { if(sortedIntervals[index] <= currentMax) { @@ -483,22 +483,22 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) baseNode->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr("Average inter-frame interval: ") + QString::number(avgInterval / 1000.0f) + "ms"); + tempItem->setText(0, tr("Average inter-frame interval: ") + QString::number(avgInterval / 1000.0) + "ms"); baseNode->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr("Minimum inter-frame interval: ") + QString::number(minInterval / 1000.0f) + "ms"); + tempItem->setText(0, tr("Minimum inter-frame interval: ") + QString::number(minInterval / 1000.0) + "ms"); baseNode->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr("Maximum inter-frame interval: ") + QString::number(maxInterval / 1000.0f) + "ms"); + tempItem->setText(0, tr("Maximum inter-frame interval: ") + QString::number(maxInterval / 1000.0) + "ms"); baseNode->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr("Inter-frame interval variation: ") + QString::number((maxInterval - minInterval) / 1000.0f) + "ms"); + tempItem->setText(0, tr("Inter-frame interval variation: ") + QString::number((maxInterval - minInterval) / 1000.0) + "ms"); baseNode->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr("Interval standard deviation: ") + QString::number(intervalStdDiv / 1000.0f) + "ms"); + tempItem->setText(0, tr("Interval standard deviation: ") + QString::number(intervalStdDiv / 1000.0) + "ms"); baseNode->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr("Minimum range to fit 90% of inter-frame intervals: ") + QString::number((intervalPctl95 - intervalPctl5) / 1000.0f) + "ms"); + tempItem->setText(0, tr("Minimum range to fit 90% of inter-frame intervals: ") + QString::number((intervalPctl95 - intervalPctl5) / 1000.0) + "ms"); baseNode->addChild(tempItem); for (int c = 0; c < maxLen; c++) { @@ -515,7 +515,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) dataBase->addChild(tempItem); tempItem = new QTreeWidgetItem(); - tempItem->setText(0, tr("Range: ") + Utility::formatNumber((char)minData[c]) + tr(" to ") + Utility::formatNumber((char)maxData[c])); + tempItem->setText(0, tr("Range: ") + Utility::formatNumber((unsigned int)minData[c]) + tr(" to ") + Utility::formatNumber((unsigned int)maxData[c])); dataBase->addChild(tempItem); histBase->setText(0, tr("Histogram")); dataBase->addChild(histBase); @@ -525,7 +525,7 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) if (dataHistogram[d][c] > 0) { tempItem = new QTreeWidgetItem(); - tempItem->setText(0, QString::number(d) + "/0x" + QString::number(d, 16) +" (" + Utility::formatByteAsBinary(d) +") -> " + QString::number(dataHistogram[d][c])); + tempItem->setText(0, QString::number(d) + "/0x" + QString::number(d, 16) +" (" + Utility::formatByteAsBinary(static_cast(d)) +") -> " + QString::number(dataHistogram[d][c])); histBase->addChild(tempItem); } } diff --git a/re/graphingwindow.h b/re/graphingwindow.h index 0d4781e..20d35c1 100644 --- a/re/graphingwindow.h +++ b/re/graphingwindow.h @@ -71,7 +71,7 @@ private slots: void zoomOut(); signals: - void sendCenterTimeID(int32_t ID, double timestamp); + void sendCenterTimeID(uint32_t ID, double timestamp); private: Ui::GraphingWindow *ui; From 909710fcfa46c79dc838ceb91b142a4e6d8a8603 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sat, 15 Feb 2020 21:25:32 -0500 Subject: [PATCH 09/58] Add text boxes to Fuzzing window to make it easy to set the bytes directly and to show feedback while fuzzing is running. --- connections/serialbusconnection.cpp | 2 - docs/source/fuzzingwindow.rst | 10 +- docs/source/images/FuzzingWindow.png | Bin 74851 -> 66447 bytes help/_images/FuzzingWindow.png | Bin 74851 -> 66447 bytes help/_static/jquery.js | 5 +- help/fuzzingwindow.html | 10 +- re/fuzzingwindow.cpp | 45 ++++++++ re/fuzzingwindow.h | 1 + ui/fuzzingwindow.ui | 147 ++++++++++++++++++++++++++- 9 files changed, 207 insertions(+), 13 deletions(-) diff --git a/connections/serialbusconnection.cpp b/connections/serialbusconnection.cpp index bda07f8..f628cb6 100644 --- a/connections/serialbusconnection.cpp +++ b/connections/serialbusconnection.cpp @@ -119,8 +119,6 @@ bool SerialBusConnection::piSendFrame(const CANFrame& pFrame) return false; if (!mDev_p) return false; - qDebug() << "Data bytes at lowlevel: " << pFrame.payload().toBase64(); - return mDev_p->writeFrame(pFrame); } diff --git a/docs/source/fuzzingwindow.rst b/docs/source/fuzzingwindow.rst index 36bdf48..a2a3a6f 100644 --- a/docs/source/fuzzingwindow.rst +++ b/docs/source/fuzzingwindow.rst @@ -13,12 +13,13 @@ Some people are big fans of fuzzing, some people have no use for it. There usual Fuzzing is Dangerous ==================== -You have now been warned. Sending random garbage over the CAN bus to see what happens could mess something up. It just might put your vehicle into gear and cause you to drive over a box full of kittens. Be careful! Never fuzz a car unless you're right there and you can stop it. Even better, don't do it unless your car is up on a hoist or something and can't drive over anyone. Even still, there is a small chance you could cause an adverse effect to your car. +You have now been warned. Sending random garbage over the CAN bus to see what happens could mess something up. It just might put your vehicle into gear and cause you to drive over a box full of kittens. Be careful! Never fuzz a car unless you're right there and you can stop it. Even better, don't do it unless your car is up on a hoist or jacks and can't drive over anyone. Even still, there is a small chance you could cause an adverse effect to your car. If you break something you own the pieces. Controlling the Fuzzy Beast =========================== -So, you want to give it a try? Let's do it! First of all, you can set the delay between frames and the burst rate. The burst rate can cause the program to send one than one frame each interval. This is useful as a CAN bus could potentially support 2000 to 8000 frames per second. Then you can set the number of bytes to send. Ordinarily this would be the full 8 but you can experiment with smaller frames. You can set to send on a specific bus. That's all the simple settings. It gets a bit more complicated now. +So, you want to give it a try? Let's do it! First of all, you can set the delay between frames and the burst rate. The delay is in milliseconds but can be set as low as 0. If the delay is set to 0 then the system will attempt to send frames as fast as it can. However, even then you may not get quite as many frames per second as you'd like. Even at 0 it will still be scheduled by your operating system and so might not quite get to the speed you want. The burst rate can cause the program to send one than one frame each interval. This is useful as a CAN bus could potentially support 2000 to 8000 frames per second. If you need rapid frame sending your best bet is to set the sending interval to 1-2ms and then adjust the burst +rate until you get your desired sending rate. Then you can set the number of bytes to send. Ordinarily this would be the full 8 but you can experiment with smaller frames. You can set to send on a specific bus. That's all the simple settings. It gets a bit more complicated now. The "ID Scanning" box has two radio buttons: @@ -36,9 +37,10 @@ The last box is "Bit Scanning" 2. Sweep causes the system to set the first one, then unset that one and set the second bit, then unset that, etc. Thus the fuzzed bit sweeps and only one fuzzed bit is set at once. 3. Random will randomly pick whether each fuzzed bit is set or not. -In order to fuzz bits you need to set which bits to fuzz and which not to. As listed at the bottom of the window, there is a color code to the 8x8 grid. Clicking cells in the grid will toggle them between their various values. White bits are never set, black bits are always set no matter what, green bits follow the fuzzing pattern you specified in "Bit Scanning" +In order to fuzz bits you need to set which bits to fuzz and which not to. As listed at the bottom of the window, there is a color code to the 8x8 grid. Clicking cells in the grid will toggle them between their various values. White bits are never set, black bits are always set no matter what, green bits follow the fuzzing pattern you specified in "Bit Scanning" You can also set the bytes directly with the text boxes above the 8x8 grid. Setting a hexadecimal value in these +boxes will set the relavent bits in the 8x8 grid. You must press the ENTER/RETURN key to set the values. Merely changing the value will not update it (as a safety measure). Pulling the Trigger =================== -Once you've configured everything click "Start Fuzzing" to give it a shot. You will see the number of frames sent so far listed below the button. You can stop the fuzzing by pushing the button again. +Once you've configured everything click "Start Fuzzing" to give it a shot. You will see the number of frames sent so far listed below the button. Approximately four times per second the current value for each byte is copied into the text boxes just above the 8x8 grid. This can be used to see what is going on and to ensure that it is working the way you want it to. You can stop the fuzzing by pushing the start button again. diff --git a/docs/source/images/FuzzingWindow.png b/docs/source/images/FuzzingWindow.png index 6a4950b1382f962ca9c67179b99fac2846ed776a..e863312991b632d02466d4188a0bcf5ade545ac2 100644 GIT binary patch literal 66447 zcma&O1ymeC)bH5@3BjEp!QGt@9D;j-YjAgGa19>Z-Ccq^!QCAO3-0c)lTUWfzW2Se zd(O=1?&_Lb)zwwE>;CHB6RIdLfsBBU00028l;qcM0PyY^^6iFy2T3UdZ1$k-*;&5u<5T<_%0wxcur#KPNKHf*2XqYfT)A9zLT*L ziL1Gj8Ht3HoT8c^3N8SU08(FtmED$)*IczR_lV!0pXs=KB-KAFA=waG#asnhmd&?I z=+vKFEVMuo{c7LhzwO&;5cdsdZ$h zc`ws5bC2o61p`-Nu32g!6adhyeM&8)K>(!Ra%4=i%~Nw}TRdIX$P0!G_um+^RL+#v ze6UYdGoVNV1%8dDYXj8{ECabh`<)_aUvIwaDpYYm3!}BrKKhIlCh?bQK?$Q#2q#WR zp;2gfsNJDLj-=S?HKp^WBq?I2n?#j%4vGx&X~Tsd?IpO;ugRjkEoigw_AS`u3W}AB zt^*F>Nv~Bg%>Bxnk}0Gt5wDKZ2^r7=1)SpD7s|iu4AZ{6FHOl2nJh^*xi+Pe`j+IU zJ0bUrLgNdx6x&tb7Vlkvu89#g_K5u+q_%3C6ByNKXr+4}Z!3u=w&IlOI1^z-;2~!W zN9(Q`x~{%L_R$`XMWf4`KFUC@4^1S?XTMuP=HFhiZeS+2Cwv%%lnkPyD7olX1+FRd zB!>^1CC7k5`uN~*|BD}*2z*X-cmJ@%V=pVjfXb8Ead_aTCzLrX zv5%4XPqTHpmX`Dr;-~fNC~#6#MftmX;r2-+|532z?4;so-BIno-8@j{UO6S$^ zz_!C__@2DoNkbEh!%Z*%^gcX9`v>S17P5WJkEyA7U1F+GC3qu?8rc55WCi>Lp=Tv! zMT21fT?=tyWc^MlB_h)OGxtQlNsVUxxjPm`alAl$0(Q+(L2v+!x-)hr|!>6?X5L=}PWOFN3%?_GguW zp8C#9)y&dxuFZTqKFR#7EjZYQ2}XEu2VqS|0;GlMC#0#E-Ad2G<=|{TJT!J?J>R>y zNK-YRH|S_8Yw}yY#JtL`i2Qnq1m0k3HHB+l6+puzNXkqxwl-HX&#WG#srhQ5Zv&}% zI^przs*n1?BT+5a#MjurUEQJ#j10B4wKm!vU4s$3QkYAZ$Hx(Bq0E`C`;+Lda3Wa( zyaY!(hsqb^i;a7r%E{@~(UqBPuldP5nLSeI$N04jZpYz9$HJtwlihXmeP01! zR4L61-Sh7iu=N1cCg*=^bI4yVk~&9J6iO{6yBTg zai(8S<#3u=%jPdQJ5()Cn%;dzV*ZBs(g-Wc#-g252m=S_)STw-(`O|mZMU~u=Qk9_ z3S<@OcCmBzb9k>*vs5Ofk`Arko1zsg9AtDHWYRte0o^}plBJEM3V0qob(id-I*O(y zf{_o4EICGH#mI6LD^yoBwc3XVVSqF7(-XS-v2S!T zPGi5kwR-n1{vyE6Lj{6`$?xE-aOl|{HR_|eERR)kzxDO^Eihi(b7}GU-~i=M&X$s9 zU#)S>7h1%Y-p10nUR_ShBefd5cwJS~5<6OY^IDH)dRwg;8Y(==a}DSqBkIM58W|8O zRtb(-K>C)+as0U@W1fSC<=jj$wtA^-??P()G*av~l19^{Sf0*=XA%a{?3q+CP+)EL z`|DkO-<4@!Z%32cP4MZd9&cm$v$^5nh#IKjH8<^#iQ_lDLa#;>(K*@--nx_Nl9$r@ z&g8U|O5FyQ4RC+}e=FO^(YEJj=d{M%zq?REX^CS+a)i*pwEv%tUyAfH;fyPH@%$j8 z>)$*x)S~a- z5wNF>ik~7kY0Dcak1}`KE?Cg|lXN^fm`yNw568yd_Zq@PR>L=0{3GHN`~@yAgNL9h5cd9M}r{#7n#fKS-0RuA8u zc(h(Q{WW<`x723}E~h=>wyl`a@}bwua%V3pUunYzcE?h`ourlx=^_*CuUpSdH!|D- zzTN;Gah1KTx_M9Xb((jeKOAsdrXJTC;wVuL9KrE}(kLk@jEs$Snq7M4!8s+-sNFwG zg$7kaH7HZ)Dxm?n?J7-@Yjcy#=}zo404}bZWQ3|#oxz{PESq}l6%P1pb-P<%R<(vw z?1CVL!GIG7hYw%fP{+c>mCE8MAdD8iH-^93c^BpNK8VWyUqvCk=6=S^QYQRHu>5C) zcVQcQ24XcOjrYbLk2A@pdPsPZY?6?{QCMJWv0R2DAU8A3uB}5zn<_6W)hCWb z>b0pf)oHWB=`%m8*LX6mK;6=Nb=07PKE>)R!}q%|zM^!QQ*A`q=$m;M(_Q)74J!VR zq30@kN}@Hg1QgS63!h3M4Fo(dz87MPzm4=G2^3Kp7|*D%w)*z%TbNOhv858L+=Ft9 zX2m@6^#-!7+Y^kf5Oh)xkN1zH(}v?To|Yc%x9Xs(vay<&Fb8BT~ZP?gn~=234d^pNGQ zX{SdbA-D4r@JEIg(6mfr8t?7$dtyn?kkHdbg)7YRD?$2_Z&!Ak=Lfl-xbvcx--aB> zpOzasFh9m8WXR0?N>0bez@N&oKFW`;!%zuL3uh1Mxv-i&|GOl^=#@6b!Et*X5fMh~ zBi^=&Y+Gd;of&8gZn1nVFJVl3+AplLI%S8n0zp+Rky|IIJzz2 z#(Zb{!!-^v00WgTE>(<$6`mFs7k7JEU>lCJP^ldn=D9q%wpP^4EEP*pj2Ea{HaCqG zRMd#}Xf)svPe~bP!Zf)yWl>+CBv~a!>PyqQlIuQG2+q7pDyIY09b&!AlH8NX-#-PeK% zvKYw`Dhm$$SPH+wpm*E%ffVi3vHM)3vJ|q?@{DPjNlCUQbh)&2TjsKu3aH<6q&y8S zwholOBEIWe>Bil=L zO0C9@xZcn&>iUKAJZpOA`!#*SPQPSQUUgO;T&unwiI=DrH9DaV5;U+yAhSfZrOy>V zMV6^8H=J!8Z(Y(cGE!0PE+^18xVWgQ&Qq1D)2g$5j3^D*gU?ae{nxl+mk6D#vPxJ| ziJ9Q7izY%8EnI(?{{G*E5qfVc4F*Bl+@^E1E#kRW+UjRn({BYqLGKK=Yq|VvyQ)0k z_~^LiN0n!Dtjnm;V_HHo10}dFZmsrrF9*ikotC7Ur%x8VRQ)SwW8!dd5MacOXONMQ zhPj0DxjU>a=tf6yXM{o|#GYKuOpT4vu`ct!R)l%rtdxp5MhDxZHJ<#zLCQ(x^T23u zKZ-J5ABFX;Pix>SmDz#<5;SkFb_K~Y#noDOv$$U>zm=`q0sVYFIm8~rmD=;L8PW@! zow^j2hX)n-2bVF^HKF-4YW+%T(o9wt)od;<4ir+gwr@}_E-tX&qHC2jwX4KWTMk}8&dibM9Q>w_`A3as0-)DitMStBx>+b?Z6-PbIp$v& z*oMdYn*GWUjCdDw7YJvcyxhLbmou z$G3eHZ3ZPble4M7>uP3cWh6OM2ZC0^rGCF=VTgd#Ie zZbr(`+_-1rM8c#BW~tuS6rMs^M0grY2B9$YA4F`?o?GmwM^<9l4_8{EgfqNJn*UIgEJ$Ek2bbyB8h5-H)b`;V zbO%I+t0=)zQn~c=p%|ro^_SA#+_k}_V}iq3-LWKD=jQQ5$j)myc!#eQKOCk zM2nJ}$P>BVZmC#xpm{vGO>9@c{dFgY%*H{le`fMXfohHNo%L~Ba9zzSlC|bzrFxz3 zm_Y){Rd2tTIDeUwB4&F?RO!J7d`<^qE;lrQ1}ls_1hw8`ZF$t!Fn4@2z`t9+WXZp4 zaWomXgaOC#9+x~aA_Ct3v-_@8pIBAp(E>xe)HN0F=YHn& z_lvK%oV`g_`S%$(G_RKG@l6=9AR!?mARyRFkYD?Z&G&oEq^iKdJvWYPC~LRb^`Rc% zr6gztaBJ5~eCLpxff24&FVAcT&1i<1uo(x7@B|DQv8KqHTydSsX(>@iIW%iS9?PkP zwD`=>U-#dl$`x;8=8w0IljGCrVkBYTZAYDGLMDEv+nneY37Qq()G{ik%n}PsStb2aI@h`7WDP?S#hP2bd8Q`tEv}2DSkmK zSfFJ~mc^06fSKed_*SA!r{bp^wmukvM#m01q#&Rl5lI>SN&*k>=|;aHz=aN<KdjQ3Xg)Akgp2g+k@p-LQ#zPY*nkHLkPWE#0C&9HabAN)!D zU@+iGh6O$;%sAP{@bU77{J}vc%@66(CpdqY_7@4ut>!=zIg2LwX%gA~rOxtXZE$T? z83uTG*xFxg>I#V_%QXN`{w9Nz@GcznU!ar4j}Z?1+xh4LiN8n#Ss-2jvZKE*{4XB- zGa2wN_1c!4;yy#?UrOnFyZrb6U-<)b{v!*#DC$3xXu(APOp-!^p#R6@|0zpCN&W#X zvtt-XljwhxY|nDU;c7FP(<9uBg%&IqB7bmru8)D4;O^cxT0yHSFLD=iwCBw6Y*v*p zjKX(6LC#u`^#CD#%d+x-`(^$l84V+WDS-_ruM^hAb3o7YNaodk!MGrObB0DJ-r zXD!7zkE=<{D}B*ad~|h7^nWoZk;hlCw{GLEb!_D8%tA|HCur+Oi|^uG2`WXCu@2( zkG<+5Ie`Wjh4FmoU}sb&Szfi>RI!zlH_h3w=s6rO@8Y%%@(|*`lQ2@X_hg`nhS!r- z7-BbyRO#0CyRU&r@UiAU4rlWn8|P6xkH3;AXR#zQXlOn)OGS}lyB^@=Ez~i&P$Zyx zQbu77iB##fcnE$;GTbxh{@(6UZF}^(p&dIcqmtaRf{*TGKO7Kl%CsImPEnCwfs7}F z+Yk08;lwope{tFMQ#MQWc{^^Fq>cVgL21Q!Ni5*`qUh(0DWPiO)^$Swc@BhOUL9+X zy2B{qb*pW`V-DrXB-!xGPiZ!3ns3>p^EmQ{xZ9;|-E6 zw4I!R5}aGgs3SZ5tcEZ53Gw#V*2w>`c3KYb2B4*Rf3+RxL;P- zzn^9Ddr_XJB%f^o znFaqX9zd;5*H#;(*(`+g#SN=J4e*7@vw zVPgb0(?2)t3zHolOQg-`$g^QDTN-F?W2$d<*y~>V5K$_jfC_C@G-Vnx-gG%)%Npgi z%+10cxVjNQa9hZMKM78}iMpV=m<%&?+DlZ$Hi+ZeoK5w@iv-;-6PU4`ovl07WxEUN zA&S@25At_^Qa4?+MbUN-OErq0d@l2%S~9v+y0UrSTV}@U?Y2X zPoi+B?4Qf5piRMpcM`mpLxl(QOs3z3WQd5D8Xj7WK?XE_K|&dEv_+D!Lz|aPvbldm zwG{Q1kBruH=W_rF zUd}spH>h)5@`cSsIOEF?$`=j0tmHq|b3&T26!D-y^Gw>eYJ|xM*mnyG83Kkw<1~M! zQ}Nm@Z)@WYuzaa5hNCdi*$~X!uTzjAH;_MVyrGVaykSDqfiB~nqWDQV>zN|P`gm%@ z(#6R$@f~<#k~gI=6d8%tYN0z5CYv()O}EkLq5-WSln1@WaJTLtoD2Ltb?IY1q26a( zpLCq!BnjMc-s^80#|C3V;U1aLK%o7R{OQCgKU03qa_$7?N{8`;K>HKYqeDOHRSQjz zh@OStFcJ@lzA~9fj!ZwPvJY!g$QVY?J67eH?fm}q(_U`4x%B1p{8anO+p6RK=$c!J z_~r4cAA9tl%1=*wi(?BT9VWBchU?zY!BHu#r&@v+W4iFAl+o!0PqJSAJ zz)5Mk!i)M{7FghMqDCgh0Ku0@{sUjT&p@Rp?w{W(O?haKoqe8F1+->dCVjK>guXU^ z^1FQ`!lV*x4{CsrweN=5*tXYFc7t^DQ-;Cf zd`ZSb_6^#5<_O8@TQdxhldW{p8EWL6(YqbS=ycD^i$JQ3ruq}>($x^=l<7I34*3eY zi2vwy=rsLoug$W3;$~bA%J+K$er%{oC8rfTFI9f;VaB zbIbRIs9odMpDYg6KU&VjN*fd~sphq>@$!u9H$$x^#v3TizE_@o-kqLaU-JdB+1}j} zqq{lXZBgpJ8b<%cSzoLW{mv}P=CT)o&HOxY=i-+hSnMWcS*cu@^MNX4V9!38Gcvps z8-Z~>SOQSw=}tN|UF2|S7;3t`8r`>zt~4WIRJNpz?b*9(Vu)4zdXoI&<#eU=qK)01 zR=7@FZ>U2o^SnVXa>IPhVx95Hsb>PI14<%KRn1d9Eisu4ah=t?*Bby-a*$f~lzzYk z)-)D!o&c(vOYS`v`I10*w{qUX)(0BrFVyj%HO zBmnRq+4Iw^$Z^o^AY1cTiUoi`zqsF&8o9(C2#7{-@A4LoNRX^g0`qNV9+2n>);{xv z+jdQra&r8>SS;{bcw;l!@1743k9_+xk)i;g^}ox@y%#|!V|-+onwA-Pd|z!A*_Leg zsl`H+m;Bu<=UDmhh7)f?VdGDLra6t0%gJ)3Su1V<7mDW*^g*GZ@l3GVIAO*Rc1rD4 z;8~qq&eM&#eW49jo&s3B&TRa;V8qkb@?xlrzt&yU8a+qc$M}eP+z+Mcx%otw(O46e z?15J5n)6<=KP}4Be{3`LR@?1ZORv2s=Nr^VPG;3xG{)wR-LyEab=%kW1iaS!VeS`O zm8X0dr3cz2!m1(O1{8LmB1Ap-*jcTcG&SMa1YOvWo@p8tO#lb*RV}NyXGt4VEt_?E z*n*jT*3K{P>%DpNe4zV#Jg!&7Ji7MVIgtMYB_$nBNRZlf_5`N>wChQVafODmlxrE6j}xhvp#U!J(W*<(51VLx5KUejWl8(&Ly)2W(>BbSyUzS@?h z03{@~74YjCp(KRkcm20c>9|>o?BK)^BYcv3mx|8xfV=3}+w3gh@e9iqfF>Zh>sv7>q{qpcAmYb9P-5EHq z6Tj1Kh@~&4Ld}sDus#OcE{Joc*XXZA3BIY%uKAaDTs0gnhGG`Ur+;=nU4C&y^tymy z7r7z5Hdkw{(0@;6&;P-H64h19Qs_X0!tPQoE0qy+}F zHys(=fs@ohza$X_o;B+oq@avRIA*>43F{m}n?53a74g=2Pg@)-VvkOWhBtSUj#3aS zl}~Af8H4_`J`S(h{Po)(Y(2^Pw&%419~C_Kto(fP=Cuhu^gfDOM*MA*+xmtalk@^Oe5ZfiR%3Vc24uu<8wHInD)I9|xTIHLA&s7Y7i7cDcw z;F;+Sl$hs}i1}Jdf^-8?jW1OsGySk$t^BZ+Y7nYChN+UCq>3vQCTU2T^^ggUAVNp6>ipD-3hj!xsTZ#Iw0O)aHV~6zrN7(W&%YTPZV5N*s8GZqQ z4oRw!iHU()QTVygl9Cc35<7e6zFZ;r7wW^?oxqEW3koO{6clwkcv3IdGcdsyr$H@K zL}X-2JS1FPwPQunz}FWPl{ImXFr!-ej4uWcWP#wws;aZK);o(sMRyQLADuK1Tp^ke zAFpm5a=0dn>hcFogfqvyh3ogu&W?Tv0_pXcQ!$tx646b9f{FgaVm%5rBpREPut=s` zO^~DjNU#uUjL<~hR6-CS!P>qdr`%(x>E7uYz+yd&HOdbmW%;5(>oWmpk3ZU3Kh>zW zIo{bog4+aX*HbVfPWTAXpKl`Y?cYO6uc=~wLelCT@0dcrmJ8~1&fLU^M)Pe>M+q`f znvYOZ=&m$)Y^DdT+qP!X3}gf;wjV@Jas2Fy&1k)oEcmwewM4na@s6lQJr{&!2&swS zv10q#0i*F{LA{LYszOr3O(u9D2nwjX?;k!`85{~9ST@dlFy58EH#I~>_v1y*ndoE| zhZg$g5L;z?C5=i+NzIAN<&%Um*G)fZSXgN-3_rJmf(<}cUUfjXSXj#YigFoz&GLKA zVCL(|ttW}I~Qs-A9p|9B$PztrK2ly+(9& zH)KfehFN)Q)thd?TKQ^q*$PcLXf&WsyaJ9ZB&ZcB=W&`q6g~$Pob#wC;I>a{eNs`Y zXRP!?=A9y5RW0MO6(pip7W6PwhlkwEX3?8&QPiFeLy5cxTz`aAR}LnWb~~*l3k}gW zIP@Hk8K$)k$36M33~AJdrw7-Iy8$v0_?N~{rKoR_qY-e1swf)Oc8Y?Tej*mLPem0K z%}oPnrKrf5lfcH=*?Xb&5Ac&ReBQ6G-W(gBAq&E5yvkBTLsCOh2}BO|F81~=udbq~ z<~$Zzx&u)xW?#(r=9DmM(iu58yVje9ioNS)fmd&DAbW%#vVnZunjsy5pI3Q&Tpn=x z)?>Ld_TO3nhg#h&dkDfT{T^~?Xp8};U^d>vAEd}X$_D`6881$*Bd@e4m~#i=()7-d z=Dul#ls8`)8QCWYLhcCJBuYlOnPnO9!wz>mMcbPB@P)va*$K&gA8c! ze|maqp3y~A&?zo<3B*Vo&typmJeMi*KKAYzeT zXUWJ)(l9XeqF{>@SAr*V{u9>ZJpSw@6Z+q<%YQqk|B>(f7mV4*MB!iFYD<4NC!xth zIpLL(N=lnk>SN*V-B9-z)91rKqmRrYs0p~9ZX=axC8@QvrHhWdl~m^I{E2N}>dm%p z;cq0|tiy_Usgbf;JhoGjP2{upSOL*4krQR-RHe|DE4WI z*dn~RS9D#+_W32kM7G=wUgbdX6VEeA2$f`PAMO&U;l_54>VBVmN_qLDotn%@=oeB{ zPKx0+igyJe@=t|4Aq0b}tTwcCnt6Qn<_9O@$rYJ<8v=3TEIqE*zQu6*6*a42<&OJuV7_f( zEHn)pvsZ=V-bhN6)j{|%ZluPsVI!_!;5mF{PhJldLS3vfM%^hC-O<(lsJNsDh7T}* zwMgf;elWHMbkZ-+x0YmFMdJ{Vj|s<`0>v?i-EObvlHw)f)ELbElF($SWL|EpmW=qZ zx)a7c4{_SZDP?^6m>tvmeyz#->NemfM`TJ|hOb7vljTA~j}q*0?6F#WWE!@NrafGu zZjQ3ohqkRfZl?Ah+;t1}%gc2krvB{VdC_MAm{msDMSIm58DooqUziVfQ?43CFv24~ z5q>3n=Nrq{>2A3@*wjI+I1%8srOL1hjYehOf-8Rb~ z9yA7Vc`a~=kK{6EjM;uE{T-lNsKmD-X!=;GW}^dgHs3ZD8^{7*KZ(YT>kxd@j|&j6 zbrx(gU)JCMLs|<>Fk!6x*;E+=OB?51FDV))C!bF=NmzGcQh?9Rmol;2RzJ=9ulb=mUt4d0i^t;xPWtPFN{d_GL8 ztjNYqQ3yH#z1nppxS%Ym!{~!t%$+2;2cOARjm&ocB>uXglmTCqvAO3>XK z=l7mb;I6PQuji}gTI6^QDB=eLKD@7wZG;J!o(JA=J=06#j&FM@1km9J2Ie`MGBJb# zSp&u~UlZ^kPv(Lo#V+zV1?$g*rSvE&i-H7E{LFMQ=4Spwn-}hcR8Y#da|tu^g32qO zQ+6{oKU?m7hc(MQNv}CJ@M|s$hm9e^X3nYc&h-egUavLH0(-%E@oa~<>%{BlFxLwi z2dB4VtCjQO6t7mxEEph|0sn7)LaX8)@wYEy8`J>NoIDCk$Jj`zOt)JqPzGHzMjfv@ zGb(-8cM)A^X!p1~5WFvYuq=FR^T0@v@%yp^E_tFYFfA{+-k5R4hzEe->t9zPE>`Q! zXZQ8hNI{iSL*XsAcc&SksP`0uPAf`14N9@5dnoDdSS}}vf&(`F*fVoBrtWuahf-m8 z#XRIKb!W5IUCB$BUk>~~s+@1l*R1in5I4Wdmd_#o64UnnMJDI%b~<_b$tS}nwuJ6% z4K{PN)o>xFjP+YsIhJwp^;PF5Ri9?_nRHT}m3o`??eL(^m;IzB=K&o&WM%R59csJ> zd#Iwf<(i}O1hX0(daBZeocDE_CVBSjw<9aR4-9Zr(&F>$4u0Fs|AY#l^1c!aa8e0} zMU+7P$YL@Tn5YrMih&Q9)xUK0U5TUY^BRCthi;(;YwzD&ZTE%6S#iP}Gb!4& z$#JVP9uBY$E}aXzaU6))Kh?i~WGTF|eRZC)+M0lv+~>C1O$}oF&j%OO_AXsnQ+rX0vcAUuaET7#q2}NF8b~&B5>{9z}dS~BXW1=jmRNHj^wZYU- z3!{#9!Icm(Au%{J!uLhO|7xGx8MXL%MNnSbiECvM931y}*Jj=sG-#H+GiBOfqf!67 zkZnNjh^c!smPsW@cA1dSSVXJgg&Q^AS?jSoPm`Ee)~>H_bxn_~Q&bsVC|SCGQsTlp zf<}eRH<~w2j5xA@XvXOTdsqHqvU#gmFe#qwYX30GNE((3ay6z$Kn`;}SsE$uB#d0o zFUry%e4*hq!(e`E@c@_hFn5gxCwaPcr?=aG1WoR3>r^u3Kz#;pGm@`qA$8tTgU$f> zVn~ICnhGET`6|Cn<`Xm-tUaV#s{OPM9&9SpLPT$SW--o-sn{mP4yq6`JX#Lqt$%;g zJ|4tv@&54`kQXu@oIgw>k`hyKs!KWAZX8_7>_;^1==&t7c8cqCjOETa*s{Q6iZOlD z@HH_#IptxiHKtJt$i{x?8+T{B_5I%e$J$mnYuG zJENv5Hy?&b^ZStWg1{G5FG#?CR4%37ar3=cIP-%Lu8~&cmxl5R;e^11$BBjZ2{tno zCDy8;K+Yrl-%zD5PSA`a<2mKauKUY9QOI5Zz@a2@SLpQcI_aiBr3k(p9HJ&xP?uk* zwzhiWde!~&{ov#Y)oSP+ox2a`3Qui$K)(f>@k^WFJQK0^a?AFwDk{%|v)&?Soo>VL zD;K^tGW|%r_O2eS0zmw<^?le9f8>Y#o#dPSy=o3?`n@7k`7ls9m&nq3u3I8mc=Z zh1eRYceB#YilnnlBEWh(O~f71@Fc>_PK^_V2_GYqmX zWmo4%g*9f;VVecrMDeXjEPcnHuw{VhMl6vL4M37FJ+=*Y{n02*#+xpKsoTlizvOzZnQ@Tiv4T%83qz^LKtg+(0?%2eucu^ zi2+crYsbWFP{!6P-^T(_qiI^Qoi1g5G2lCliuAMeg}U3^y|?|B2)=J(&>8}1>iuVy zz zz?78tOFF(~ajJ`Mf0VQp&l zdfqM;X?bKK)c?!5_0%a(tUlSY(ff=C%KToSleGOG{B7&+&dOZ_GAp$OxB&sGYWHMz z0K?rWt=7eUZ#4~SD87>3?YOrDlUhFGv(M9M_q05zFq_5U+JUaG=J$gF%_eTFwx2&$ z_c`@pr_jx#bWgR{kdO!$g+F?f5;X)f7WvHhrBofkReMTA)U>^h;Tjgn;&<{b)}=UC zR7vV{S{Qm5gPT6YiTgBDbNi`~~xA3)SmDQz2y{tC|QZ4Z_vSR1+|PpUji+;1S%eamP)OnOzAjbFx_HsK$R$!{o*GW# zwh?p8&>ofiM#3>FEt@z4)G<7t#=XPekXwT;RC}7Q^(~uff&Ig@&TKx_B76c@SF&|~ z{GYF%LJ5Q4bIS=g}c)>}k0br@)8&x7id6Taa{VwHzYh*^eFe&bY7 zz95s()S;jyiwyEKLH^fTibGFSOoJ)TPEsSa$zMwlC>;{dZ!~EVWXTxy(|$%QQUE{& zU92Q_V2;>CM5WMp+fGu3*V_wP~Aw$@Kcx|6M8cdg9JW5(nk;8`aA zY5Td3sU-sO3a`=MT!z?xr^ZYxwPsb$u2~t8*BTvO`m97rGnjwlH(d{W7=tC@7xa<* zu|7i3;z%!0Ms=Ok{hOI4(hVuQ{>R3w_upsO9==F;mDL#A&WEFdL17rAfqp6_X04NZ zuV2UetIZM)aupFO58tkY>3#6G$BKTRy>xh7-4KehXG)jar9JMY+Jti25+t~rKU}Y8 zA`+${Y1CU}E4d|{FdUUDN%>5q)?C5PjE)MaWLKZ3wR;FFyys&4uy*${Q53gW*uN~2 z^6+!4Guk2I)w4sptM->(58jgDu<+L%aR7K=l<_k$AY<@fyj z%HTZyBd!d$Qmmhg4mE$;w(|=R;XohJvRgW9-_XwjR>aj~fz(O8VB5x5!v!;5Sy6~s z!b21$kEZ4CPSBj=gu4}(A1cHO%_FNviZX66h3fHB6h!Ox@8;x1e(gVY?WC)0VMQR4|za(@LEwi9G%ga-thkf{QoWXsOrM3{}<~2*YW>Jsqrt%e^Uhr^7JwOKOoPF z+o`}n7>yhTK|xeR1XAUJhCW#mmdXWJuQ>JN5yc3ouR@CU7+I|#=R1%J^p zGou;$(bUwWD#r&iIhMvf5c?IrJ)pXp1^6>Jb~5&R&N{zHlIm!-gdB$nf^F^}AHnUQ zszWCYef_=h471dJ2)Am7CMM3ZSV_D?;CE9+Bvp>n6i;kxYqMCvk>!KWxl>B}`KQ+a zg16HCsa0O-{|R7->8_QV$`ok;-+^s?lmDc^UN$unf){)UAE12aD>U^_h2g!dV1$1_ z1Trt-F?-HNN;Fuc(Zuxe(tsNl#&oIiNbTxXaB%26svJZ(gWhJluVl>s<>9V?x+-3r z$vPy0Xi+(E-Q}lZJ2L-HsDwr?1G13xPTsG9$L2k`k)z1#$)JhxQ$Y`A732B*^(LiH z$BXJ+@HuZ>*YSZinQdRZ?rTfDko0tx`9L&#rX#e^}eGb16=d-wrI0-H%Ru)CgT91vJLY75sAwAJ>qh?bp z$z5#X#kw)l1^W8>Dn;D3;MLyVzw0Rc`R=W!;~_3Kt!EtL0v=CG{sNAg)po?B%C_vN zFPS4ru;k8v!jt4HRy8;#h2U2SKA`qen0s*dS)!qr>dC^q1Dc$l8eV%Vby||)Vb0}| z$DcUKkoKl5_xSL5$_8{<&Qm~kYP`pxq{=>`6Cl&Vg_=-R9BfM@B=-WI;jW*0@ye2$ zrmbkDA@vd4cpdg}_^UVo%*WV_eTGyq#Kr5*{>>4XBQPw{L z_~GYfX0LQ}zMfs=dm^Za6&;qJQ$uBot^DM(h*136xv=^(I~-xOenra$43w!{CL3F;z`sn z5#MLM%S`lT@E0Y0!^34!AM(d!cL?M(--im8o7nMjRm1st3(zEOtNRb7m6g~TN zj(h*(t@~{cd-ini3E=e1hTkJW#%E*6nu6`7tu5pYCFT7Rw|65GRgQC^9BmJcjVx;< zkcE$zZel_)_?NN@lox?3<$F8?h&WB_>U-II-hUBV90;LJ3HeV#JCo{R@fjj!^H_i@ zR&|_XyoQJIKR>r4CMIUk4OFCrc}Uze4l6V;Fo1a{JQoruf-d$QMoJMYh%ywd6f>5B zHWdRw%+|J$V%q;p#TQo69Ec}^U|ShHQe|q09GZm1-r2FDL7!GB!Og=11IBv+SESem zB2~q~?AbgowbuR5MVNtKXvBJoiZn4lYiQ6!oXN1lWMyT+sc!$tWQ#jUYT*BA-v0kM z-2V~dLIid>sFATUwG`6NQ+5zm8^}@vujrLX;Iwb2$OIdxKa=zQ`GHgdMQ|_&-rwj_ z?h`@do$a=PcC%jgPPY<`V(4_7}`8n4sYZ5N`BaC z`*qk%tk~LEjeaFJ`_?@zk*}7{W$w+wA>M0k?lZf3u3!%-CHL+SpOrfIt6e>xC$GI6N7Oikl2RG7 zOS18o=F?V68MTVF$Gelj+~}#RIVwV}mP;puAQ0wU%HYa`%5IpG-KQK_?89=h0)aGcuO|Evz=TS567ryg}gUAuji;fVj9OBy3P1e&> z)r)azm2dGm%7u=0h&lPwhj<<-d*b6yx!>&AA;t8fp*L|WWw1tAr87PdR_5h>K%@bF ztTY}k6>%a0!Je%5mzPrOws$-&`J8VzyrLfJ>Pjmqzv1ePp@jA_jgM%DJRMF<)Pwum zq#84=Pad%8^B`#UwtfREsB5jtm6}^8Tmidxz0>~9UBLQyvR2E|WCNmks%49M!8BcJ z%j%42`C3Mcd~vv1ys{7w7uMt=pwrS=yYp@`sHQQC#S?$n|179e8J@uPY;D{xx}Nxe zzN+6Vy%)MMuw%BtqIjy9qb@O)kGD!~silQr|1fIzVY|pB05$qhU-X?&(&c7Wk}&tX zTs9dbE60zMQ^tS2@-`s?eXwWYbA00bKHiV=;GT%N||i|7W7d%fU)j1$@s z(~e{}H;uFBW}%9!wVKLPL$u`beRN){^rrnM{TuJ|#T64jedX72B$iqQaj^GA1iqHV zWKO%nJ4#4ksy^h*6|MhLV)tIGcda}KI%^CWym>@9(AO8U9vH))Y!=#GJR|Yj`|bLA|YVS%u*Y{H?c_IbF=GHrKnn-BfAK*;Bc=JsdD(C z&D={kliizqUCXmu&8?Y`fPmwzdyu}I+-1$fZ&yCUWgdL0(}%}2pV({OssDl90*oD3 zGrh19-;+&DR&%XtM5q$}Z`PY5)mFRxH8A2`_VhSoW*t@b;%qlU?rZwZO~)bzpm}+J z<{XY=)k4l$$E5~dXSpfT!f11WR|${p3!<*&Cs>JvsO64%uUXV|lyhnm?tFCcd5X08 ziQ+ZBb#Ukq-VV&B@aEN#A(9)rRQf|X>GviOgyP5KK_4A7Xp}MUeiItWo3K(B@Hts> zYAC?QtMQtT{eFN2Kv=lx2{&S62wf1@pSJK3Q=1P;K>XYZe&Va+7mVD6^n=*^E)yl>1s>e|8`qk>Q2FGUnMx(Kht#9L#H~fHEkV;k`qPTjD5NAxtxCchn!R=$VD7OpUG!j*?UMKzQ7lJ|` zY^BFORq#y4lT}0S2>{vb7x`}|pD7nrKwTWg7yEQ)i^@8~l!wvR3_Y`zFkI z9px~0yrpX(3Hz9I;~&Muzdn#pY?h&-4u-ZoOf(Qv!^kaPJ&{;{a}f3%T-`K<2PScL z!o03%Xps5q7HWJL120PAtY0?Lo~lI2H9ij$cw@Ef0Y^1`r@M|A4XSR zo=PVam}xA_lzdlETZ6m;tbP2krfFTt+Uy%Nu?aW+zS0g2>1Q0d$MGe}HbOfA8dD(& zl(Yq#?(VGC%bV8a0>@e&tEh#)pR__!k|YS7Swd8D6+3P+!pAma!wg_ygDC(-gyvYw zamb1b2J@D%t%Sp9D$?n8{gIs+)s9LT<;VWR;r=eXCT~SwE;CI zRI{6r;0oP$WZQ$+Lm!M4zN)$wzzBCdZ0o5cZrQ2h7f5R@7hBeAt_JM~5MvQPe#+H| zJVX24mRI$DN6`vJt3okR@0#X}w&1_D075dARS&xqbGChFD{s$NNvx46wmd&d1zRix zM&Y?aefG#w9YIoaf3bhh!FC?nG)$X;b5}w|jiIHl`mM8Kk$S zoLjn(65E}}u}Vr5I6uD>{ZLR=L}zW|fm0iN22Hb;SID!91&HsHF6^Q(E}4H4Lcnn zJ6w~Lghx=C^YL8x|TR%iyO^3BxTl_G3%&+GB;- znWFNLvKZ6nrzRraF`X#RM`@vGw{SS$)av&jjk(d4{INdFvV7EV(W-u4x=bL4 zBY+QCsYqEFs%``SD)dOOo+AOR6@5S?w#IuKAs_Bt{fH2bq$&4!ibx%M z^x4a~Q$;X>s7-D#hTbcYP-D{c=U6UwUS5TJyv80%1q=_dTPt2Ltei$u?TzD7xsZlUv0`!8yYV{|ugGk(?=QMTj z#PDXF6sIC{uG=3*Z1sXz^)$LL2(Hv%ujI-58sXWdJ8J^j8n!N{s#8PJDqM%agW`qD zfZukl+x-9H>@A?;3cfc%B9H(<8iHGJhu{tY8VC^F-66QUbO_dX2o~Jk-66O%(73z1 zL*s1c_n+OJIWs$Fcl-3Ice-EKtNW^}>fQU@@7_WqK!7v$&LKGA(ZlC)X&EB9N2sf+ z4T;ySQ|piU!mR~)94$Sn4RLtl1nxd})o((BxnHj}Eqr`?k*}vaPs(a4d5V9o#vZCF zdKM%mp71P9Bct^x!Lo@ap5~BQwTl}-PzCWqRF(E`{Kd`5wf;Nr5)K|RWg%Bg7^3Gf z{TfK>qj=M*PT2hZa*8rV@a{iqc3pQr!)WISpOBFlm*3z z^7v2q(|-jnjqPTSYFrI%U$nLgBEi!NtnI!Ze|c91kG8cM86Lj%5mvVhR3pstas{?( z>0yYhl%Z6uhyufn@^84|(A_G{ z+hxDK7i7^Nx-9+wb#^uCy{~I~en_Vwg5m6|rxy|c7uvN+yrgCx;65VyUrmG`n|XUL zmDpbl|MP8ehExw6f%q3*mGvU<55fBXZ$4&uMcpaQrpKkmWhAo3%e?yX-PqX5YUgMp zA}FdC{O~#)5DtJL;=XTB0FjZ$;7-Bwl9V!KDGr#nQ!~@Y4XtHnN|KUFN(NKJyhpg` zHBS15iwDb=OX1dK!COpT_-p*A$VnIpq^PQ{?qY~kBP`)O35T|SIanX{?FO8Wi${wU zMMOk=pUsb~s&dQ=fyV*<{7H6qd+WfP`V#w_DaIVHW<s9rkE2>R*Em+uCB#$dJa%oBrCwD52Df-tig0ToK>(fj(@Ji*>q<3b$_>_1R3kr ztTwewo6%8K?b$s4dLhSbzV`HxCg4`8y@c?0+i7HE#N`pn-8lJqyw_3DLottHC47@%SR-Y}HPg5hq8JLWwS3ko5Gp z_v^M0fA#L|apu}D+8D=u6mcDIz;0TDK2eAL!G;4=w2bLFgB2l%PNqnekdV;t7sHdm zaX8EtCC5!6qiS2PuU5o5f30OJ^tfd9AgHj`UwRpmiej0UaGVT6#1m zuzseGX=77upTG7)MK~KBFE9Mf+xmAx$Iom2KXN{Hl|ZDPy9vfewwF^-Y`eBe8p4GL zwc?M7Y&XtN{XpvrMKQ=qti@LaD>Ac+NLm`8=jW{bm${bHMw(F(^tII%CMayu+M7*?q1)W?Z;KIQB~!3COohm{A75@ZF{Nk5H3U5`oap%! z+(B@k*mG(u{M;>$OSs&m1wHr+jJt(t+J>ndo-J89+mJv-J zYPjqr?te0VfPZE@3G$q8barcCMYzQih0Hj`umgLnPcx83TsY)5^ea5KQ!g=y#os`x zgKU!Uye~Y|`9RHn7sfXE!!3T{hGeqJt1C@j4|lnN9X!i*6${l_LBg}&*FwP_wq4UI z7WRF4d&6*tg}t)q`S@?ePWC8bu7kcZos$z%pv!Szq*9I)G)15R%#HEWvMEcpUZ zIxL=YzFkl0{%M75up+>+zV!Sy@k_}7w#8{aitb@xkg6&Q7~DAQ-G7rFF;sn4bUGqf z@;ZYAw6k_j6peuh{c4BOKN8alv)x*D9N9}O)U3R0~y3WY|A_^$1hsb-ms{4#%a!}{%0$CNUX+E$T@ zJBMpCHppJO@Ii|t@lg_IP;dE&iajqRHt5F(U__I~@z~DxTj16Ft}VA8Y~+e-TK$HR zc1zVgnuHgphywN9>EGaMpRFShKf9no-{W2k=EGIxNP)MD)pal>&&Q!iEpGiz+t(y> ziIQ*hop<*nj11$KVp~RYJ!{_e-ObP`>8B4zo5Z%gMU-$k#=%%seFs$-PzCU`nc z@2K#!zO2M$URf_J84UVu=-qda z`fciKlO*z+(-`r@Wn8|NWT^^#ZYPrn{{hT4zuO-M707A2%%pay7t0u^7(MtX%fF#b zO};BVF*WW~;G^TD64~I0#d|P^6+0(V6hOvQISDv`Z*s%D?8FsINwK`#z24x=l(t;D z!4rXBLmu_;J!hhZJ-a~&1LANDLnSFS2mHIojr_h6E!4PJ&`tEI!O#RijUpy zuy9sneHvq{rL}Y))2+V5fu%G$y?T#ffM>nFLDk%H(Qx)L3JiueUqOBtmE=f7$&7yzsJ<)xqz}|vPIi*XYtKSx$$VmhV&47r~ z)ydGFnMBB^FDy2cIOb-jnmmU`xZ2Itob`$GyOW1gXSw!QSpw(D-&0<}M!n|BMVe=l z56l&Fv2V=hJiX?Wx<(4!t*2oG1Sloj9U-=U?-grr8jPH+nZVFZIz|!ZiK2Gj^;WnJ#|Qd8gAO-Lo~Fc7e+v&m-7Tk zp2W16vU%(r0EiEKtcj52-Q}uSzwpPWz9`u54*$*p7<9iC(gvAh0# zR3Q*_45dwg5^{O%o24x&rPsSpG=#H!H~4M_Z$4indWi??JV^sa7RE-Wrw8QmBNB8~ zV1TiqcJ&;;cy3aY0QOmIHO~&a%tA<=fqjK-9kz`{I5_n+K_ufo_r2H7eUbVu`UZW2 z_vhv3PdllK=e+K5tx4&y&bi>ojC5{y{Z%){rxf>-@fuegovfST1+JzHW`gOo6)-fsXn_1lJc8)!S%?;;NO89%kBukBbAoNT$- z5Ka~Tbg~vpOR%*9gRhXG`ad```)?5_D6IbK+IU~-voxWKj3`CcDoOE_hi`FVuo8FR zjDh6M$YUzU+BWGj9|GI`v9f8x{zM&C6gaixTGSumx<2}T!^23pZQgOrDEmYqe0-KH z#BEScw9=PAZgo_8()|6hZS9plP*A7sa8G|)BDW!0fK6-BNNE*GnLR;PL|{j|lr8t5 zK)dtzIBtRk_bH~!y{(Ogd%~@ugmH)n7_1=DY87I8dL7WU^?~bmzpJj_n&@|o2HM2W zL3OI8XFu+ZxNe@3mAOjgP$4`*YT?I_AvOB_R5n(=<%tsY0Gj6+GDm&Nhx-J?OZjRb zl>_Sj>P_zq()PO+_0(cs7xC2vzWEZ=W_z<6wUG1cB&`t5ybvuDUG#Fe`$|rOt8BrBK($5I?9U*i}VO{2W z`=BS_*WKXS{a`Vg?`{|CeixQ+b?S5!&|$DJzu>CS<^y!6+;ELzmc%&G45k+TN<8a& z8RK`&KUW1GIHhAbzaWznlWA$GVRJM>LV7HTc>I^Sr7O-i_fZ(Er#cUda}^a)+QNg3 zG~AR2U1v~uUchrsK3FM&;n<48R1inX}XH zUn6B}M5z1hqzQp+`JDtLpzOtT0VXD!QsF_+p+Z%zS}Fv#6r3kjP$<-se%RHI2K`G2d*%uYEe-1EseB&F!{tT z947ng!GrNa;g9*swq7iB2};dMG1J*g-|<&DsVObHl=tOyN7jr)?`+1y!e=YqKKJj6Qfpm1wgim7aL-_h{xk6!~~dV6&1sDT78VR zNpp0DADT3Fx%_S0_on&g^d$4A@?H*=?r}bxk5?VkIC~r5007&?PTPL_f`BTsCEd$a zgD#noJfyPc=~%2Hjpqh)Szz440HKQU)@LVs``%!wLE<)+tF@(bgAh_8qD7|2{5@N^ zSrJ)wTr#_4I?5$XB*Y9$(SKL5b(bV`>kBQKNe9uj?4u7Rd*nv zH5oj|lM=Wy{mvq4VdM7c5s}Nbcm6@LPT&F{mG{#umB;<0a6E@qzZnP=_4lu_81V{V zmuVyNU13(lwBbHV>DAF;KG?|ioL*^rq`{~H8V`(cV1jv5;B&zyYn68;dxh@j+<@xd>JEpq?@BPSL5w<^F4O|HXPSB?4f~8i*qE+c>Fi>s0pK&M;6^Fc z1W#dc=~x=qv&sNqq!)X8doO*9M_53IvEDPFL3w@YJejh2E@QlVax?%}UvfZdJd$b8+>Wz?v|iDkPU-d_xk=3{76?DKTqwOeg=eV8eO zdf5WZnC99?Z98l;YTS%Gs?SGYQFSG{N`BW4Ktd^CbKxW&7B2;s$5gqQo7;;PmjbYP z;3*`w_^sN<_~5(8=mlJ}$0yrzAjavby|y_2M%Fbd-5nhvSg z_fei|EiGN*vD^l2iVo$Na*%=StzW^-%BL+2-=3e~ugdh56HiY><#9zCI)gGLGbJAG zkbE6^|LM{sG4~raxI)+Fw&8=M&{HycBmz-KaR96>DQ%d0feY2kv@J%{OY|7&Ks3ie z<<2)tO_|ztsJ@0L#~n!D_zLT4<56okv^rR@gKtr_@(*FFH1Z@1mf>}gR8nSze8%&C zyvj4sPJPWbc=T@gwK;b5(eFt7uf04W%@yAXQ}~%n${wmE-PBqE@gdCrVmW%xKH7D-c?+S zU{ELyoe2=rD3f8SXYn#tu_=@HbKLZOieE=IDBj|o^E$KI9*)!SO%W=3fgq+_#xA?H zDka6qbFh7PeYGH~x`(>;{jt3eVH0ww%Pi={=|-X`Ao4Ru{A;A)E3C(U%I*|^+6QFp zNTQXeR@ouGc{FJEL*@DEWw-R)06ZvPajjJPejqbt?K(uqoFDI zP|GQw7*mcjSjO?fvj-VC5ulmD>9B%mJm$B4Q;Ygao`{=zvf;!rS)*y47gR`9I@jt= zU{D3IuW^?w(~f6U5?n>^IjosZLMrN7IEwWuIe7q?zew4mrU8uzp1d2Hep@m=+@qvF zYUT}5IOmCJ4$^sl1^LGnu@m zN<+SCO~#;anhY&h-cb{+!m{xr+7Pp!iOr=lWjLA)28$LM_S>)Dg=#*N+A1F*(%*v_ zOf515vYT$XRzFBTnnMPLwAG^iY8$aUg5NFBI-K18F%Uoh22XIMy%1|J5DVGnuCG7k zaDOW9`?|!rSMoZm|LT#}jMMQH6nv{`+bPuS$5Qf*I+}fAX|By#vZ9i(#?d8{pqaDz zs$eQlBu}Zi$7S?!Y|t@wi*O78O%yNnoyvlK*-%Axk%}bm1_O(VsuTAuwOCm+UZec?Dl8AI$ z8wLKUsroNpv&@@7QuyJ^KNimahw?v_5CYS?&IF^oIt%jlE?fedZs!QtV=p=m${!dGJ2Vjqiu{Ut3 z&*-~a8sJBStaty*a!rI5h>Wn7BjA2Q9EN~!N>4}k9{y9|S8^$wk^qFYG!lNFt-3`+ zPgHny8N~mtKR)BwyDKORC2?6pa~*m#RrkjlVvZi~>k3GnKXjv*aofcoau;t~24Nz? zYf?`v27NTk3&wGlC;94Tt=+~XdqDI6lMMBRBwo7J4>#BPI270?=nkzZslb0u=f7f? zFJ-vDXT%!&r>SXBq6ZK&muLPx{1X7+JCpacf^4Ah)OOHUCCV<$xHIK%!V`<8EC4;| zL}Qo_RkDO%;kGc$o=;{Cb0Uu|P0&jPy1dHkG?)A^Lz-?zUBvV38NmSWoxf5VsTq@$ zm8{l@zI9hdzXI9X{5b01WFeFWFA;K$xq1*xXibYmSub79XDBsxB<3W z!RFw(HUwEKoysOEce?=05D56g_#sl{vVA2hB}9JO%a^Y#?7b^IU+9nM2&Ezp;hsxE zC)Ite)+S}kDDR|KYp$bd0wJu#h(BPCduK7xH_V4(N!bw}0P$RZ;|I0*<>Gki^ z|6D#h+drL@rKYA%O{w~S`GomiQ5eC)Su$t*JLkf}!jP$lJuQL{N*3+*xrr&iwJD;&=Q6!$sMKF}wPIDt#Wx=-Zf|e*_UKqy?c-jlRR^*@(BK4e?VI-bb8_4YzO zluC@MjjDx<*>biP(rki^iC%8q%{BCr6sRxId(Jq$xyol%lzvP`_6El?)fS^)1=9k+ z#?(R~P>@QEKeG_x9vZw2n$lYfFOTcs>&odB-iCPcX?WjgXlp32xMplQuM zr%FgV`7*IPqNB3-yb73XzWOF_y^^L1boYObBzrLQ&Lnp zN17Y?!;yp4Cg0oK_Q?dKQZf=cY2h|Id-ZS0{55o}O!+Y9^fJh;{fN3feNJ$BZ`eTcS$w`F)A2FeeqLL(9p zvOQ^SKUTlzIbF)JIAo~5c}1>v3>qmKFP6hb2E?@9{bB|6$nc$cASsN4YVJLy6wzY3|Lmp>6HO9k7DckqZ=j{dq+z1=(oqa~bz@IQrw&2@Ii{B6p z`e>LTy!u}dwpT=P50&ln7FrH66+BSkzxD_y2n&-@6F|nSW z(wImSc^()W8=a;wG(H_VF61)4r0^M0t`;d2lF0h(Ea$wd$lrL*9MbckPSlaO%l*`;Z8(c}W)ytr*shYaTe?v?57FoGd=>%_%kDV@Y$Pgo#Ldkn;Ifg_v{DvGwI# z$6|-suh@b3#tZ_ubm`HwbpI5G|8bd!({yTjU55T+6BWMFe9Nd?Iv9#){O<9?*!}Lp z%-Y&|>J@?vlab}3k%B_S-demWf@{E~5>^z~Q1d%&0c z2_zo&e@}Wb40!RsFaHX#KSB3H!@?#^ty@|76L}+gchJww{i5#S#em$oCy;BXA2?w^1}yypF9p9aTC5hV?@NXfL5KMU#J}H9?e3Aq}Mx++Y4xogUr>@SGW#l zVGW6`dc{Mnb+*01n~t9KYTBUrvi6$W>69Jd@f~AoGMdfZl`G!;T;9PwE~eV=rIRUs*o=<~nyYGh#v<+B#B6p-+)+B%~fbgQC0 zrQ_sO1z8smy*-H`fAV78YVeSL%t-Sn>~P`lLfVE5pV@C8zCd)aX8-P#y_QgT8HBqt z+UP!<4GOwnEW9aeRi3O|C|3>!)A5{lJqIMl4pd22uJt0&RGm91y=O`jJj!F*22_Pz zbY_i^Xi^-^QM*XL0G#WQ9w&O0V9<25SrP%4Vj5NoT>^MYMci43TUJ|Z*}{4$E8xSm zR7csKubW1xY>A=>YH(wmtU&b6%$}l-o71LJ3%TK$b~M-iDAF%q;NWwA?VHkI@O3L}X$8}lXQfSyj|~8zgq0LVG~-aR zSQ9)!2FNJcJy;xfVVez>M@NYN*Sc+RWpcT3(kq}wmmv~L&1^8vd_SZ2rWqTqqYqvi znR|Imubh`fqlAKiYVCSLEr@`E8Vn+?(%3h$x@O1Qorx4^a>e~2_Rj?iYmvT0T9{nd zGfVjR*E=0QtK2FR^9%AXOAU5=*VBsjs5yl-V$EtReX7U$G&PegB&-LDpWQ_`4}|?j zgQJSMn$tUAH-NTtlSo3j`r&0k5sqrygdNYMq$Z&86&-wXD&u(JON1xs^gXq-_L^av ztRGl)V*%KdlcoxJFbS|psRR-(2RmMAt{v=Cc!ZCM-V~?Z@TFLf>)}w^Q2Y9Q>fB)O&3VpSN>5a9L@0*JkOw~s z*kh#bjl~?wN^~@F*wY51{Wy_``q9?LugA}!l(Y$8S&u)uee-~$F9Mg>-WHG|bF6L9Z#QtE%ei>o2Y@82GXZDp!v;PZJ+Ia+H=^ zeW-O8lE8~dR*y8%lO;I{wz_AVI4`1*M+xA4AgWZhE#cH<94^~BU4;Lm%C*VBrr5vy&zUv(%{=*G@?x4+3+?bF()UxwY5z| za&KL&MJnh7it%cPoTDMik->&ha^B{w0k1FOb|Wz0cl_oFEJ zsoN58n>hX=;3xUp;`OUH8l!d#IpY!G<-R$4diZjm^+h)}iGXOn+<*Gg_KRgf6@M_e84jccua!fI7baFV+ZJV#NpWNsYZjZ%7wg-UHnVAL(2xRT@|+wTJl1yuMolYF z=mTDNp?Z<|JX+gc;`w>(t#$4qx&0{ERG9qrE4$(DmeyL*)F{WN!U9v1Zri@GXl$u( z^pON4XB3U|6~|5ait~qY6GE3Dh9Jo83vyrS zqt)|3^u!1bIP;zH+hGzYMQt64GiOWPJ}6X<9|*Aj->cz~4}?xjh{4XPO;} zJ=4T2R+i+k9Who|Re;M*V8u-_05cW8QQp9+7V=1FSfp*oa2K2QOU zm4cePRQRpC@zP@C8Yu)T5@!g6nOa~+tKzd!rADLJFIVa>rKlo)Tz8Vw<;M(~ORH@8 z{V}49`9X1Pz>{R2OSe0#EV)BoY1VR5&b8->>o*Y84{V1WB+aXJvIWKFJc&LWygP~1 zLTxe+k*$p}1aLJ#g!*bLf6Uu=INnp}w@M{!j5z1a*Fhz$0)>`|&)S;Z)%CPz^M z*4-ZtnV9e$#xkn!xaqUCr*Oz-*$wLT*|psDf*JB2OQYe65B2s34TfFMYUr`xp9CGF zCD*NR)5>mYhK=Kt$+s&b~%N? zo`q-EN}qeMW0EQ|bS8>CNKWOb94=gN&Vj(!MNLV<(7(=uN^AtMQQt*3RB>QmY5zW# zG8!=NvHTN!e$bI?G~_vQ2S9nN4kJts{f*+OfWYU8D+6~DIaIq&k+iie3sCnrC~7Qu zt8_nUI(S#>LEYqmWRK4|UiR0obb_fNZ{UjLwRk7UZSJyw1g-%@IiV>N;aIQQ=ZL*0pGY)HQ5fprJAq#c2;mL<~R^Xl*5ew6%<9SRKnQ&Vw0KFoEQ6$~nPyGXn^%%o0 zG(=y7eqL~3Y+`CIn%&K$v0aP6FtHuyleCNC`E-W*3I21lZ+!PtM%GA=R!OkuO8lp5 zqVej{?X(UPan>EV(pw$VKwkC=MXCGi;mOEs>!((ms5|^KO`CsK-gz+%jbyZ)p9-`c zVyJkzJ$6gx%JbNBI!ZbdZCs|Ck+y#^HE#8KJk#6AeYWKpw58%>5Fh&!k4>lN=Xn9Fj3*JIqk&)UES2`NthQyaaq8bmM>DR4u)MGkpPu`G+U{V(@1iBml(C!P| z)+y?+#`1n32K=-z@E_8+XN_<+N*O!;-RG`ASp9YSCVucbFFMz23S+H=U0$*hF=1JS zEJY{5{>nY(Gkg1W5BV5Sj>>s3>3;`B`gjr`v}(<*d#3N!X_tvsfbKv-Om9<+odmqW zS`iGtR*iV=RCLP=B*?R*SQ^@057mQEa2(Ko`VjXox4X)+BzCw#8vGz$^78UlR>l7y zq%#~C!WnMFKM<+5t}cGil=bCwhWF{o#>6xq09PF@s<1q?-!C+VqCBk@4h%#@e2Zrw zfq$<|7YE<{0J2zeEB?>e`C9;%fa}ozkJ#q_67$Sd9Z;h!*nMK771(htBlx-5nKMV^ifd@;iQe$uz+V zWK{(>?ajqWde3!5A*@&dn~@&T-;3uKh$WInrim!<|0NgCW0ycd6eP@=+bAahG; zb*_U)=18SIH9A~vX1;gz>iOJ9eqHCF;!)VrcGy}W>@{`oW8H<~#3Y*M^f?!3Wk>}q z^4N}V!ZY%CS(THBc)7*d?z$bd%vi5G`eZLVr1>yqt=#*JO;i>brIs>vQ|$xQ7}4|+ z5Y%P&J`^9mL-^S57>b4Pk*7*_WDHjn4&JP%uehz}J$NY``*Y|TLJr*E&LZxEf%J!j zQHKB{TnP%PYN~D#SbS8qs<}5B;JHd-W6?eP@u!4}| zndF`M=CPK3iUak=@JM4nzcbX!h#co5;%LQvBenopV|~#n3kAi=`1~=UBF@*{EihdTIl$WdkFp&0iz*)L&cYc*p;)=)fsIclIIzqdOp6u-B z9;i~G|Fg_{)D)4s_7;}tSfl|<&Cxb`#m(dxJ!&HWnXx0=8o+|xPNYMA-)F@$O~vfW z>$g781yo^tCsc<>V0!ylv^tjr>zf1NJDbS1K&$F>?zi;>9kwoVh^r#KzqBDu6X!ww z{6#Bx@FRh6&I2VLqM9$4g@SHVg_*ux_qumIf$-%@7A5Ver75XvSF)P&4v6jI z(wljtvFW9<+2_#f#2D_utr4h5Mhroub4D$(u-mxvePE?#m*)yVL_~RXA_)Cp*$_p{ zOBCD5;CsMre^fTRONd4cNOW*B_$e#5f1LA=5%_9ETRl;d1SJ|gT7O}^RGgKFD6R@r zVNY;v=G14mgIv3HzwY6!NBBJXMp4W$xkV$p7l%OrnfZz*@uiV#=qs?tB3+x!0@TVv zyEsrq!BO;$0r#`+Mf>3DFkhLwOM~>?>VT+hg*B@F(JVTe46nnjO*`3I^5Z}+(bu$P?|W)9)QjjD*CMQOyZyE89;rH@PgF)6nCO@KxGP7p zZ!)8h<{Z|bQ5iHHD?7(l4voO}G+T~8@D(XHK5`dFDiPI?gFPUBDtFz)aj;{z$LHnFOu2W@(gj{+$f;`97)^ub9X`{SI?ort<5eW;K4l={ zRah+|t%`WgG@FGlIRnn0d~7Rp!6jozCRBLTb_aXFls1bn;D=&E_W6{L%iXq+2E)&ekm;P}y_0t4rv@HZjJjsCxD?e;`DA zgZdkjORcBYz@0gs(c=0--<@>EbjsbLJ^i}H_i1tQ7>UJJDS^2(odwyOd%1|)#kr#o z7f3FwHQN#DBS9blvRC3lbmwsSNbm)8|7T67GTLIcup=U)+Hg{_f41*y?Cj94Al1#& zh2YMJ!*ov!<{!467vpsz8cg=jJ9dF`HZH~+ZPEtBMxo%@d^7J{D-U*Wx3E}sjIxZo z%g#d5G8W=ygUj&dnGb*rPw&~{NGZF5t*YFp2?1`;V?8omr*AfrTX$J$^=v@XVHxR) z^dUy^5VSS{Yhc0h3b~$_i#@jCgNx!C2M6cQOo9SCG{?8496#s+agyR|#$SWz6};JV zS!h8;bX^gtrrSub0RJw;V%W-em0@>Cp@7tP-E+NOqih`a1DtG8=Qwh6c}^HLPEBX1ik286uTVgjK}$sV4TwJgy)$1q2az&qhV*e3^gOzoJnulMiWl( z8eSUCobBMv<@0RZ3HjtdtZO`Wh`MYYg)i}x6@cMNP?L8t)seDLrTSecG8&Gc#KT4m zRmx0S;{D#jYgXF%;ywO@~)kqnI~_fo#sW8R~3M{%e5FsW)Q+tCNeIL z{7qJ8Des}%#SXWW-y)uBHF7yYs4ywdg0#x_X$JCNBg}e7RWurWeYkw?x=xV6PBRl_XG1=W4P$wjIK?oupHA*1}VtxIR&+x8IO=_ zZbI?X)ca$pFBdd=1v>_wZigV{K+ttZrwC@3tyEDy1H*Dnut~u`Gb@Ym;XczF>%*-~(CwFAz_#A1?+=XQ={nCE^O0mOYutrv zh+=1_kOoR?sO%bvrkb5siNa zgqE|ovmVE(59JdrSC%5FheoEh&Z+3J$K1ZW>xi2R>AgX(5`%Z=f`Emi@}i}zg*)WQ zd1mHapV%S&RoOQ=hqct3`O3j{`(Epz*$2B%wB*CrDHV*a){%Idw3I?jgAUydk1jErqp^fYT6CD zzhYg+Wv|qxWVZ9`JJ9u+#0ir8}+SCf9-F!ai-CKRk$VW$#IC4YBQGcv<%T(+R- zyme9K2dH^?_Jal0}be$7mY_@;tz1y%1+I+3KGb1NdIej*7Te7a)c^U z<}T9`P;7LnVgmIB7A%6SS-7{alYv#vcGkp+#PVEkp{;fn(kznR#ME+`UA|&22CZt7 zzsaboTKFk;Fi0mJg3c#$&@$Q&S$N;m{z?rZ2w%(lr`@eY{N|RC#TQO1>$j?H`B`+A z&La&B#^t124~FtW;*-_7zG3|Jh9hVnR9~%$#JF!4lwUHLm+kLfG6_3q*U7M|X-dyM z61={mkoK}Y$1ZuW6If*2XS!kqBr5h9u6mo z8SWXV{a^`(+{tyvP{+%{{J9VW2y`b|#;<4c zId)MqOyqAHBkRdE(QGVqGS?y|Z=+X3Z4R5T?%>L1M&?d=WVMaH>B0IQBmDS!Hn$nK zG3OoJh*-n^^SI8-a(L2QWx^W5^i68EdY z?{L%n@s?z3T8Q@YO2Guns)Wq?y~h|CHN+&=oE2?W_Oxqcb_sQu{~LT8+q{X z@L{Z)@LpeTo83xHivHQ<6ytwcws($e0{|0-Or=`6AKQBTD zzX*p=Qc@Bc5%;Iq994#B5&{2zjf-lXSUA2Z9srg$#m@m}GC3|A-B6xk)>ae%um2{% zzq*@6^m55Ko8?(dbdZ#mM9Zm5Q})mhaA{9zdI1csjM$56DdNU$`?{gQJ#}%4rD4Uk-od#@%DN`_Rl(_Pk&HiIS5eF>K5v z9gVfAI2Wi^-qSbu>K&HjeJXKU)fyH%Fo;})AY_HphaM@sm+r#FeudgMxNhd+1 zij((sdd##|P=oa;8$($Ie`Hw0l`JEzg1+D2`69Dy3qSca=J|X~3ocZr`fy=WFde|a z;F>Pz{#CC<*?_?JqRAB%uT&T-XWb*X?#znMYo&3Ar9uevZrHg5SC3VsP{CHB^UWxJ z&0#;Y?!&sRZy=51$2{WH3GWH=@m-J&fw~0P1kUMSmq1i$em$$7*T#el!Z@it3vORSZl74IVf4zZX_eX=Y^nF@GkBS+ zO%p5&UF%`3nHkn3taWs8t)U>z3SZ`MUiuvyJypke%`-pNZK12fVfHvNIq0Ro1(v@) z;`BY;3Ocefco`d7Wnl9eZ;h-`pIYS4m$d9S!o>f5LdEBX&r=xHqIHqgTf_a*SSlJ| z*;zfC>%Zj-Q9pj_pR6%c)aVEG-z>?dB-qYdPR(FEwb#!Vka_;T8BGJx1yK?M5&Lk} zkZ8-NEP*-(uK-o`&+1(GR#%O#MQY6o>2qU~?5rB*(fh2*GY3MW6lvbS6QJe# zZc~co#6uVQS^fCF_uqc!-oOI15w+Rly)sb9Kt}^*CRMz~o5bU`3$1;hCgg2ci8C>(!d`K2+nP=+ z7Xz}BH-}sw>%KAbbbRYqQig_%fi*C?tk#DalawN*rA|U-V2ZC89FZc@nDCqJ*u9ut zU6eB6iTl$4(ySq?3#+J~#E0`;7Hal#zxJI%WH_Bgi57OU(r&rGMerh9X*pJ1|LE$u zwi2Z`Pi`yFfXa5X;<$KrWjWPfpGLo0-EC3Epts=2`!p%s_rVdQ@NjK4e-eAI{yQ_| z;|HOGZzenYtHEfbg2qFvFD=Q&U*y0MRq!MNGQ<%U`B(c3P5nQl8AqvdSP;$N*;1Hg zw0AGvPIzhC4E6@$n>X7q1GUfHK@ZM&s`6@w8kdgB4BNl2ASo6Y2QToTPSxH&_q@}E zZO$iAo?mhdS&#S4a-=H4MYYnYw*%qOXud$8?}}z<&OWt0j|oGnNpg!4uIMr9=p+r^uL zZFNmPGxqL+-NFicA+B0CY}w5Kp+y$LpbJmzsMu$6cP=OQ;oT?U@t;di)(aUtz40yD z%jl~lt_D!+9EBrc7Tbltv)NNH{1}*p$m5OHb5&fs1u(?&;YObj`=jnbKTf*IFj185 zqd13bFltP%o1WYK>+6f2IWb_IyY*#0qhyDFT8Z(*)suL1N&$y6I}LYiMXq7BNe;HO z)(DzJpho1kc^(ZkvUZA2&>SmAb6jlpCn(FRR*WVS)}qGuh1#Jmp$AeL%r^F`vx7|? z1wLQyS~VPGfD)J1y~1KtE|*<=tFyaTfSovDf>|itfLzP>{-{L*-|Xu~S7z=0Z9Pqa z-L6=&R2?A=69PrDtlcJkldNCigJ6X^8mu^1H&e8xraNox*Zx1iq8gs;hJ{gOt9N7B z&lqKTRkhd8Ss~Zor>4BO$x3!A61j1KkDlxW-&%tG=qNt=wE>#^Zrx?=8B;K9jND&< zw?bvy8v1!El&8x230ry`JI|Y%4Pl7^P0#CtbyDn9I3;Qx*E>s_Zx$<%T$bm{gHp*U zHmsfd7i^dDPVpNO?XOKvXK*ewKA@e9w}jYr+=jLXlw2?Qk_SIJH?1EnMPXNjuxLKn zy0*%N4mdnP*+7TIvlq_wQ~f0RzMs2`-Go4K5^FdU)f)@FpixbFyW_Y@Z-_`lzs9s8 zE(HaP-NSvXmOM$vcRHyLOu=K~r53Z&-6v5xtqxtH`l|Wn8D#ew9O8g1#bhE2J5vWw z0&h@qCC>|OHen9HYuKiYUA{cIO#*WMfss*XGn^h|!U=f&HlBwobXpARV)03d4P$C@t z*=&u#KTqoHFW+Vgy%MhRvPcF$pXJ4)2yMm~M*=B1Wxg9sQVmSE+ttVX@;N@l<*Z){ z3s+%}oc(g2*TR`d&WpyPyq4R$#wfJzw0Yw@Jumt(|C3uW_WhI!cLuZY6T#3K#o1s|3&};9tzOzr0 zj4%2x|NQ@=LmpzsClm|NH}n=!QzgKMma#0k9|sV`C6no270mFh`pd5O(Fwf?QY?%P z2tg5-HRvA2QENm3zX}|GD0&+FfF&v{j6SMbqCVO6$_6SUrQ)tFzuJXPy)PY?m;2bIOPcSd=pb@?uRg;r}+gYI#gg8siNPjvYIH52te z5B}dttzXZK2aO+;;qpKYz>|c?zEnjU3?8ytcqZU}Wwo8h@!|U0B-Ui*XZ+SuyWMRT zXC!!ptYA+symg$4mF-Zh=*kN+`f0PD2S=FQ`J}%vF#eal+l316-0vImeM7bJt89(} z3yM5gU%$j(TGwnUne25cv>Io~M;hHGNF950*|{Rb4a5l`C_Zco*st-hD3AJ#zCIa8 z@&*kMj1gA{SP?{F(r+*cl|XjWqSIN)dp!sx!eKg5JAZu|jA9j~RG|WXB7q*~-Z}pD z;~^lOT`w1kc4Hr{p0Y?r-9P*fK6f8?_9jWsZC|!3{jPn^RJVu%)PR9Hj`{{RKiO6my5ffbyumwX?a{LU?p&#{bcVx0ilel; zd(9qScl+A7q`3}_ll!SVi?+)|T;lSY*j?pWC!mPzIa<0OKrRGI3BoEvj}Q2nKit>1 zI$XTTs2K_K$M3zJtDWZ;tMTA7y`8L$;_Xi-tD#n-wkOZW$f@)^MkEBOcN6VPN_SZdhiA)ePZif9iBm)li1?hX zvJs)wT5;zIb*#gWr_E0rw`omD6Apsf3rDly>7^m#>?5FP$nlmzLh(Upv!O|rL#u|Nand=3V~kyR?T8xfz4>k~CunDKSw?-e%`M+soIs+y8NM47 z4!7o!SsYk`MeAgJq|3-Nh+Si!R64xEN)hi8$R0`q2KDs3Z`<|DWyOZF*4NdSB-Q1Y_S@zT08zdr)MY#A(0z^bH6x!}&1weR;q5LV5XH{8`B)R#0+Csj{} zr#=c(l8jb0>+97cY|8FIN}qxr5Hn~9-a6z`-GqAjL1_ zG4)XI;QtL|NXO_hJjvJdP`Jx!qU6b?8SYI(nk`*}TbV+cbw(49l`E9&qEygg(;tTU zf=5njJ@&3Epc6Ue>4dT75^k#Ge8)-Ad*?i-;XSP~D)(n6;pkbx#n^67ENzym3X(~K zR?u{`M$!0YJ0WZ?{N$-vWeWfcw zIBAs18y%T=^}b|C>hM|2;NU>t+VEzazNWgjLTFq9ZMobJZEru>8d-BD#@7lBIrUxi z5+uKNH1HSr?El*L@=wQxO6?@h5nN{gp?d>Gnx(|&ol4N$(rsJZ9GUtb)LY!}osgOW zv^q=mrnPG_VR8K5LIVJ}Pir|x4#`wsesVXi9Xc|0 z2D8Tcy+J3cju3dkLM5FjQK1Wii7DK&WC8#B>*q2OnqXZ)csO}EG$9x>3B(uK5q@w2 z0z}ynslZv*DyBBRXPQLg?qz(f#P%+H-u_5U3ER!buA}xN3A4U^FPbB7jYSx-jvi{p z-fP#k=W1smh??b(Q!>XI8ewYNJ}WwAG?il`&7iP`Z@NyY4g>%~Y|2)B^_yQ9CG4_8 zAS2w412e4?$zN#ZXg*iH8eu!lq$-4U&%XN3tYH7Sr0DVDbU{&h_^ViTiW`#H>+4K& zRv>h30?ra~+}Whks%4zEt^%f`gPiBB#O@E%0(f65{k08-{e(%=Xs8 zw1&4qFOB{ItD%RbWUi#NYZEetPFQM>Qwo>INeF{cmy0)xV_dubx%9i~HVj07pB{De za|Qd|$Y|rA?(U|zdT-3zW?)*rIux67)tHz#shO&02fwu==xMtSG~}x>9a(PRYT`Du zt^b!FXGvD!^FcX zQRcqyzbjMCbDqEM!E!yqOS^eJ|MioigAABuDkPfR$B#WG@vp7yF zNL)}0N51DG>^+Z5@kcn>!$`qREr?TtcpSfNP9FaE299$%t+w5V5I7?93){4|WBRL$ zuzgg?1%>m3XqGRF(Yv#=&ngN!GUek<$mUsl zy20&Z2fOw68*JaBTQkfu?i2y0f`3*a4R5*8Vx`Tf!C$OW@hNT-M)F59PYq7LI6_Fb z+W+Qq>%xVHtMOKGF%*e_&>R0tlHNB&Rkt}i?9!&O6}*{B+Mye`TQ;ryBcj1;{muds z-bxKr>9?O8nzU4w*odtreni5;S|FRKf~q)6f(YDtcc8O^!f@xnwyP&g&k$K+B`i5z zs~JBut!17r?MEVM^t5W9chZ?Db3u%rZBVU!>@*?W^o4k#w>%dY;Eav`N37^O&yBa*@5 z`2DnQf^A%(Tr)?Fa_FdW9fESeBll^&KEj=-3?w}MA?q?cFzjfG(#TX*!L~9nmn)9n z^!6taM zTt~{>W=KtE`eOI3%V4=w(q4R1^ZZKrqM4}{mbuM#RVng~ zqa>nO^p@r!B;@$esKbs6-noQHZgwFM3PL*1}7_{6YZ zqmN(bf(`db6?=qU(6LcXzDlG>m*PKWsJq@+w2o9R!i=$|DA)6)3>c3I)gFhX6gs1e z2S|jK=A)Gzksck(IPR(lBPYEPPl1jCSUk=-2D=r&X;eWd?6^(c=2|!}TyHwdj#-l6 zL+iBtHI@&@H(s&CD$4w%ntT_aYVD+4Fhx~?6A|XyHD7W+OpYTh3&G+I9e=I?1D)6R zan2L2hk_sa1uC30%2%Qn(Dy`wU*CO1W#;+y-B%*~SD+cg9+mW@;iOt!(qES_+d=Tx zpMEx-?3N^b-31@zDNGkmTFB!{2%1qL zW=Kd+M2v;-)z8|i!J&8}UOGbn0A6$oZU#k$YsEN3LTSyY9_YRtT(e#kRgY?Kn&MHc~5`IrbHRkpK^ zW%suz>Xl$B1tk%FJpx9cfmAhKa?is)f+bL;pV>SyP!W8@g_A)%QrLH?b461PQoOAh z_Xi(SBg+HUPnVkLjCyeuT-Dw97wZE=aZjk-fp1Ch&zC~mt5J|_ZFF(gCG^F7Cdj4M@S4??QzMFoleThC_h&%ECCHO$ny zGph`#ONiS6jRNP|Ufgx2A3)K>W&Vt4DJNI=RYDWl@eKHN;5V zdj0;Mod(gm*=UX8Fx9olWr59F+HScAJ}X*U zXY877yZW+ni_oe#>@1MP&Wj8^##V5VryVuO@#jQq_)pUcuSs)D+V9=(keEpoin}uIUJE>>K380#BWpq1DM!y z9?hR4zO<|a4oTk5=dig zg~sDPt*up+mOyo1&7G5u1Ic@b$`Yy)-%s=r2(!kD8zZffFol}C*PedyIy_hpZux7& zssKDNiN9-qVT0iyP8G7rz8b&mY0?rE9l^8FH~b-oAMC5iB)rZZK3EU|2+W|ElJzc3 zlp`so)}KU=ZZtxbHbrI;IUHbcf&O27TgT3e1^~uCeMQpr_m_)~oSR9`qim`LHeFr4 z&xUnomDbDnZ0eC!*(G~IE&CzC3m9DmkEO;|K`Bdh`c9(gjad+M;*p4B$SR(w@19=e!!6{oyv8RSzZGZ;OL(?-EkqtN4V4 z?X7^spVL1U52-Od)`Lo>eL4%*$P=?4@Xl>Iw?S`M!bfYJDGxyP4mA38g^MK8p!?P& zNV~JcRn7OubYsYl!(O&{n2+>#O3U-l6*41q>p(*VMqwV^AGt)mSj z?acYyTp2B{gw(+gG{^bbsUK+vwa|&CS!H%S3B6}KC}Elhei~#Os-(C+pZ0UUCBdhr z)MAx6!X^7?#0thE2?y!7_E(;{B@tA394Cesu?W~Zs(Bu_@!^GEzb5{rGa%ynCHHk9 ztao6ZQg=!dUpd^>D2fwT@G-i*An3o*SkYaQw&in6fo3P+Ko(a(tyIh^evl|A2(@e z=9NR0EBLJ?9P|=D3hOq5dmR@nUM)w(51w-mN_*-xZoofeEu%U~ZcfLY^oaNO{r#-y zll%N{I%(?mTSh+`!Y%cAC<Z9crEs*36uwK08C$(8 z=hdK8APL28!jx%fA?u+k|9U!hHxWw7j7|>11`stiMxs0}Rk@5srHDkP`WgOHy{?7$ z98LRS!7^;t*tebxumB-%z5D7Rn2!YGM@`#BF^k(V7h+6Giwr!p8A?mWUMxe^~$|!oS6s!?Q#EW8R{?)>UBf?wTcJ}$k~GeDtcXtX*H`^ zm>TDM_+K;6^V^0KBG;TdRp%zYK3yM^#m{w(RdDH~eoDO-|4~JK(u;yFF%&{`aFc9e zzka50zUeCV*~08q&fd-3c$z9z!w-X9rn9r{3$D!!L3MUQm#y*M;6SyD)3d%i#E%-H z!ygV#uw{?^?FF?mToz zAuq{7=u%=n&)6FZw}uo8d_2@!t#6r31%K+;WLfc-ZJj+ijHh>8f^6+A;AuCJU-tXT z=deC&pGuyVd{;)4ePsA4C;k2bA+>!RQJ@5&%%^=*)G4Un773t5Xy`?{HVd# zatTqCaE&jV0%5Ca()R|1=qAB=2v6u)7zzB4B1hK{j;CLe?l*6YaS8#_By*)2Il$Hr zG#=UHiv^=nMk*_ypf z<-}bN0*@dweQ@+30swYMvV4=(uEL_#x*|WMmu{E}RKWpK%Qt?u>Rpw;aNkYb!1y|( z@jzpS1#!}SV#-afCkf6Lmk~@*e3ia65kCkDTpVqmteY?jDzf?9a~CW}Gq?!XI#u^< z0d3=KByi5sQRw-pTo#F@fr}4Vl5)CYOWllrz+0S${k;9&1Zmh!_Ob9|gLS&_9QjPH zUZ<6)>Nj?uw`N_791B(@REd`!q2lzD?<6!YCHa{=ub!*AS;M8C>39|3ayN%pW|4Vs z{CHURyP-#^Sy&g)WYrREX`S0TV9DneT57$)g^mQds~_rB^qlg*+XKvHkN=DXkp;l) zRtf}VcHWao_wTM%3jks~eB2)70)sG?Csh6$a!fdAudq$f#7jXH>gR^wg zfljhG(t`OKdG-kWJB7|7ZFeG{we~Vj<(W%wRwQI0@r}8pgIJc|AA8*&9P!uO{6?~_ zc-|1ScnEJxUKca4_(!t%10aH$_Sf0M-NnEtvtDZ;Qgfp*ACvrznXAOds^Y4AB(rMU z8fJO0sbj)x^a*q#>#UEnhB+qp4k@7;QA=6{QE;-Ii-^TF%GDw1U5QSr;+uCBcoLsx zFnHG*QZO2!2PKF{@)6a3AuWZK+%E z!^!v8XWKO{j!4gHRmJc%%#IM*+xgJ({ zVn0sx%zpn2?FoD?pZ0bi`u--qla;S^H9%_ud7ItX%wKjfQJ(o^_i6HYNfQYAR{Q&+ zs1;z-&PO@9I4f)^KG*gZJ{&*aWXHO(7;=e^9I z#AbE7Jdzw8gBvd+e4tX($Dm}N@dd3(q{kF&QM(NV>y!Pu!LOUXW~ylgwj1ZKZmL{8*s$d+ZM>>iehk6$T`rN$qZ zqSn9rqs9BHlT@!y4KF6ekN%eia-C33l=D3}Pw@`;rf5ng$%!x)q*C_yDl0&~Ghc7w zgSq#cnzk#Eg(ZPsqPmd___I6xOJv6`em9X{e;b0i0W9QFKe$w@79;g)V-^x1TPkQWScDbBP6QR7SAVDn?^Y{T=nbzVHWDQ ztHnA~?R!+gMS;rPB^0o1;==M-grq!fE57td=3R;jEH&O1g*8zJ;rbNSe&8yqUy^>~ zWhbbR#q1Z=+S4N?l{VhC8AG07`*0tdfNXv@*R52Y{Ae}G0UpzEzms!`d-F???TjIL zgjVbdtEet7u+O?^s{-JbR$^Jlji?_kv(jXyPG5n6quH`D18QC~I3HHVdqII}yX!d- zc0Ek5YIF{Kv_EpHRHw{UF*Uts2L9A?sQ20BS-X-GHdso1Xm~V;ma2Tmhf^bW_Q16- zVHG@9G1|smA-hCLsZS|;EPvn8Y=oJ8euHJap!Rsz7;UZAGu(M|O%QpI_jdS}%h@FU z+iPSdtal|Bc#P&h*w)$YbuDCEUDQo5@#P1XnoW=13IG5~6^bU|rBvHSbMxxLz~mZC zqj!bKc%L=AZPZX0OS0m~-JY9&RvlTCJAeVmuNW1TI8M9)05Tg+`qa4AoxbyvL&X~U z%))r)0kvC>_Af28zmIxZY>wkCX^~0Vjr4OuX>jB3T4X?KZ{z(u=%n`ny8fmbj;C@W zd_+JJfN6q$m4n#~%8XMc7{>%tW~n zM$@S6&}Jv)O~*iWIGm8^ZNW>x{$>d!H~q8n_oP9!w(%r(TbmHSnjA<%$Xe^mbI|s&9Nzjo>+dOB$XsZhrzzK75U-7G> zMPboH*Hx&Jry?`4>xtP)Ho8J~#8@2s9)H4hp&Vmv;-TZ{E|wElsXepRrm~L$yi(Av zRC$wXWy(6O(Qwl>Lesx+uu`oKeVJiT=}|Mmg877lNEYA+6G<-GX7jegl11dXo!mN+ zCUi*25;Pwhl=sRV;UoD7FN7J7Tk+IR--KkINBO}~E^xtOYBOK!=+SbgeDzUOb(_O* zd6Q0bSZirT#T^TOK@EHme8YVR2WFC%^ANobB}oz0S(VI=fYA35i> zPgbAI6DtaXp{^>_c3Pubs`2u+lNQ(0TKGOn;q1pqbUGZ0bV<;|#jR)idAk_cE#s!1 zqzpX-N2Ho9l}~XBbuK!Rto{K01#wif1tu29)~VgkTxS#(adbn=1&pV@1yxZN&SXdA zyiS^9s~ zL_^1>WLD~nsnwS>vRWgxzLd`>FD@w^gi1coug$i(9mw?LLFy^fTxJ?p9z3PdF+HX8 zc~Xsn2xcTl>vt}H*>CQx>8HpqUMn=x$l}j^wpDrxk{`gccEgn=9A-x7h zy;_q>P3)Pv6BUBM3W=I~FFI6m(#a1tSjxP-fNMXrrD4InRnc?YmY3yy<7IjqYyfn3 zD6MLHFTR|k^kpTe-u;WWRjh}oTtu zMR#9a8+{{WHCnjJ%?tzUQzf$wBGEB2jT{65<&+M^H z{<-NO3FsOlpDMdksu2^N2&Bkdwv8r&9wa)-mV_^_#f1a4E%`bWtM3vx_%v_*sw$uC zbz0d!h01KB(=};jOjZJjW(P)CZ-nlaRzAjKM-$vaexQnG36I_46}OdX$**N(myo;9 zM)J5C=foK-49hrf$8A_&nUu)xy4>0+u)%Ro<|RnjoM#fj;&GK31OWUj zxI|Qa_Y(VI!taRFx2=@HU~*vDXwQ{wem5&2_O0Xk5h1k9`QdZJ;G4D1MabQ3Rk!rr z?vrbq-(A4&n2)EH0$=$wl^1!=R(wl>SE}gqdZnB7dUW zk6y{6{#@zQQ=R*Ho}``H)G*G~{sJj>SkJk|!(;}@ny}!p{p><8IX)g&leOqP_$Te8 zr8kq-NT_sk=5v|A4J|sDnB+T9)pemyDk-IdeS;8uUeqD}kix@=Ka-O-q|oYmTgT7` zY;5nk@vi2VukCeM)NxuTwbRYD81+@L|4QS{To~>r@24IAWh3nw=+`%0uW_)3o8U4t zf#w#Va9S2OB)da@f+Y zLYo(%)E8Tzf4w>}#i5oxOUXjLWDC_lF-xv!qc&mjjWYpxUI#i!FF`&uVd~;~&mDjz zjdG^*1|~K>ybY@s_Luox{X`TZ?en|T_^X)t%NGBu65)K4UC|#>!p{b+=Q>1wS?Q*0 zS6n?${?4ml4K{di-lA*{6#ksfWR3{{$>B;gz07&2VOzU;``cV+ga2!tKW*`^f4T}n zA}gUoY%IkL5B)*(3A+Ko_||8Tkq3mY2$jJ(2izo&$A1cV0FnqazSX9>HQfcnwymO92l z`Ajn6*&hcNI5}?507P#nMs#LpKT7H}UC<7eR9$7iLD8z7>=g`RE>L_*rtU0~GM9fj zR~!mEWwI|MkbQOAK(2>snwDQF3=nHYo zOY@oi_#d2o`(Xhjor+{7YR>=+4qcItA|iQc)WPb0|G}z&m&m4$DO|S@_14bV&=1QQ z?Hf!3AICg5g{`Zb!*9Jva(0@a9;!tilP4=>r_6_jqq(!0=s|G*Xx$@BI?RDc{3|#r z<0PUabg^2w4-H6WU8LpQqi5Vjhl+1h=N@!n-n+<_B}>t1-9p)qkK;n0bc*R%{wfOZWQWPm_q^<6WaC~XY7PZlUoX4oE% z3J#iF;y9119utUrIUH1w*WhbE{22KCem4C#Ran326+vgBAX3!1WGpBC<+4u)rT0X zvexo*e;kvR6p3$&tr%lPIWV>vSZmeVz2~qaTuBg$NtZ!q0mS!<+byql>B3oN`H2?;@sG;LRyqdX30X9 zFq9W6_<>gq-YEwai!rXO*5Yo?ITGmHNY3=-Q!4$nmRh8TSad6;Q3IC-Y%%5KlpbC7 zT$Bz-TAooEALpDDDNy(=VMSHa<{}q}_C3=rvf>V)a~&(CSzosAYS_jR5)knRMaAV8 zyk`aY%-tX1D9@An=6Fi`Ju~BBda%Yb$9feuyoWU4gW&BrWl(#A+4ciYBC}`WjmXrS zX9p;D!;8^xF{^3=QyJGLSs)2PKV=6MUnNrGe}hJsc>1@wC~T875YGrN)P+r9<&jOq zWfJR-M}us4OXE4jpnU@!#ElMUmK1t!0Kc|b~$N77Vtt3+ksp4C%nTh&KO7zDev+f z20oT`eFczgQj}gce4|;@TyCy7IBpeWp9|U65&C5y7+PM<#~1@cyzRB=|MU^Y$dJe` zj|rvkw6?Uet<0>`770V=i|p(OFpcB~|20o&%yfGBD^E~}jac|woS+Q)ohD46bWMBa z{3w4tSP2)wXF;|cbX;;s)iBClg1fC;R?b=#5MT`T#Q&aaA~PyonmuRnppB z5IrHbX|uykM%PveUfmwMq;Iw2Yk-R|))H3^yM*Av_S%vL&LkBKK4|9OG#G|odIA7| zPN(`;5w7@8B8+BG_mjFVz6%>(cvq$oMRW&ls`|y2{p>I#NhyP6g&^zw4Dz4B>PFvrFn2@&c33z zueZ65`vHN=`6Y#^oDd=5hGbRPvKRz@DPbnU!jb!(+EJP}mB&M>k_Iin#0408wjbO8 zB1Udu5EMRpjc=FT7CCMv6r9<^ne@#2o+zVhDE}vP4lFAh9X*RqI+^)@j0SSb;QuH2 z_A6v0fq%`#-2--CU64~XT^bDEehU=i1Olv9u2h;5Q|HcyVYf;bPlnm>TGh_a#*kNC z_+MeW8QMDDhe<%~@PAX@77xfJ3n=@fLwL_qyiI<^MO_?>v*1+KGh77~=rg+RX)#!U zTtHeqADxRqfJ`Qh`M=uzsKMEj+m0OS88Lu&vAmAvsiYTV*``;^ce^FaX$l|}kwDtg z_;n$Xa_MKSBzxsGjKm>7{RyC8Pmrlq_0GdRd#pr`LctsS%){CAXcV&B63F;QV=hgF zbZ5YP*wL%VW4ittP+*gN{k-}6#LN3Dc*NRaT&*n9YBAU`(?u9xtjdIsUin~>^o0WQ?DF;prLr2T$1_6s}%FpGJ1{(4DWTXOS4tR z>_dz~QS7j!W(bj1z}>5do+~m@mDI98wE6bP$vh`Awmv8G#kgu->3qM?s(cL(HfDuSx+vYT$QG4z_mpM8k}J`cpD z4ep>!{i(MM6J*(?7FSFd{%LV}Z74XwYOrl%bG53e8`IdJJAoGhAzx2rF6 zJTw}CX4(oFcX5ALQWEKmEx33^Vi;6G>Eg6UC6)}qxmAI~aq^CTQ&QZlEwX>f(Z`}9 zB&01zz|mUsaXE%8XpIAbQDG3xxu{%9|ACCt&;fM5jy&!T!@HW0o|5!^^~;>xgBqK* zCj+mP6xAQ-xuZI^qHxw}T3<}-npo7s0D++m4azBNoz}MJF7JEL2fGPdl4gBqo+J=q zyJA0_SuU2&j||@Suhixo%G@4C`j+L=rr>^~Y+42}K|`$6o9DH%p+>{3V-PWN4TM9h zbYXRTSVwIjJx-?*-8e+5=Kc1pm6!$V@>QLu8G>UL((6Z$%cBXMBC{ZM`}GYoR|W)W zUAJyu01-8#YPe2xubhHNe!8@o;Xh1&c4Qq{FzH5#dOZ)3=O)UjcIM{2t0;gowaCtx z-u>d?-N+~)ltp0_x`PMhfF0OH0C8EX_`an401=J$%FCNAi`crFeHgEA!NuB^pIv~K z=~5$!r@gV(EeP*aFzLrJYb1YoFjG{izhBS+Y{b|74CUT3q&Oz zW`PT$DL9_70#nPOGix|GxI9scbiBc&9PebLDRqD_x=4Cx2o4<6>G(Hmss+|bvlP3{ zmo(LWY%e-8YRB^>*OcE9bkfqlI9^G7H-jwpd_X)tG)ryy@||m}=W(L-%*_ObP4S|J zJ{p7boV6{qp{3{i9h67b?7}Et#5lLAvjQ89!GqUQ-X`AoxP-rr`+Q~|0Dtu=?9p9s zl=Ar+W9y6?fz}^T*qcOx@WmtZ27iUK6K>mN+#+fPu7$y7(HrdPlWfzeRS=kq!X@i@IYYp2b6?328uZ(ne|6U*xI*6hMuqKPI)0a z_O5X?25lVOs1N?9V8-Wgu0l=6sbvaIX^J4yVL8X%i((<2AiWrJE`?U^73ozoFR07b z!1?36^+9(w9G*2`-)T9sAxnzQr9heyVMd}2zmDV^JfOc*0#IBh9L=SBgb)oSbVX{# zm;X+hdFi-Vm__~XTikC^eul84+;^YQ7{2pr6Oe3IPH~#UbRvhJUTCwkO<*QopRG3t zBSy&#UQbwqxJ~!^~mm_mRE z2ZXxCV!QMAqgBlzl6DsV+6wo(ex6dtE#SgY(KiP8)S15nK>r6XUA4kgsK^1LIkG8< z+b?)tQYSvtdAaRh=WVnojRi2T1@CXNN1|Cq`;-N8T9!z?h%OVN*4>Hhm^|g8`2qkK ze1EkaW`eT_D6K4o;L${pK#dKU7u=mR>KkHRIhF1{0>60W(>G5yzV<(pFfyz0bF4A6 ziyXhimd5q%&)@kh#v}?3n^s#cJg!@%jzC`rntnbjyt&+$(5@%4nFU9 zP^OW6f1IrPJRdmQ#^{u{0p_{xr({e<3^=ri(o?n;RA+nm)h@%bsW1f`Rmx=7W>TQX zOHa#<7T)9Cy`~xMJ65NYBSix66UN`V6pkj6XCN15iijFnkRJ}~{>ticA`C6|-$-ZW zsl4^zIe1ThtAn0YQDJdUIZwNa`==5 zSN$b74`0%EQopn;0)bA0b8uxy=W%DhXl?J-739aUBQo|23 z(gCa_vV1La807y>!>LW7dbSmx5+8ki3CV)-Xk>40L0J%oRs`KZ@#9h@@vnKSNAOc~ zGy&*lQYhOq|G^As)X;_gt_X#S&R8y{7VH;!@bQ=lC8`tRIIMA01NPW45)AViv>^>u+L{OR}JeB^slCY=Knw9 zR0Dg^EmvsX#|V`S!2U(nN%osZo%70RfJ_T4OR`@k1`BMC-G~?krL8EY>M}m4Qw&ys zQ`73}=b&efbREql(lso4wcQlIU0u|dQ@8d|zQ8k17gA5iq$gznrbv-o08i6*&7MHI zA+n5KPa0oZoNY=VnvZ7EH-`&Itz@}+3+K?0cqW(&t;u%V1Ad?7cbNW-!gCE{F3I7D zRp{@wUk%ZtiQn_YoH%ax32!ukyf^pa%aH;lqbE6&ixN00JD0W>s*wG z?!OlV0}2Vf`tK_=@5h@Gkn+#Usy|pdJSt+)?Yw5yGPo#=l{XH0YbG7y_zx(+1GLMT zRvC8BQMwk2s0}HV$g7#G5KQy7R|%Ho3X%R5?N>2vlfiFKRTh`1*)_bt$k8Z+`KH|M zeDZit7zVZwOtid7%Mq6dmCPe?Dw+f8Pspi@Mo<*AXck9TAy&psxlLTS;4G|cSkjT4 z=hu|T6ge}=jBHWhYpH)@H^RTQpU|Rsw79%6{_Z=8SC6cp8MLCLQA(}bJRr~+uTVoA znm8*#Yv$){D~%k2E3wZG3LNZ_&sWyBGDfQLjp>>LUK9#~?F9>c6-<=$nSOQCVqCsD zGJ*eZtNZOuALoPRQZ+o@FL*rTrH3K@t>EKynRx`F_o@aJ)ZP9~fXfpdmbIYO)&1t- z8mOVb=YL$5@tKWb>nyRrOnb5WH=PgYwZ_ZoC{FtV|ACSlPCr&*-@J;#&T4~di^(4BWXY_42y_q$KqUvW>_{W=%0znD#{`hI!1lfCk|9mHT+PlmF>G101a);EbiA3?#jxBrEa`+JE%bOCJv z|D!l_{8OAa(ghOp=bKG`#>Fit{dY(%ZT=$|{Ah)LeIlwOYjHc9@bH+WBkb5P{yZFA z<4FUrstw=P^#G0tN`~)2H{wF&SvP+F#a!GkyZRCU_>G3^th`_W@iRkg9+-!4+8|~n z$q+G%Kj84SJSZL~2Zh5Mxrn7WwPB>nLiuW`S2D2a#N1poweD4 zUD3j?kw5^rR6R@}Vi>wz!2v;nu_1IR(sH1ANWZXM&LZ0chI+R6s_*v+2*@jI=GP06 z4z@~;J`5l`KnpMYvVOlm9ZzdzCO42@+Wt5M>$r9&cHvaIV0qh&?8J{8>k6sroA8X} zG+A22HsdIvSkp`$EF_I#vkP;aZ1}fK3qh^3n(AlY(!LbgkN^Pas>>T8mzWt!0ZlLc zrp@ui73+E>y407#U38rvWY8k_BPNHB?k=IMponU<^I~In6Zmn_`cTX+WTQKe8CeE$a;HD|^TpR)*J$%rEd(vb$ zO0Gerh%b)9U0pJ1I#!RrQPY)tc8>Eu)dSGE1=Ba8wJn?y`?h=dGC7mt`H@rDZp1SC zj`w0djW%}6*@+K%orbQ?Vj3VCVE{ln{Ogz68MXmzH}td65zZ7QfWWc`Wk+slllxW2 zX&$AAqJR%0G>yH=yFFo>4{=_?0mHn`OLzDro#3jD0?@&5m@|9HDh7~}*4y~fve*WB ze;FwIQ9Xv+CfG^?*Tr$z%Q*7{eAe~Vc$Q*}P%P`q6zD=TYjSguB%(eP!6;g}RC#87 z<-JVLIw*AxQxRQrIS77www`BWp+DG7@_EZ|S@F4!`{e$;|odiP%)c@$(+A6}?bv*}M>l~t`D<6w@VC2F6H(p!if z?UDdOoVEPs;7XQ%v8e8|4>1$O9q-xV=9#UrQj<{0O3tIV63^H`jPjNN5M4 z5HuY(ujl>T`;vT&Oj<6DM#)*Wcpbp67j~3gT$o`;`vRI`;amqDw_4y{s!R0NFDiiy zJr-%VPNC-wsZe`z=e6M{?pK7knd>BeTr`FC<oVsv%l}92HzTW)^9VRBv#-jr$(rjp(q6|x5I52( z@BXTx=4eRxtM${crb5=`nivt3HIpQn+V_L zTq{r{|I*jRj_K^Db2~R^olo4o!@ z3jNph`=o8$x)2litau%?Z+w@7*O1~LpPZ2t3?9RV-M=5AZ=qg_Wm45>Oy7VJ&FGez z%v8Uq?ag?ummvyK9SMh0I$19%8bS!8RZZC&uD{t#l|0$m$b-|Gm5WF2T)`doNBiE3 z9;VO&b5$(r!;+&EtB)9 zt#g3ihgUMk)6}eXGgfAco|pilxYC5vv8`6gHrK{$9m(J}IyvO}qN-Z!g-UvegHbdF zd)i9h(aSoiM8;|+XTfBlPMg7{83z%7z^B*|E~>)ga=Wa%A2Imli(QCqB`%P@`kU%c zWlybylSeL&bNd`$U@?3bHhUg@4OYXK_Bg7ncGu?A%wRc-O6_@^wmh$|Ag$j;$>G*& z`-ba5>!V8eH+r%#-mWXlPo1)mYtL(N+o5xu;u{==ur(*S9 z|Fo0(BML>C{`zj`w^4hZsOV^nyDo%CP})+W>vGj{T+p<(@2AVogYCAW20YbU%gwfE~rsbRAz zagH|~B! zQ49XbB2@qIu{(U>eB;}x)>T&p#Z#-Fa#oJaPdB z-EYRGmS=ahkA@qjo|8H0!E`l}ehcQJ>|rxL8g+V2SPTOg-5q+TF>}&IgfOB#0I0ha zsPVrKx9VSaKAf7F-a6AgXZG~d-RE@kmn|AL3L6+-$S(B!I^E%7R{}gn9Hu>Nto?-l`Zb6s`UU&UiE}d@M zysV|(Uxr%+&F|xxpXa(?>1!!kwLYnnTEy+8xU^?DP)J-kA=M5C!ps{=CZku>m z$*UFQVjx{@{s?uXfi$|~zE4kQeow&S?>sHM+g0G<{hyi5?W&@;W-LMwnR5}lA)Suwd+sA4R%gp{hS%7sflUx;>;6WP?jn&U)A_eNE`5a<+ zx9MQ|`ce#}&V0QvG#9iDvh)2~ejS;{Hpu7j@sME@?jn6D^7(<;CM+nTKQxfKt&Rm7 zhMaR=jpstoUj)D4{%xhLtu6TTa%l=ErBrH=RWM>Y`7pKgG~i(x3f}V1ZkmTH#fxsB&aMH<*36?)2NtGu!E$UAgH<7~QLoO( z=JD_m_w76lUq&~XNV_f*wo-)gr^8bRPi-iVUHk1~CrTy4iSi#N#ZGQ7qsqOb+FQ}J zJQcJpt`W&^(muUV=ZbK1R0LcLReAu_2aUxcXXQ1PMV3VQwn1CnFMnTU-P;p%kIPGk zQw4H3DCQcxWA#RI1?0)BikH1RA z-kACn&uI}0UNlBqzEp|{ncb1V&0put`$F`nr+Iuyh4*4uP};)VUVV_pN6)+c*yclb zfIu+W`knH1B1*5XH>4m?E$;bhZx`E#DAv1kBf2IgZPv1<`hflHVIIm>mxu-E_ShS2 z#%Fxm&6E`TXuA%@n8VK5jHDGi%y4BHI7VFhSi?-pEN*(lQF8WjJM9%n2%vCV6pRH? zuD)E}fnqga!qnCUo#xxB$-M7$$gGHSur=!+TERfBj@#aVJp54Er=DPJeddny3dIn- zcF1so%|2DuZ$n$tfPt<~*YQaP>`<2FTT|^`i#i{oRdONO0U1WTNZ&Fz>p(#QBjahr!6S@;k?Nd28&;j9X1t zowXwF6dhIdQ{nQn@sR>$qjz&)437<>ebEf)+~aFUKqK*PzyaHW*Srg&7`X+N~uF2e~fT4%MigzuRv+?tI}={D$`)ZtG?GA34Z@2H{f}(!`C_HQ%6J?CtZF3 z8vF-b>jkS{_)!BPQ+;dEJ?4E2UcGA~;$Ja8{ECDxgHet_N7Qg#2Lg)lv9a|*6L%Ou zBqt}Z6G#2T_~`ghnwg4lr4By9>iHC;1TQ{ZwfBy)6sqY9Z?0CT?slw6_gWv!8tl?I=_lvA9B3~$5K z8gPSA)S%l-fhM&M^i|KFoi4J$#s`*@TOXU&i$uoO9F-23UL97-GJwz%6p-3^mvr

QjB51&=*1mHNkTgNTW!5WFgi)PeC$UW>zGuG?0c{t1*;bv}3TsW+ ztP$C#Y5e;WSv?)F?QmS^kus5AJgCas=Zex)&nu1%^6#7H%Ht5WG1kcRJ=&xP_wTcn zh&QMZ<&McFB%a^O9_QnOz4wEbJeD|>&immBhlb>^jCX&#2AKcQKX)_=K- zdJVvia(H)a`BeZQjNj4X`mCdT8t;-a`xT53K-$Jh!^NA1+)zmSa=92dff{OOA77!C zxdh9DP>a`Qlkk?5Nc`?GUbq19^)PywfANxKP>7kZtVrt5T99N!!;%!)3h3Bu*&)do#1gQUSOjysZm!uPuP zt45xBGE3~ryb_Go)|3gux%oKI(Kr` z*2iQLr%iWzX0@m4s*#T+#et-k;QOyK8>KXg0L*l}m*Q8Uk+Lkl(Bam`$54IOY~qa_ zD^WrI?=^OY$OCYsuaS|Rh@c3jz= z##?zVvf5MC!S32lj7P4SmA;8v#$`M+*4O(y3hMT&nT=>Cb>nXev%ODf+3sRw&b@TU z(R*g=$aFf5PsJ6?C5)TD%gLJWUrijnZF?b3Goyx-aE-EcwQC91E&2I&39)RP+o*iT32hwDvHcck1;v<%1?t9nempy$Do*lGD{$nHL>B zTgH*?!qKpbzmSX%sA>B_foIb zE~@$tKV>@p@FCTnol$SJ6-iFj0E9jr?jazd$D(|S4-1h9AT1~F^203Ea7$~r@HOa$ za1sLIWU@e-UnY?N!s=7Dqcw?Ob-WMBMBbb6Mz7uRdskF|`AI9|8lUU)wFx5L?_7IZ zjf~*MX|D!DSikb^pFJewrzk$1rIFakfS`!upi5U6JOJ%?&(>ys&rfXf2r#B&7d>~e z9i2?{prhW##(J{ru=2XIF5~bLQIx&2byBUyyMp5*NVTPLbfou6^=wIx*%MK9%h4UnOacYt`x zX$mw1>Ks3=NT7EP9As4`qCJyd%bgtl_QrSBH|Ma7;Ji@2en*41A&=b>`L*}|F;tM8Tk}xeI?S7_ODuYS^&SZ~R1uE|Cn#LL z%u9d})JGlu=Z_>V_nY-8e8tC~0k0D-mnqXPL}zt;?NRDK!!`NBiPN*^C`@f*8l1%_ zNLQ3!X$?OA*-mkbc5~V^y(pHiGhC)7ifYD)Qryi>70pXYcdYH|6XW*Bg+_M zLoP?Jk0y-X7dvXhXjlgy%iPF$;g4s;X_tlg%xnJgvj&c@->pV)QMP07VGwZtE$Irx zE4c+bE-=!jNWEK_O+3b;<75S0wfeQ_peIENq5b5zFFzu}txLF%t58%!c;TFU%fA=m zFORr;#8_A+U8Zb!n~sc8VHvKeNu}~SPgPLs{aP5IC!3wj0IIcvY#L3B0bSLq*)X(lAcISh z&$VEmDtEMwFvBCS`wL?gEaC506t|;~`kP6H)PU-1P3_dU()wa;kyMKwr6YB7kIY-cokjk>u`FP#3;+UD` zjs0*z((&}(R1X=-YM!Y#^|BF)x}O8Cz&sgONqu0{Z#Fn!s@KyKq6~pF;>?`&6Uhmu z7E$>m6~i$Rx)qdLUSUhKdM!GtoEhI!P;iD^eh^sDNx#L-_ zItonOUI$XA{b^5RVeCVHk@%uT5}x=wKz-lH|>~nSx zzAS*UXJ`oIOsztX%?-?5Ga9=RB9lRBLB1!}Z)4F8hVD6BB z&acuEpPt9FNE=+|MfZsk$58!4_zL&l(`{%78dHuk8KKM&U?`N2{{R>S5oR?ZtMHI3TaSr0lX)-2ecXQdL6nhpwh`I95j`0`O8VHm-? z)cQh>x}bG415spAy_$-nB432f$$@#lUDHLXwZW^|8r%#UP3k>XIs1{H5c=Wv3sYLmy#ZUdRxX7r+o~@Odli_9 zd46V0B-xyvEM>R*orGESx;xNP)BA!_?+&7iiv$quH90%Grp#IEwBPG%Dk}-3era-w zkRRivsq=@M6Q1t21e!y4h$7ddcu635v3qO{(P)~#HEM*EUE8Pt_Df!478pv7f)G(u z&V_)3=Si-_6d>1=;oeV?(*`wn!a=ha0u4c=H9LdB7sP78G)u^7ytYq-jlSl>YL5s{Db)PC z)R8eg2ApfBbBw|W(wqr?#a11}XjIZz0B4nXWOB=0y@v>D#rCyRu}GD_5cD|muz@2Q z-PhUZn-pUwE=Io(Y0nMHoNE z&cYs_O?Peu?u3_EW{0f>-4yLwf1Bko|3{{+Py2)>{L|^nazal_deOJS5l#-Pl zu}J9gOL?}t`qX!n-go~iA~mN=Jl=){U}d7VZrUw zF%JD=Bstl8$=z`@Q0Ic}%)$D%kn*czO4Df?li=Xe4j;rJMW57pCvQH1nd!R?eMGCT zq=MW+UVR!!AzkAQ=S|+wM$6mcwAyekWTcw zcmAASxVx&2!to^zBJ(GMmc?TC4BxdH-jNRJ;&Jc zO0rygR|Q3N>4~bWt|~_0oiL{+&?=RxY|Y$mu`WFdYN-Fq(*MV)`R8b#dzl*9)T2#; zkTNHYS(>{a$l8*T>SvMtO4FE=(hd%ygi1K@!-Ao$3<3*{w>ya^?}=y~eE$~kJEVbr zAZA58k78I@Sd`HrE1rM+-gqpu+S5@&s=c7K%m;aKBwWk@+2Pwx{jflxW9I7E`4G9Q)Y|7LEKU zSTUEYL`E7cB+Y=M0#I#b1q=UHQQw?9f^W%c7li+Y_}ZTOO(EQg^mNswV8z0KMIKA| z_OYdF{VF%8O;X0vV=T)$&Ea0Mi)o+iGqP;Ia&0L&$LETM+OzDLQ~t_|n*lpwmce~l zKcE&sJfVzf$86|`x1FE zecY_az-fQ1(wtbMtwwKjH=0p@Y1)h1u!xGB-7Zis0C6#*LsNCw8atIFRjV1_=l5u> z5CpbEMp5#977sgNlXl@MbG_98QtU#fX0BesOnB*!4rInIE-89DK6iz85VkySs5~(H zfM+`9e$CD#_t_cg;YlM(iSSGT)mYYf%?e& zNH{&jrQ7N$cr?3ay5}EGtw*2%$Q7d6o<7xo6@~pH<5(w4D~`MSz1FxF`6r&97*m~s z9a2k{DkE%MvPmauq`S5Zk9|yZ1G#9~d?oKVZYn6d^Ks{;d9mN9tg!tH18>2U;K0wT z_X70WV;Q3F&A&Z%bo;njQhmnB?C@ecJ72r=n=JsD1%uk+F&*DuDaGCMb-)Oxzg}4j zD+!1TWBZ_#6;_X9bwOG+*mD_#B+mGWrn;e!!Wgik z8~y#nYCKs#;P3N34U9r@Iy#~8>Wxc~#o<|TG7gUWnGG_$s@BozYDu}-TN$_vh{J2auVSVS2{R#$khphU-!cf5}acWeIL@^0= zy2pq-GTL8F3IhU+uT*asy+&8rd^A^+XA0Ao`!-WkZArJxx-$xz~ED7_k~I^X2Q_3pRt#(A1?f;O#5h-RO5ZGyq(W`b?ue8+II8^yfsP7vIJ2 z?It5RoupPKR7&J#8_?-n+Z4dZiZbmvCE{02K4&{#=}8@ve)cKvDSadld-SqgxZ{gBihXiCqeo!48LoTw+a{)_z& zqe07NY9&0Tq&4Y>cfy!dvI=8=n$B7Hi`~l;U2NCW=7dyqJ~fS=5ch4}>Nxo5+9{x* zce+;9JJuDjgb+nLmu23Y{%rYzf=n#-I$WueQystl-v-gcFif$x&slyn$ zg@urVh}+P%eV;?KILG%gfJG3a*IZ_F-Q@B^;c1#msZe>jXT%1aKFdGYI2YYWu58x5 z)DeLQ3+4M|zG7!Y1n@H$wrX@ank~*tpK#4&ExCJU^7UbHSnRd~4ugEV&_uWb+`L%a zS|cUaqO=>nIJWX{`k$EdsD(d;TGo0!2z z(KOZHY|h%hZ0oqdwU3L!(%gUl`=@D$?BL@-?<7SuaJ)yWHY%)W)y~JihDQd!ZH8&8 zOBc?D)CdgFFb@yYnN(Po&n(L->SQMCex5ip-hetV?=n__O>=u7imVs$nB z@YeBl2S(bnhj$bFwO!Vq!4=2s@5H!zV*IbTdq}cK@GdU>ulC#Q<#IpOgoi3B=4)Ez zNuGf!NVsgop%WnBk@3c#iR0(etLKvyEJ8^wG5Nfnv%GLP_HVuldV~}-N;PUuLOGPP z-YEI~{jI$4yWLw1W#xZ(v>=BO0kMlkjI-R9jxMm2i(tVYG^=M#cMEdYHO|fCOk+dC zsY6!g^t081(YH*06EaVXM70VwZ61l}+KMsqsQ4=|lQ_9{GHfa9>e^3W%G%RCD_jq- z@l9sWpy|AqkIAj!NsuFRU-izfdh%lat2!^%zo>{$m@=w{4*1bLp@Yf5bs-1SuM>iU z#Tl=s!uq#o-8Zwiz_W}s|6us1WND-3Xu(O->)UUS_f-P=Ol!lPb@r+Nr_Dn&Qu^0% zuFZ8z5$A9B>9s%*fsgmRlx`(Z6Zio^@D;^4ycs>DuS7y0tTy@My0f1v=a<9*h$xH9 zi}}FN==ro_Vy5Ku;YZTc9d=)8N1NmG+Vb69jR3iiE8O_6zI~TpzWE>w1qx^h1&wqv zz6PyZTJ7}mG#({skR>O2TvAFLwV3)i;mXTXTRvt`L*>`v(uG3H z3PTi$A_$7VVa)#Tg!Mq=t;PJEi}==xA_bEEYbj7Y|F!>{_(=4Ht>*2Z6>BP7=VStU9S*Uo_KeF9d9i5K=LnF&2=eoJ*8ytLG z$iY-ru?3_4zX$kG^0}T6;JMZ;8dsrs28mKg2>j#XaSkc?x9RkTsOi5Sy(pSPL`^>) z{$p_R_3LDy!+)%I(!q&%C+CJDbjSV2^XqwoZ((ThJ4sK>$Cz*MEHycNWbx!lLlzl} zUZ{iyMWWA?gd=NuORf}p%CjKW2Cv5$x5v85vM7d%IiHWWPsXy;zIsS`HTXA^oYB+T zNIqT0cDG}h4kO-=JO1N!D~0#5yjT&%Cbi3{!1FHKd}$OipE%B+dg+ems$Z=3FcgO2 z0CDu?l`Lm+NuzD8%~MprINl^&%|$F}e&cA$Ly?qPEB49Cn2Z;2Foh4GA|oSN^x{>Z z7L>3&1fw1s_R%VMy0cuF)F~A&<=p`^RNw<-V%yVgXJoHecaJB&WC=jBXJjTGSG+J` zWZhWos*W_nhQs(?MO$k!THmnoZl6y0rt{set&T1qZu-D-x!;pcgqHDMOgk>pyDUwbL(a)8UTC@M<`+qk^Ol(5PgJMpiy^|n7mqJa+5IEn zTTavM!AML5!Kzbi2Tzo|^PkG#TO$4mvdBZuA(3cgfUpJ*Uh=RpT;#~M(;{l_;qL_o zo;A9qCaynVeAZo&%QBn5q&3~OCVrNmHO6zRO|am0*T;!FX->f8TmGnM#>>Z;AJhsR z%R`BPIt;hlkLs4^wnc**7w+Egb8jAobR85(5VUGr1hS(L=wHV5OAJlP6f5#e3qYo{ zX!fcTk$iRZ2f1o)u|jy34?ObC#Q#h{x>AqPjJVF+IZI59e?Fq-jb+M0rdLpyWAtLjnwm+i^S8b_G33e4}-@6gi(zJ2Z|tjPZWw7@|5z{rv|j=*tKQRbwyiQknPhd&E?ehOs<1j%@D820DoaF4;`_-2pQSQN6CMWY_Fc>p{ zC4KF8HEiBFzHWLl#kbjSrtkb{xrDUJEk2Xy`i(H-?MYjtzpFcGkkiv9=Jp&>&}%9l?(Ci!i5rG(D_K-_5u&Eh>1nb3FrL}Vgvb!t?=_p6U@*fOR=m-_!;2RoO7Z&!PYZAXT&`>jur$T*aSq0RaF+sw;bAk&jb$=A}*aL+osm zhVNW;kSGA3x^jQ*me~ad-5l@xAHVzX*a=G}E$!<|*RS@0whjoq_!|EHib_CD937b) z8+RviVYvL6+BU#kZL1bBBk=BD(H$-jk%FmRhmAfLvYYr`?ztQhDiA(Aq#QZz9?hK>w6; zs(J96hb{agwv(U<``y$UN6qLX(~iMl@T42bO#|&>s$fTO7NY2ZHZ`u0Q0wbm--$Az zVsHC=oqp&c=Gy?=kQ8h(_-xXiO$WXylrrb+y3XsQB|-d@_oC|#lhbw7Q25x&ZD0KV z6i#t|HeQjX?qorJzRVPk2o{dGAu{Eg@;LxXCQym1(9@T{+D{f#R^D9g%A%m41a!01 z{-=&WvH7JMEc)~e3_KZiF0qxc!oR_8!NKAW4<4Gs|MfgkR$BT-&iI-Bd@b^y;+lMw zq{4gmf9Tc!r_%kOi6RrA;Bs1K=0s?y{-T1cc>m{J_R6>9hCy$0b#w^IgWcIWvQLt- zgv#Q?`C9AKrxLaGHU)ejfBR&K#>87p!R7p=*MI)icSU4Z0?NBQ(wJIdg^}8MJ?`7h z5-s`G59@&it9kfJ#{y_KjfXSY)93nc(Xv?mcWf@H>~rl}O2%lznZ}w5F)uG!u>p;6 zD|X757njFxwqla%o}j^q-q^Z_J;BG@6Z6^HKs<%(f1qNWcMrcqCgq;{+wVyZj?D<{ z_2NHYvPMz0<3B(yhw5W-dfoemnWmT6JLl5A9a22Go?*)$@XmazI$JLcSIov&lH*^O z&+z7CUHkXySr5v{q?1fRf9ML{|Ipi=w5Al-8ZKyY+bwJ(;^F^XC90y){n;`^!egR3 z8|l4IiaJt+r@A0wEP;8&IIRL|m+zh}5qtyd$wbcjH3V@qXH8d$#C_64+OR}XC(<`n zSK?0@v6ibV@xh{2DkAx~xAW%x!-WPJ!#$sRb+3fiA`*e{elVhKEAtV1U)jZYQ5er6 zt^m_Wb}tQjts0#y5^vkKTzk~sD%8g!L7*Ud`6IA-c=IDY5IR1~f#GBkSERSzPL(pf zwoj9RudP_L5Jgz?!R6wlS2aF;Fn^ZRcxJZ3EvMr(j=`v+*@={Wd{87%>m??u5V0PTDd5AwTn12;&q9bIU0aP) z1SpBu*pULA7?|mr-=MyU_4I6SMUMF9gZ_Lr%df1~ftn0JFYCWgK1cG+OlcC}(skaI zoKX9)g#i6pj=tpUVPQ5`azVR4H%vETXDo%x9gDL27TUJF`F=ZZ384%G3FI&o&;b(2 z{VaCfjP5i$hDQXF9UPzwkobN$#c%O+^o$<@BlZV47`!f~$6q)-rUSjxhcpZ#BV~)^ zI(jm0ANU!H$uhR;U(ga67+7e&2o$TTGQ(H*&vbwm^@zA>Xl2zQ)ZLs}-WQXqDhL2S zTI1Tmt4YW7e1CFem<8sp%p;#=GZP(RA74+Im?(te#Rk|&*;EDIbcw`|@E#Ic4rx&) z>}(g4jIw*7KUBP%x(w|LagC@`KQq+CeRw}WeN~361s5t7Hqy_T$3?Xf)P&bA9{5kAG*U!?CFKF5(;kL1Op1vgD){&y!(iE4`Db4kPtWN2*MxQY?~T2h1Yq z!tiW+Fe|UO^z|WQV|l0vg&BUV(GbQ$;L!Z5>di}m+6N54#R_*w@Hqh^x&N#cWc8rb ztIJ~;7NC}%BGWSZbUfSv=5dW?ZzSSd2aQ2T@(kTQiSuasikxBtTCXLkSzVSdgBrj+xHM@*%9f5mq9_ojs{%`Mc?wlS}4mT0k5^OQ7~ z?gs?qMuvz}@G6&lPp8V5V#iO|57^F1#IgC=f?G{+nB?1QotC4CHdCckzolW9`uqo%Mo^48?ClYQE&ef6$Pp{C$YM+TD1U6LJxr4{ry!kqHr84 z0$`F;eAg7kM0i({>{amb+AD7N(}(8cl`&tFio2jS{<^cZ%PdnL8kRKRicEZW+kdi& ziNv;0>C@ui?WRl>ek}OJQctc3!ziTt2`2?VMTJusQk1cA)dCkbA|qq5fe8TA{r$_H zZg6qGQp|iVpmM$&x~Nm zGPd&h^H4cu!F6DB6)!%^oSQzY+gBP&7xkIP55xtQ@KSK!p1L4nw9KipfL`;3h|i?y zIZ(M@v+@K%?T`aV2?Etxm+RTgknGfa(^!R)P)BtQeTd+jlA4^KX|vDU9);aks5zn` zC)mSw%i`rMp+vb~eOAl_+n?REL65kujG7voHa;ZayY<$;rN?XA=*iOj@N4ON<2Gk@ z$J;u*jwtGTPX)0(YoV4=HPWwk`k&dxnB;#dWnb_q4lXrC1nUv26%}iA#i3&P;c2Kg zcrJtv%gKD0LKTY*@6~0DcEMp^FOkyDKond5s}o=NSHH-9S&giru`H#Hy-R?D3k)MA zzI{fC|6Dsb;V+Kc;qf9uJst&ipgYn15bc8X&mgCNv-~lVbK>GquF#9W-#pbL8-;v( zYFc?LLj@UT001y$(qBbXk&^YcLrJ3fo()EJ2-$R_2oI;+3W9SFd8@mak)xSgK8Pbm zt!1u^kN?%w{+3RJtMxuJ<}_;IF$McEQ_pexHo}3-)|FeO_&u->7&CtfZnqIuVT+CE z?EFNS>T7b%Vsc8LT#pPDDrQ>24zvD|J4m67HD3Ahc_IKp!N*A*X%1mU?HW0V$IJ6) zxt$<*S!gX_)=q*z59^hiPieffYlvJ*Tf`($DCJx0+%^@93cZ(I+gkBcA8o?p-|Img zrq(~qv;TvA>%x+O8ro7ik!s7q?2)rN{YQ$<2AaY2jZuXdj=5 z?U86L=G?+-NXQmVORdr>xkbnp&B%pHy0|l?8;+Lq}E>ga# zc&=Z@4f&fu`r^m8hZch0zo(?CkSD6BXc#wJH++4&O4N{XQk|LI)pEHWo*2Sr!eVT$I}0dasO|Z6v2OTyqWKT zZ@o|i{Vyo}hIZf1F-8Ax;>`r2AXY7b?7RO^CukD;m5=oQzvy^{%|E5YE@BIGeXBqU OkQP_?S}kf2@c#jkfo$^t literal 74851 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 z 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 OkQP_?S}kf2@c#jkfo$^t literal 74851 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 z z_@g#Q6CpSmU_qHzfeB8zXJw9q?DZB33asGzK(qqaZuR9WB&^{&bYSZS$#>iRQ?DZG z9ZlprD%Cf!zaL5WPPP?dSL7vr*e=Ldsi;WIpROCtZ#WN4Nxv!}k_b)|LqESn;5IPf z(PVsXJlI%ST525NpKvO2ur7kv6vO!^<}zMY);QpQ%zM+_v9=`k=FL4gAv54F6wFpf z2yyqe&ZW%NW%A*Y_V_*~BogqX;gAXysUVN>`4;eFT6T2aYqTgPK5=c%C?a$03Ml)T2IT4J&%km%>16gQBwVZ&*vBitrsr*2~3M)rmnZk zq_L9DqlBtjo|GelBJHvT2XU~9S^&AFw+ug?sk}VPB*c8S#=*Z^Ol)9AWZq-LN+ym` z;`1px;{&yo^*&dbSmx%NnlgU-)t4%7EZ!HrF%}^|d4PVtFMNJWq1I^lwB8H#rA`r* z*EKROUqw8g!|E8n3R&(gdV2%MhJMX9TVQa5)&$9lNMc_9IKrn>>a0^={xy>Rb)6uV z9=(XHE#VbRS#bAYc6c!S39EK>K$?vm{MT#qoq=Qq{16hJ#f|Kq&sUR}xc=CbInY)* zf|tAEdsiVZbqk5o&J_72vgNjvm^;FsXQ&UjkzPAHyxf-M!sMHsg_a#@$08 zYt5>CA((#s$3d!*h$|eV4wTM*KYx=yVK^O}kXO5N$mw)2?Q3t`^FB}J<-V@()`T}C z&HF6R4a~00Rc|z;-Ql&Qa-pDyNBf+T_=!5rjnoHtuYf~f2$+2g{ao-kJ>&r|SiHkC ztmiMYNI+n)6xB#MlIt=+P-QcjPGM7JsZemPxw1mo8Rp<^cov+c=MF?<-))5MT^e?( zMSC#?j9bk(te-C^7upOqq42FaWjW1YvpEXXSPQ!^?U8=5(Px-Prju)wnXGL{hzfIT zEG!Jfg^BFS4FrT8VKM(rB{0$6n#m=+gx4LZX6tvW6!wDVMIz9g&MOw#B$1OH5^Ya=n}!8Y{IB=b6wQ6CVHFh)xk=@vMyEgBPOuU z_G=!#_d8VzytKH><(`cDv8-WLHV{yBGn`&UUuS!-Opkb-@<;O|_#(bZY3=jiFZ4R+ z1CCt0EXPKrkzy_jlKx}bpw1X3dR(uo547b1%^BQ87gL0z1TgQV4%rV^UKpuNK9pmzTB^9k>csYV9A7oqCmHe>nI;Cg%6+R=U1l>Pswd#M4r# zZ(FP1<=sVzj%MHV$3na}v%Zf{c4j*2MgH5V z>r3)e&T(WQ;W3lUD}BlbVn5OT#MIRy$1gBSu#6?nb61}xwxX|=O#ey_T)|!vM@fA8 zlr>anhftftc6;!Q^)>+%XlWBQX)0oK!BG>D7vAq8`ze0a4^?6+ZpvQ|BO48)hV*v zcfx@N2|eGZ{bPA419wcI+B$MyW9by#zxzWl8K_Xb;s@9ir6yy?riAX;i0f0gGs)xP zGaw^iyMHp`eIj|QQ>0zfU9mHI@a|zInHMhjiolfl`tR># zz1DjRzGzUD(%FE#E>$E7!yw_^?{s|`<_^1^#OmOG<`N}L&)T?oc6W_{PH|e{8Uq$MjtsJhF*qzNkh+Mmor*kO` z?fR5e@#~-KWDPY*8oROC8z=UFpD*Bh;I_UG|K}EtOLqh1J-R(>?N6{UA6(zv6&2f8 zpiWZToy|7LORxz>$OHt`_YXhaOp?Pmk+}OrT|Y6>fos+)aC zEd_X%9Ila#0tpfNC6?ZT$Wfv?WFq5ACCo*{n;Av4eUKI zbAb)0NLFc8EJq=+A?fM|*$)2L1guK;ad5&utpWKf%lQL6o${C%mB_O~owCFi zK4@hoh=*{_gBITlv*tI;%l+^VaX2Wh`tD6NWv2Y@H)=Tw2`#-_BPi8g*U$JJ)(i(4 z5y2Gvk)W%a!~TRwGK{S2?_)wfH_lifxQqho&M&)Tnb4ZPqF!%J*Sw-PGRh+$cuN6< z2kyxZ)=LBtdUZ>@ z6sZU*36wvjr8#4D2d~Vw9a;4n-zMOF{myG9iNeNa-QEEoi_NDeO-0!Q)Int>bHI1NG*V;Jq(&Atdi&Svzml(155m4`}**N!OcdB$GSB(uqb#<1ls%{0plDbxY{P^)V@i!1#TidL6A8bH( zXo%_8dqjR>=cx^#QI9*$qy8onvsHg#zJY*2y{X=^y^#M)M3$$_UVyOOH@k?4!kKc0 zE@$}qZRe@Q#AUktOxqs5Y@!}`q~M^S$9@LyE4BG+o%g>R5RnPEhJXKl&1Ex$_a(hM z1|5?br8YJoAWO#mT1GgSiRu{Vae-FcpIRr4Drc+|!E$;KI7Jt~NAYPRNP=CBM3s~T zlZk*SdkS~VVz!RZ~hkv zPS=~_S0_7C6lhZLQc&e>HbuSdsrLkr* zk57PZ%5>Rd?Hn8wzrQ_LC-Ovo3_5}YxR-h=w0!Y@rYoju90b?q8N({)Co#c-Dc}-3rP9br<@ih>q^=A3)wVz6T7UDbj8*FEU*} z+0uUvk7bD(y_Q}ODfiqUug7?H<56P}&iB*iUhBKzMk!=vMcK-5c98ToP~s#>w+IY) zXP)IEM>8iHpItA58)2$d>4<)NivsFO@EIFeiQ5R>L+I@Tx~Q)}Vw_`W$n`1n{iYbv zqHwk}CO;q(X|p~#lo{yqUS1O5Xrq4?4-t6cU*b~QI6c-EGKmkfyF50w->6*c;CHBY zIcKGdYQBWqVIu6=AICAvDp|K)IlZM6>FpfSqxZmG93n$VottvrT04BOp#FN>Alu1<{?>B_$iqnHhb1%fde2i}8z;e+`^ ze$D=rPV_%bT|}-OA*5B7nKsBGO{--bW33;Rg$HR=SRm=4y)|NuTsN< z?%I9*6;kq4xT9w)oi7NOBuke6are;{?y2J9N~f)dkRDm@jDP&CyQ0D?@piSs)Yw>ha3g4HjP2BG zeb?>rDSJ^szzw|8YU*2Y8l^^=u@MOHc4tWc9&+gIXadKD5z7X&B=Cv43kbj}Y1Wz3 z>IjstfY}_YT`Ns$)RIe!i@)w(Ib;1SW$$nR@PAp&^r&b@T}@)rv0DP*3IbbuY-vXB z@=k;Q_#9;UiGoD(`s;;-vDj-mRB*s4;!j?Ky8up5^l$fYB>1Mc+6-5myhHlIN_5l* zCY+-W6^7~>8j<~G!vDCXRWoKh@32p!`K<&4aj*LRj*xA-V6%3{JHZBR_N~v-ZmIjg zQMmKSqem_k*{+Srj)mInea0g(nG4+2@7;d{{_|}QCT`?^p&_XTP5(rA}{Ozrjuv$8@%UhHdgzna})y;o!Gdqpb;$-94MAdxSgAuV0 zf}S~muvjLJ)fW&6+;;cU3i>y2$?;pmGg>cKx?^&!s{l&z3nBUoB1v@2!;b#(XL<|E zY@ zHe8>L-0Zn%)Hrg`%Em4ZXNy8QBd9$h*(?A{j|(Q^wT}6FrkvjVpfSz^^3O7BCrX_h z0Jjo2sjdKu@O%OfU0itY{kC7p&a7)PBAf?w zgEbBF3Dn`fFJ%TSOs=Y(t5ed|(g9i9F_PXyI5n+ITu1X%3_ug{fEjv6+W4u{(a)?` zg7HOZjWN7Ogl)m|TBr0(E`i4vOE=-BKkTta5dUvjh zGODW$y!c=0{P%eW-c(n6;A&C_SL}=@?dVi`($?(4k1rQrV3Kk$9=XETay9z|01+Vg zz&rjm2NI93#{CKSZIFtgGn@D(5tIim0yZDU^~I6yPGwuFP{Jp#Y{m?}I=AI@jex~` z?%objmwC@*ZQ;)oB@RE*j7JXJ%Prt2vd{W94sT!n#M#f*7h?lv(}IGcb&p z{(9Li8A^S&Ds3W{Vp(LmaEnT|Jsu@c(Q0eg)zL1W1bBpkfXBrrC}?18>}x;-;rgW( zN5)^O+}k0j;ei>2uBfQ^^XHkNjj5FSZxRA9T(%ynII6qNYuy5l70MkpjE#*Y3k_Z0 zZ}9ktg|&2bx>Kl87y9h?yzx#Bmh00)t~g477|8hT-*eN%bMK6oc??kGj~9l7gm7~g zsF}{3k-wGL%a*STeY-dPu<7Q(U_QVwAL{Li0C6lPVGl_s5wV(aI3YNQ$(D2tnm5PgLe zO`sj*0iMIcLiRVw*yr~!3P^vyg5&mJYSR~pTn?Ia_#hsFf_Kq&jvSA6yOMZgFqk@y(XRDOkmE_{bvg~%l&A_5X=BH{k&RvgJS)qEk|^xG zX}~#ym<%L5ehqHz=$J9w&|}`C>giFHHSW_2LS9}qNo-0Cnt6T*2)KD3G+?mX^veke z3wM1oLT$>TY`i0fw6eP%E~1Dvj|K4Cf8P^yrb9nDm~ZGQ+){pod1%`F^KZHJNGjN% z&?~!V%_@v#7UvZC6Zx$+^a{2>N78<+yX#LxCd%d%f|`W;7Y@xl`c#Pnp~ZyF^Wscu zKLdRuBXPKi{4Wxc7SN=}o`46Ne0#F}737v~5K-Q-3<-Q@7*b&rpyV5RDr($IV;WiIa_HOfe7+POu^w1)}oIyIsdh7 zxsbrsBf2Q`FC+#|M<4@~{z(J=?zC$%N_v}7t%w=&?AbHWym`+S=$Cw~qOG`_w=zT} z(Ft;5;3s#)BoV|FNDT$}l#!mf>3}u7{{D6Fq2w~%T+!KAo2X52@w8>M3B zf?-_^9_Tm6vdgbfQ6FNr%JDninr#?9qR?bEiryebiSViBqCKw1AVO`UryqhK*4K)bB;G_hzCzrEo?BD2ze4 z4)sDdBR*DTNS>NoE|f-;EI>PE)7D>kH@_LTnQR&l+2LUCby7^@>>{ha{oB*S)OEd! z<1^eb1OLgt+7-2m=s?`|^iDY(o*e2gU)TYwGE||Z_|wQRGNvz)+v0~+?;Wl({&GnM z@f@zg5t)~WDm*`Cd=1V3vj$fhpr3K?Wf-^?@Y3&2 zp|>(60N`#Y-dYo;KiS(9B{dWYhJ0;baQ#{o4b4*r0a3D?=Cmqeq7w z!roBpg+{E`J?njwwPq>&z9yGHVjw_U;co4LKhLSgX=T4yg{5)oFFyZq>00KzR!So# z(iHo9rq@LcGHx~bsEO33K4Ld0>@K3KS2v>=IzNZgYE@d&(9pc+=KB8TK|^n`#aQ8O zn?3n-h4FYYB_NhVXaT@VNnyDg;)(}v9{DM@LI)7f4+4f@10qnAAqS5L{v($vq#~_X4^fMQ>9C*(Q&{Gk7QlW5S%c+&i?Uq6BM%& zr;{Sz_+sFJ!0ZA&iqGSWSn_fF^R1V! zN`ONP!A=p)a?DfW<6q;>J-u+H7qPq<`-2M%v45_=|0T+T6c#YKfQnA>rMmj@$KM(= z_5FaQogGB@J{L+22D~UhdDX1vgLwxoM)Jf3IZgX-T0&LsjzgsEgwPclHqFD$TWI6D zKBAcHn7%7+AYqItmcw>%!*Zt42gEyInMbmf(MY&0!*dX7h3a1^H~Lp>9WgaZKV`Ub zP>Sph2mX}!0XZC795_0$HJuLDSg(5s=5ljc;iZm?8}G({IN6#=i=tON?m5yk8~p=K z+C}@aLEZTh^_+lR12SmpJ&=o%xR4JFzdk+GV8Z zn&}4|+0ynhNR!jw(P&=0KoJzw(&Vu*QJ7&^UupHeg;U-`Rle0$OcAkyA}-hnwX|q& z%go10ACU4|t;emvj@OekUTT!yx6*=1wLWB9Cj-hK0i97AZzUy3`bd}#@$QaG*GrQD zt@yKlW$i8Z)bny$(7l

61. Controlling the Fuzzy Beast

-

So, you want to give it a try? Let’s do it! First of all, you can set the delay between frames and the burst rate. The burst rate can cause the program to send one than one frame each interval. This is useful as a CAN bus could potentially support 2000 to 8000 frames per second. Then you can set the number of bytes to send. Ordinarily this would be the full 8 but you can experiment with smaller frames. You can set to send on a specific bus. That’s all the simple settings. It gets a bit more complicated now.

+

So, you want to give it a try? Let’s do it! First of all, you can set the delay between frames and the burst rate. The delay is in milliseconds but can be set as low as 0. If the delay is set to 0 then the system will attempt to send frames as fast as it can. However, even then you may not get quite as many frames per second as you’d like. Even at 0 it will still be scheduled by your operating system and so might not quite get to the speed you want. The burst rate can cause the program to send one than one frame each interval. This is useful as a CAN bus could potentially support 2000 to 8000 frames per second. If you need rapid frame sending your best bet is to set the sending interval to 1-2ms and then adjust the burst +rate until you get your desired sending rate. Then you can set the number of bytes to send. Ordinarily this would be the full 8 but you can experiment with smaller frames. You can set to send on a specific bus. That’s all the simple settings. It gets a bit more complicated now.

The “ID Scanning” box has two radio buttons:

  1. Sequential will go from “Start ID” to “End ID” then reset back to Start over and over.
  2. @@ -46,11 +47,12 @@
  3. Sweep causes the system to set the first one, then unset that one and set the second bit, then unset that, etc. Thus the fuzzed bit sweeps and only one fuzzed bit is set at once.
  4. Random will randomly pick whether each fuzzed bit is set or not.
-

In order to fuzz bits you need to set which bits to fuzz and which not to. As listed at the bottom of the window, there is a color code to the 8x8 grid. Clicking cells in the grid will toggle them between their various values. White bits are never set, black bits are always set no matter what, green bits follow the fuzzing pattern you specified in “Bit Scanning”

+

In order to fuzz bits you need to set which bits to fuzz and which not to. As listed at the bottom of the window, there is a color code to the 8x8 grid. Clicking cells in the grid will toggle them between their various values. White bits are never set, black bits are always set no matter what, green bits follow the fuzzing pattern you specified in “Bit Scanning” You can also set the bytes directly with the text boxes above the 8x8 grid. Setting a hexadecimal value in these +boxes will set the relavent bits in the 8x8 grid. You must press the ENTER/RETURN key to set the values. Merely changing the value will not update it (as a safety measure).

62. Pulling the Trigger

-

Once you’ve configured everything click “Start Fuzzing” to give it a shot. You will see the number of frames sent so far listed below the button. You can stop the fuzzing by pushing the button again.

+

Once you’ve configured everything click “Start Fuzzing” to give it a shot. You will see the number of frames sent so far listed below the button. Approximately four times per second the current value for each byte is copied into the text boxes just above the 8x8 grid. This can be used to see what is going on and to ensure that it is working the way you want it to. You can stop the fuzzing by pushing the start button again.

diff --git a/re/fuzzingwindow.cpp b/re/fuzzingwindow.cpp index 57a5238..b2c016b 100644 --- a/re/fuzzingwindow.cpp +++ b/re/fuzzingwindow.cpp @@ -26,6 +26,15 @@ FuzzingWindow::FuzzingWindow(const QVector *frames, QWidget *parent) : connect(ui->listID, &QListWidget::itemChanged, this, &FuzzingWindow::idListChanged); connect(ui->spinBytes, SIGNAL(valueChanged(int)), this, SLOT(changedNumDataBytes(int))); connect(ui->bitfield, SIGNAL(gridClicked(int,int)), this, SLOT(bitfieldClicked(int,int))); + connect(ui->txtByte0, &QLineEdit::returnPressed, this, [=](){changedDataByteText(0, ui->txtByte0->text());}); + connect(ui->txtByte1, &QLineEdit::returnPressed, this, [=](){changedDataByteText(1, ui->txtByte1->text());}); + connect(ui->txtByte2, &QLineEdit::returnPressed, this, [=](){changedDataByteText(2, ui->txtByte2->text());}); + connect(ui->txtByte3, &QLineEdit::returnPressed, this, [=](){changedDataByteText(3, ui->txtByte3->text());}); + connect(ui->txtByte4, &QLineEdit::returnPressed, this, [=](){changedDataByteText(4, ui->txtByte4->text());}); + connect(ui->txtByte5, &QLineEdit::returnPressed, this, [=](){changedDataByteText(5, ui->txtByte5->text());}); + connect(ui->txtByte6, &QLineEdit::returnPressed, this, [=](){changedDataByteText(6, ui->txtByte6->text());}); + connect(ui->txtByte7, &QLineEdit::returnPressed, this, [=](){changedDataByteText(7, ui->txtByte7->text());}); + connect(MainWindow::getReference(), SIGNAL(framesUpdated(int)), this, SLOT(updatedFrames(int))); @@ -110,9 +119,32 @@ void FuzzingWindow::changePlaybackSpeed(int newSpeed) fuzzTimer->setInterval(newSpeed); } +void FuzzingWindow::changedDataByteText(int which, QString valu) +{ + int startBit = which * 8; + int byt = valu.toInt(nullptr, 16); + + for (int i = 0; i < 8; i++) + { + bitGrid[startBit + i] = (byt & (1 << i)) ? 2 : 0; + } + + redrawGrid(); +} + void FuzzingWindow::changedNumDataBytes(int newVal) { qDebug() << "new num bytes: " << newVal; + + ui->txtByte0->setEnabled((newVal > 0) ? true : false); + ui->txtByte1->setEnabled((newVal > 1) ? true : false); + ui->txtByte2->setEnabled((newVal > 2) ? true : false); + ui->txtByte3->setEnabled((newVal > 3) ? true : false); + ui->txtByte4->setEnabled((newVal > 4) ? true : false); + ui->txtByte5->setEnabled((newVal > 5) ? true : false); + ui->txtByte6->setEnabled((newVal > 6) ? true : false); + ui->txtByte7->setEnabled((newVal > 7) ? true : false); + int byt; for (int i = 0; i < 64; i++) { @@ -132,8 +164,21 @@ void FuzzingWindow::changedNumDataBytes(int newVal) void FuzzingWindow::timerTriggered() { + static uint64_t lastByteUpdate = 0; CANFrame thisFrame; sendingBuffer.clear(); + //Every 250ms update the text fields to show our progress and what's going on. + if (QDateTime::currentMSecsSinceEpoch() - lastByteUpdate > 250) + { + ui->txtByte0->setText(QString::number(currentBytes[0], 16)); + ui->txtByte1->setText(QString::number(currentBytes[1], 16)); + ui->txtByte2->setText(QString::number(currentBytes[2], 16)); + ui->txtByte3->setText(QString::number(currentBytes[3], 16)); + ui->txtByte4->setText(QString::number(currentBytes[4], 16)); + ui->txtByte5->setText(QString::number(currentBytes[5], 16)); + ui->txtByte6->setText(QString::number(currentBytes[6], 16)); + ui->txtByte7->setText(QString::number(currentBytes[7], 16)); + } int buses = ui->cbBuses->currentIndex(); for (int count = 0; count < ui->spinBurst->value(); count++) { diff --git a/re/fuzzingwindow.h b/re/fuzzingwindow.h index d920bc3..23a7ece 100644 --- a/re/fuzzingwindow.h +++ b/re/fuzzingwindow.h @@ -65,6 +65,7 @@ private: void calcNextBitPattern(); void redrawGrid(); bool eventFilter(QObject *obj, QEvent *event); + void changedDataByteText(int which, QString valu); }; #endif // FUZZINGWINDOW_H diff --git a/ui/fuzzingwindow.ui b/ui/fuzzingwindow.ui index cef5ff5..ba6e22f 100644 --- a/ui/fuzzingwindow.ui +++ b/ui/fuzzingwindow.ui @@ -13,7 +13,7 @@ Fuzzing Window - + @@ -248,6 +248,151 @@ + + + + Byte Values (Always in hexadecimal) + + + Qt::AlignCenter + + + + + + + + + + 75 + true + + + + 0 + + + Qt::PlainText + + + + + + + + + + + 75 + true + + + + 1 + + + + + + + + + + + 75 + true + + + + 2 + + + + + + + + + + + 75 + true + + + + 3 + + + + + + + + + + + 75 + true + + + + 4 + + + + + + + + + + + 75 + true + + + + 5 + + + + + + + + + + + 75 + true + + + + 6 + + + + + + + + + + + 75 + true + + + + 7 + + + + + + + + From 8a2ce69fd698ab9d5402bc924242dc0eee133d15 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Feb 2020 20:16:50 -0500 Subject: [PATCH 10/58] Attempt to fix TravisCI problems --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6923721..eadb337 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,11 @@ matrix: compiler: gcc sudo: require before_install: - - sudo add-apt-repository ppa:beineri/opt-qt-5.12.3-xenial -y + - sudo add-apt-repository ppa:beineri/opt-qt-5.14.1-xenial -y - sudo apt-get update -qq install: - - sudo apt-get -y install qt512base qt512connectivity qt512imageformats qt512serialport qt512serialbus qt512doc qt512tools libgl1-mesa-dev + - sudo apt-get -y install qt514base qt514connectivity qt514imageformats qt514serialport qt514serialbus qt514doc qt514tools libgl1-mesa-dev - source /opt/qt*/bin/qt*-env.sh script: @@ -67,7 +67,7 @@ matrix: - os: windows before_install: - cmd.exe /C 'cd && "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat"' - - choco install zip + - choco install -y zip - curl -vLO http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe - "./qt-unified-windows-x86-online.exe --verbose --script qt-installer-windows.qs" script: From f27045cf3c1c3dbfee9f7df38f65a53d61798bad Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Feb 2020 20:28:57 -0500 Subject: [PATCH 11/58] Another attempt to fix TravisCI builds for all platforms (well, just windows is broken) --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eadb337..3896139 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,7 +67,6 @@ matrix: - os: windows before_install: - cmd.exe /C 'cd && "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat"' - - choco install -y zip - curl -vLO http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe - "./qt-unified-windows-x86-online.exe --verbose --script qt-installer-windows.qs" script: From 2cffffe0b9966540fa2b1a4d27fae551c1594694 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Feb 2020 20:42:49 -0500 Subject: [PATCH 12/58] More random junk trying to figure out travisci --- .travis.yml | 1 + qt-installer-windows.qs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3896139..218f99f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,6 +67,7 @@ matrix: - os: windows before_install: - cmd.exe /C 'cd && "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat"' + - dir - curl -vLO http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe - "./qt-unified-windows-x86-online.exe --verbose --script qt-installer-windows.qs" script: diff --git a/qt-installer-windows.qs b/qt-installer-windows.qs index 92908f7..043842c 100644 --- a/qt-installer-windows.qs +++ b/qt-installer-windows.qs @@ -14,8 +14,8 @@ // Unfortunately it is not possible to disable deps like qt.tools.qtcreator var INSTALL_COMPONENTS = [ installer.environmentVariable("PLATFORM") == "x64" ? - "qt.qt5.5123.win64_msvc2017_64" : - "qt.qt5.5123.win32_msvc2017", + "qt.qt5.5141.win64_msvc2017_64" : + "qt.qt5.5141.win32_msvc2017", ]; function Controller() { From 56d438ba73c0cd08be58262c63b25923ed78b5d4 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Feb 2020 20:51:17 -0500 Subject: [PATCH 13/58] TravisCI sucks. That is all. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 218f99f..37905c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,9 +65,10 @@ matrix: draft: false - os: windows + filter_secrets: false before_install: - cmd.exe /C 'cd && "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat"' - - dir + - choco install -y zip - curl -vLO http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe - "./qt-unified-windows-x86-online.exe --verbose --script qt-installer-windows.qs" script: From a93545ab85c57af4bb64cab3b88e988ee9231df4 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Feb 2020 21:07:47 -0500 Subject: [PATCH 14/58] Travisci is still trash --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 37905c4..7be7aac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,10 +67,9 @@ matrix: - os: windows filter_secrets: false before_install: - - cmd.exe /C 'cd && "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat"' - - choco install -y zip - curl -vLO http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe - "./qt-unified-windows-x86-online.exe --verbose --script qt-installer-windows.qs" + - cmd.exe /C 'cd && "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat"' script: - cmd.exe /C 'cd && "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" && cd && "C:/Qt/5.12.3/msvc2017/bin/qmake" CONFIG+=RELEASE SavvyCAN.pro && cd && nmake' - ls From d15cc4143a4093063a9021f983fb324d768bfd9a Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Feb 2020 21:28:08 -0500 Subject: [PATCH 15/58] Might actually be a windows problem (big surprise). Trying to fix. --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7be7aac..156f1e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,13 +67,13 @@ matrix: - os: windows filter_secrets: false before_install: + - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" - curl -vLO http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe - - "./qt-unified-windows-x86-online.exe --verbose --script qt-installer-windows.qs" - - cmd.exe /C 'cd && "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat"' + - "./qt-unified-windows-x86-online.exe" --verbose --script qt-installer-windows.qs script: - - cmd.exe /C 'cd && "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" && cd && "C:/Qt/5.12.3/msvc2017/bin/qmake" CONFIG+=RELEASE SavvyCAN.pro && cd && nmake' + - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" '&&' cd '&&' "C:/Qt/5.12.3/msvc2017/bin/qmake" CONFIG+=RELEASE SavvyCAN.pro '&&' cd '&&' nmake - ls - - cmd.exe /C 'cd release && "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" && "C:/Qt/5.12.3/msvc2017/bin/windeployqt.exe" ./SavvyCAN.exe' + - cmd.exe //C cd release '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" '&&' "C:/Qt/5.12.3/msvc2017/bin/windeployqt.exe" ./SavvyCAN.exe - cd release - ls - rm *.cpp From d57e20726af8b2f6939f4423ff8784e83ab8d757 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Feb 2020 21:49:20 -0500 Subject: [PATCH 16/58] Fix YAML syntax error. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 156f1e5..76111f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,11 +65,10 @@ matrix: draft: false - os: windows - filter_secrets: false before_install: - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" - curl -vLO http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe - - "./qt-unified-windows-x86-online.exe" --verbose --script qt-installer-windows.qs + - ./qt-unified-windows-x86-online.exe --verbose' --script qt-installer-windows.qs script: - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" '&&' cd '&&' "C:/Qt/5.12.3/msvc2017/bin/qmake" CONFIG+=RELEASE SavvyCAN.pro '&&' cd '&&' nmake - ls From c80167f2d9eac7d0954d14ca9f5971c89132404e Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Feb 2020 21:53:31 -0500 Subject: [PATCH 17/58] Fix another syntax error... --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 76111f1..5004cc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,7 +68,7 @@ matrix: before_install: - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" - curl -vLO http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe - - ./qt-unified-windows-x86-online.exe --verbose' --script qt-installer-windows.qs + - ./qt-unified-windows-x86-online.exe --verbose --script qt-installer-windows.qs script: - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" '&&' cd '&&' "C:/Qt/5.12.3/msvc2017/bin/qmake" CONFIG+=RELEASE SavvyCAN.pro '&&' cd '&&' nmake - ls From 3b41b9579c2599f311262e2ab08d563107f1be24 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Feb 2020 21:59:21 -0500 Subject: [PATCH 18/58] More fixes to hopefully make the Windows build work. --- qt-installer-windows.qs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/qt-installer-windows.qs b/qt-installer-windows.qs index 043842c..97166b0 100644 --- a/qt-installer-windows.qs +++ b/qt-installer-windows.qs @@ -98,4 +98,22 @@ Controller.prototype.FinishedPageCallback = function() { gui.clickButton(buttons.FinishButton); }; +Controller.prototype.DynamicTelemetryPluginFormCallback = function() { + var widget = gui.currentPageWidget(); + widget.TelemetryPluginForm.statisticGroupBox.disableStatisticRadioButton.checked = true; + gui.clickButton(buttons.NextButton); +} + +Controller.prototype.ComponentSelectionPageCallback = function() { + var page = gui.pageWidgetByObjectName("ComponentSelectionPage"); + + var archiveCheckBox = gui.findChild(page, "Archive"); + var latestCheckBox = gui.findChild(page, "Latest releases"); + var fetchButton = gui.findChild(page, "FetchCategoryButton"); + + archiveCheckBox.click(); + latestCheckBox.click(); + fetchButton.click(); +} + // vim: set ft=javascript: From 367c072b45435259b765d2f38942e0aeeebaeaea Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 18 Feb 2020 21:31:18 -0500 Subject: [PATCH 19/58] Various fixes to issues caused by recent transition to QCanBusFrame --- bus_protocols/isotp_handler.cpp | 34 +++++++++++++++++++++-------- filterutility.cpp | 2 +- framefileio.cpp | 4 ++-- framesenderwindow.cpp | 15 +++++++++---- re/flowviewwindow.cpp | 38 ++++++++++++++++----------------- re/isotp_interpreterwindow.cpp | 25 +++------------------- re/rangestatewindow.cpp | 13 ++++------- scriptcontainer.cpp | 12 +++++++---- 8 files changed, 73 insertions(+), 70 deletions(-) diff --git a/bus_protocols/isotp_handler.cpp b/bus_protocols/isotp_handler.cpp index 583cdc5..ecfb332 100644 --- a/bus_protocols/isotp_handler.cpp +++ b/bus_protocols/isotp_handler.cpp @@ -156,7 +156,9 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) //int offset; ISOTP_MESSAGE msg; ISOTP_MESSAGE *pMsg; + QByteArray dataBytes; const unsigned char *data = reinterpret_cast(frame.payload().constData()); + qDebug() << frame.payload().count(); //int dataLen = frame.payload().count(); frameType = 0; @@ -185,16 +187,29 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) if (frameLen > 7) return; msg.bus = frame.bus; + msg.setFrameType(QCanBusFrame::FrameType::DataFrame); msg.setExtendedFrameFormat( frame.hasExtendedFrameFormat() ); msg.setFrameId(ID); msg.isReceived = frame.isReceived; - msg.payload().reserve(frameLen); + dataBytes.reserve(frameLen); msg.reportedLength = frameLen; msg.setTimeStamp(frame.timeStamp()); msg.isMultiframe = false; - if (useExtendedAddressing) for (int j = 0; j < frameLen; j++) msg.payload().append(frame.payload()[j+2]); - else for (int j = 0; j < frameLen; j++) msg.payload().append(frame.payload()[j+1]); - //qDebug() << "Emitting single frame ISOTP message"; + if (useExtendedAddressing) + { + for (int j = 0; j < frameLen; j++) + { + dataBytes.append(data[j+2]); + } + } + else + { + for (int j = 0; j < frameLen; j++) + { + dataBytes.append(data[j+1]); + } + } + qDebug() << "Emitting single frame ISOTP message"; emit newISOMessage(msg); break; case 1: //first frame of a multi-frame message @@ -210,9 +225,9 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) { frameLen += data[2]; frameLen = frameLen & 0xFFF; - msg.payload().reserve(frameLen); + dataBytes.reserve(frameLen); msg.reportedLength = frameLen; - for (int j = 0; j < 5; j++) msg.payload().append(frame.payload()[3 + j]); + for (int j = 0; j < 5; j++) dataBytes.append(frame.payload()[3 + j]); } else { @@ -220,9 +235,10 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) frameLen = frameLen & 0xFFF; msg.payload().reserve(frameLen); msg.reportedLength = frameLen; - for (int j = 0; j < 6; j++) msg.payload().append(frame.payload()[2 + j]); + for (int j = 0; j < 6; j++) dataBytes.append(frame.payload()[2 + j]); } msg.lastSequence = -1; + msg.setPayload(dataBytes); messageBuffer.append(msg); //The sending ID is set to the last ID we used to send from this class which is //very likely to be correct. But, caution, there is a chance that it isn't. Beware. @@ -266,7 +282,7 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) } if (pMsg->reportedLength <= pMsg->payload().count()) { - //qDebug() << "Emitting multiframe ISOTP message"; + qDebug() << "Emitting multiframe ISOTP message"; checkNeedFlush(pMsg->frameId()); } break; @@ -316,7 +332,7 @@ void ISOTP_HANDLER::checkNeedFlush(uint64_t ID) qDebug() << "Flushing a partial frame " << QString::number(messageBuffer[i].frameId(), 16) << " " << messageBuffer[i].reportedLength << " " << messageBuffer[i].payload().count(); } } - emit newISOMessage(messageBuffer[i]); + if (messageBuffer[i].reportedLength > 0) emit newISOMessage(messageBuffer[i]); messageBuffer.removeAt(i); return; } diff --git a/filterutility.cpp b/filterutility.cpp index b577c3d..657deec 100644 --- a/filterutility.cpp +++ b/filterutility.cpp @@ -60,7 +60,7 @@ QListWidgetItem * FilterUtility::createFilterItem(int32_t id, QListWidget* paren // Filter labeling (show interpreted frame names next to the CAN addr ID) MatchingCriteria_t matchingCriteria; DBC_MESSAGE *msg = dbcHandler->findMessageForFilter(id,&matchingCriteria); - if (msg != NULL) + if (msg != nullptr) { filterItemName.append(" "); filterItemName.append(msg->name); diff --git a/framefileio.cpp b/framefileio.cpp index c5a8439..a931a63 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -3198,10 +3198,10 @@ bool FrameFileIO::loadCanDumpFile(QString filename, QVector* frames) if (thisFrame.frameId() > 0x7FF) thisFrame.setExtendedFrameFormat(true); else thisFrame.setExtendedFrameFormat(false); thisFrame.setFrameType(QCanBusFrame::DataFrame); - QByteArray bytes(tokens[3].at(1) - '0', 0); + QByteArray bytes(tokens[3].at(1) - '0', 0); for (int c = 0; c < thisFrame.payload().length(); c++) { - bytes[c] = static_cast(tokens[4 + c].toInt(nullptr, 16)); + if ((4 + c) < tokens.size()) bytes[c] = static_cast(tokens[4 + c].toInt(nullptr, 16)); } thisFrame.setPayload(bytes); } diff --git a/framesenderwindow.cpp b/framesenderwindow.cpp index 2d17b36..622f729 100644 --- a/framesenderwindow.cpp +++ b/framesenderwindow.cpp @@ -511,7 +511,9 @@ void FrameSenderWindow::doModifiers(int idx) } } //Finally, drop the result into the proper data byte - sendData->payload().data()[mod->destByte] = (char) shadowReg; + QByteArray newArr(sendData->payload()); + newArr[mod->destByte] = (char) shadowReg; + sendData->setPayload(newArr); } } @@ -830,6 +832,7 @@ void FrameSenderWindow::processCellChange(int line, int col) sendingData[line].count = 0; int numBuses = CANConManager::getInstance()->getNumBuses(); + QByteArray arr; switch (col) { @@ -864,8 +867,9 @@ void FrameSenderWindow::processCellChange(int line, int col) case 3: //length field tempVal = Utility::ParseStringToNum(ui->tableSender->item(line, 3)->text()); if (tempVal < 0) tempVal = 0; - if (tempVal > 8) tempVal = 8; - sendingData[line].payload().resize(tempVal); + if (tempVal > 8) tempVal = 8; + arr.resize(tempVal); + sendingData[line].setPayload(arr); break; case 4: // Ext if (ui->tableSender->item(line, 4)->checkState() == Qt::Checked) { @@ -885,10 +889,13 @@ void FrameSenderWindow::processCellChange(int line, int col) for (int i = 0; i < 8; i++) sendingData[line].payload().data()[i] = 0; tokens = ui->tableSender->item(line, 6)->text().split(" "); + arr.clear(); + arr.reserve(tokens.count()); for (int j = 0; j < tokens.count(); j++) { - sendingData[line].payload().data()[j] = (uint8_t)Utility::ParseStringToNum(tokens[j]); + arr.append((uint8_t)Utility::ParseStringToNum(tokens[j])); } + sendingData[line].setPayload(arr); break; case 7: //triggers processTriggerText(line); diff --git a/re/flowviewwindow.cpp b/re/flowviewwindow.cpp index 9d8f3de..31e4010 100644 --- a/re/flowviewwindow.cpp +++ b/re/flowviewwindow.cpp @@ -609,7 +609,7 @@ void FlowViewWindow::changeID(QString newID) updateGraphLocation(); - memcpy(currBytes, frameCache.at(currentPosition).payload(), 8); + memcpy(currBytes, frameCache.at(currentPosition).payload().constData(), 8); memcpy(refBytes, currBytes, 8); updateDataView(); @@ -645,7 +645,7 @@ void FlowViewWindow::btnStopClick() currentPosition = 0; - memcpy(currBytes, frameCache.at(currentPosition).payload().data_ptr(), 8); + memcpy(currBytes, frameCache.at(currentPosition).payload().constData(), 8); memcpy(refBytes, currBytes, 8); updateFrameLabel(); @@ -705,23 +705,23 @@ void FlowViewWindow::timerTriggered() void FlowViewWindow::updateDataView() { - ui->txtCurr1->setText(Utility::formatNumber((char)currBytes[0])); - ui->txtCurr2->setText(Utility::formatNumber((char)currBytes[1])); - ui->txtCurr3->setText(Utility::formatNumber((char)currBytes[2])); - ui->txtCurr4->setText(Utility::formatNumber((char)currBytes[3])); - ui->txtCurr5->setText(Utility::formatNumber((char)currBytes[4])); - ui->txtCurr6->setText(Utility::formatNumber((char)currBytes[5])); - ui->txtCurr7->setText(Utility::formatNumber((char)currBytes[6])); - ui->txtCurr8->setText(Utility::formatNumber((char)currBytes[7])); + ui->txtCurr1->setText(Utility::formatNumber((unsigned char)currBytes[0])); + ui->txtCurr2->setText(Utility::formatNumber((unsigned char)currBytes[1])); + ui->txtCurr3->setText(Utility::formatNumber((unsigned char)currBytes[2])); + ui->txtCurr4->setText(Utility::formatNumber((unsigned char)currBytes[3])); + ui->txtCurr5->setText(Utility::formatNumber((unsigned char)currBytes[4])); + ui->txtCurr6->setText(Utility::formatNumber((unsigned char)currBytes[5])); + ui->txtCurr7->setText(Utility::formatNumber((unsigned char)currBytes[6])); + ui->txtCurr8->setText(Utility::formatNumber((unsigned char)currBytes[7])); - ui->txtRef1->setText(Utility::formatNumber((char)refBytes[0])); - ui->txtRef2->setText(Utility::formatNumber((char)refBytes[1])); - ui->txtRef3->setText(Utility::formatNumber((char)refBytes[2])); - ui->txtRef4->setText(Utility::formatNumber((char)refBytes[3])); - ui->txtRef5->setText(Utility::formatNumber((char)refBytes[4])); - ui->txtRef6->setText(Utility::formatNumber((char)refBytes[5])); - ui->txtRef7->setText(Utility::formatNumber((char)refBytes[6])); - ui->txtRef8->setText(Utility::formatNumber((char)refBytes[7])); + ui->txtRef1->setText(Utility::formatNumber((unsigned char)refBytes[0])); + ui->txtRef2->setText(Utility::formatNumber((unsigned char)refBytes[1])); + ui->txtRef3->setText(Utility::formatNumber((unsigned char)refBytes[2])); + ui->txtRef4->setText(Utility::formatNumber((unsigned char)refBytes[3])); + ui->txtRef5->setText(Utility::formatNumber((unsigned char)refBytes[4])); + ui->txtRef6->setText(Utility::formatNumber((unsigned char)refBytes[5])); + ui->txtRef7->setText(Utility::formatNumber((unsigned char)refBytes[6])); + ui->txtRef8->setText(Utility::formatNumber((unsigned char)refBytes[7])); ui->flowView->setReference(refBytes, false); ui->flowView->updateData(currBytes, true); @@ -782,7 +782,7 @@ void FlowViewWindow::updatePosition(bool forward) playbackTimer->stop(); } - memcpy(currBytes, frameCache.at(currentPosition).payload().data(), 8); + memcpy(currBytes, frameCache.at(currentPosition).payload().constData(), 8); if (ui->cbSync->checkState() == Qt::Checked) emit sendCenterTimeID(frameCache[currentPosition].frameId(), frameCache[currentPosition].timeStamp().microSeconds() / 1000000.0); } diff --git a/re/isotp_interpreterwindow.cpp b/re/isotp_interpreterwindow.cpp index f18eafd..50ff657 100644 --- a/re/isotp_interpreterwindow.cpp +++ b/re/isotp_interpreterwindow.cpp @@ -2,6 +2,7 @@ #include "ui_isotp_interpreterwindow.h" #include "mainwindow.h" #include "helpwindow.h" +#include "filterutility.h" ISOTP_InterpreterWindow::ISOTP_InterpreterWindow(const QVector *frames, QWidget *parent) : QDialog(parent), @@ -136,7 +137,7 @@ void ISOTP_InterpreterWindow::listFilterItemChanged(QListWidgetItem *item) { if (item) { - int id = item->text().toInt(nullptr, 16); + int id = FilterUtility::getIdAsInt(item); bool state = item->checkState(); //qDebug() << id << "*" << state; idFilters[id] = state; @@ -240,22 +241,6 @@ void ISOTP_InterpreterWindow::newUDSMessage(UDS_MESSAGE msg) buildText = ui->txtFrameDetails->toPlainText(); - /* - buildText.append("UDS Message:\n"); - if (msg.isErrorReply) - { - buildText.append("Error reply for service " + udsDecoder->getServiceShortDesc(msg.service)); - buildText.append("\nError Desc: " + udsDecoder->getNegativeResponseShort(msg.subFunc)); - } - else - { - if (msg.service < 0x3F || (msg.service > 0x7F && msg.service < 0xAF)) - buildText.append("Request for service " + udsDecoder->getServiceShortDesc(msg.service) + " Sub Func: " + QString::number(msg.subFunc)); - else - buildText.append("Response on service " + udsDecoder->getServiceShortDesc(msg.service - 0x40) + " Sub Func: " + QString::number(msg.subFunc)); - }*/ - - //Much more detailed analysis than the code above. You'll like it. buildText.append(udsDecoder->getDetailedMessageAnalysis(msg)); ui->txtFrameDetails->setPlainText(buildText); @@ -275,9 +260,7 @@ void ISOTP_InterpreterWindow::newISOMessage(ISOTP_MESSAGE msg) { idFilters.insert(msg.frameId(), true); - QListWidgetItem* listItem = new QListWidgetItem(Utility::formatCANID(msg.frameId(), msg.hasExtendedFrameFormat()), ui->listFilter); - listItem->setFlags(listItem->flags() | Qt::ItemIsUserCheckable); // set checkable flag - listItem->setCheckState(Qt::Checked); + FilterUtility::createCheckableFilterItem(msg.frameId(), true, ui->listFilter); } if (!idFilters[msg.frameId()]) return; messages.append(msg); @@ -303,5 +286,3 @@ void ISOTP_InterpreterWindow::newISOMessage(ISOTP_MESSAGE msg) } ui->tableIsoFrames->setItem(rowNum, 5, new QTableWidgetItem(tempString)); } - - diff --git a/re/rangestatewindow.cpp b/re/rangestatewindow.cpp index 30ccd44..498ff20 100644 --- a/re/rangestatewindow.cpp +++ b/re/rangestatewindow.cpp @@ -3,6 +3,7 @@ #include "mainwindow.h" #include "utility.h" #include "helpwindow.h" +#include "filterutility.h" RangeStateWindow::RangeStateWindow(const QVector *frames, QWidget *parent) : QDialog(parent), @@ -68,7 +69,7 @@ RangeStateWindow::RangeStateWindow(const QVector *frames, QWidget *par [=](QListWidgetItem *item) { bool isChecked = false; - int id = Utility::ParseStringToNum(item->text()); + int id = FilterUtility::getIdAsInt(item); if (item->checkState() == Qt::Checked) isChecked = true; idFilters[id] = isChecked; }); @@ -161,9 +162,7 @@ void RangeStateWindow::updatedFrames(int numFrames) if (!idFilters.contains(thisFrame.frameId())) { idFilters.insert(thisFrame.frameId(), true); - QListWidgetItem* listItem = new QListWidgetItem(Utility::formatCANID(thisFrame.frameId(), thisFrame.hasExtendedFrameFormat()), ui->listFilter); - listItem->setFlags(listItem->flags() | Qt::ItemIsUserCheckable); // set checkable flag - listItem->setCheckState(Qt::Checked); //default all filters to be set active + FilterUtility::createCheckableFilterItem(thisFrame.frameId(), true, ui->listFilter); } } } @@ -182,11 +181,7 @@ void RangeStateWindow::refreshFilterList() if (!idFilters.contains(id)) { idFilters.insert(id, true); - QListWidgetItem* listItem; - if (id < 0x800) listItem = new QListWidgetItem(Utility::formatCANID(id, false), ui->listFilter); - else listItem = new QListWidgetItem(Utility::formatCANID(id, true), ui->listFilter); - listItem->setFlags(listItem->flags() | Qt::ItemIsUserCheckable); // set checkable flag - listItem->setCheckState(Qt::Checked); //default all filters to be set active + FilterUtility::createCheckableFilterItem(id, true, ui->listFilter); } } diff --git a/scriptcontainer.cpp b/scriptcontainer.cpp index 30969be..e877944 100644 --- a/scriptcontainer.cpp +++ b/scriptcontainer.cpp @@ -306,9 +306,10 @@ void ISOTPScriptHelper::setFilter(QJSValue id, QJSValue mask, QJSValue bus) void ISOTPScriptHelper::sendISOTP(QJSValue bus, QJSValue id, QJSValue length, QJSValue dataBytes) { ISOTP_MESSAGE msg; + QByteArray dataArray; msg.setExtendedFrameFormat(false); msg.setFrameId(id.toUInt()); - msg.payload().resize(length.toInt()); + dataArray.resize(length.toInt()); int dataLen = msg.payload().length(); @@ -316,8 +317,9 @@ void ISOTPScriptHelper::sendISOTP(QJSValue bus, QJSValue id, QJSValue length, QJ for (int i = 0; i < dataLen; i++) { - msg.payload()[i] = static_cast(dataBytes.property(i).toInt()); + dataArray[i] = static_cast(dataBytes.property(i).toInt()); } + msg.setPayload(dataArray); msg.bus = bus.toInt(); @@ -379,9 +381,10 @@ void UDSScriptHelper::setFilter(QJSValue id, QJSValue mask, QJSValue bus) void UDSScriptHelper::sendUDS(QJSValue bus, QJSValue id, QJSValue service, QJSValue sublen, QJSValue subFunc, QJSValue length, QJSValue data) { UDS_MESSAGE msg; + QByteArray dataArray; msg.setExtendedFrameFormat(false); msg.setFrameId( id.toUInt() ); - msg.payload().resize(length.toUInt()); + dataArray.resize(length.toUInt()); msg.service = service.toUInt(); msg.subFuncLen = sublen.toUInt(); msg.subFunc = subFunc.toUInt(); @@ -390,8 +393,9 @@ void UDSScriptHelper::sendUDS(QJSValue bus, QJSValue id, QJSValue service, QJSVa for (int i = 0; i < msg.payload().length(); i++) { - msg.payload().append(static_cast(data.property(static_cast(i)).toInt())); + dataArray[i] = (static_cast(data.property(static_cast(i)).toInt())); } + msg.setPayload(dataArray); msg.bus = bus.toInt(); From a364444f76475538c4f239f646f335bdb33cb5ec Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 19 Feb 2020 21:01:45 -0500 Subject: [PATCH 20/58] Fixes to isotp handler --- bus_protocols/isotp_handler.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bus_protocols/isotp_handler.cpp b/bus_protocols/isotp_handler.cpp index ecfb332..c589cdc 100644 --- a/bus_protocols/isotp_handler.cpp +++ b/bus_protocols/isotp_handler.cpp @@ -268,18 +268,21 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) } if (!pMsg) return; if (!pMsg->isMultiframe) return; //if we didn't get a frame type 1 (start of multiframe) first then ignore this frame. - ln = pMsg->payload().length() - pMsg->payload().count(); + dataBytes.clear(); + dataBytes.append(pMsg->payload()); + ln = pMsg->reportedLength - pMsg->payload().count(); //offset = pMsg->data.count(); if (useExtendedAddressing) { if (ln > 6) ln = 6; - for (int j = 0; j < ln; j++) pMsg->payload().append(frame.payload()[j+2]); + for (int j = 0; j < ln; j++) dataBytes.append(frame.payload()[j+2]); } else { if (ln > 7) ln = 7; - for (int j = 0; j < ln; j++) pMsg->payload().append(frame.payload()[j+1]); + for (int j = 0; j < ln; j++) dataBytes.append(frame.payload()[j+1]); } + pMsg->setPayload(dataBytes); if (pMsg->reportedLength <= pMsg->payload().count()) { qDebug() << "Emitting multiframe ISOTP message"; From 22e5f6dbfba4e9ef3e90f5c471fe33155c5ef533 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 19 Feb 2020 21:02:09 -0500 Subject: [PATCH 21/58] Try to fix qscript for windows installation --- qt-installer-windows.qs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/qt-installer-windows.qs b/qt-installer-windows.qs index 97166b0..7e1a79b 100644 --- a/qt-installer-windows.qs +++ b/qt-installer-windows.qs @@ -28,15 +28,6 @@ function Controller() { }); } -Controller.prototype.DynamicTelemetryPluginFormCallback = function() { - gui.currentPageWidget().TelemetryPluginForm.statisticGroupBox.disableStatisticRadioButton.setChecked(true); - gui.clickButton(buttons.NextButton); - - //for(var key in widget.TelemetryPluginForm.statisticGroupBox){ - // console.log(key); - //} -} - Controller.prototype.WelcomePageCallback = function() { console.log("Step: " + gui.currentPageWidget()); // At least for 3.0.4 immediately clicking Next fails, so wait a bit. @@ -61,16 +52,6 @@ Controller.prototype.TargetDirectoryPageCallback = function() { gui.clickButton(buttons.NextButton); }; -Controller.prototype.ComponentSelectionPageCallback = function() { - console.log("Step: " + gui.currentPageWidget()); - var page = gui.currentPageWidget(); - page.deselectAll(); - for (var i = 0; i < INSTALL_COMPONENTS.length; i++) { - page.selectComponent(INSTALL_COMPONENTS[i]); - } - gui.clickButton(buttons.NextButton); -}; - Controller.prototype.LicenseAgreementPageCallback = function() { console.log("Step: " + gui.currentPageWidget()); gui.currentPageWidget().AcceptLicenseRadioButton.setChecked(true); From 6e3a4995e51141cfb6681fee2a2aca70020f96c9 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 17 Mar 2020 21:24:45 -0400 Subject: [PATCH 22/58] Fix some bugs in bisect window --- bisectwindow.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bisectwindow.cpp b/bisectwindow.cpp index 99a27ca..d86fc3c 100644 --- a/bisectwindow.cpp +++ b/bisectwindow.cpp @@ -64,6 +64,9 @@ bool BisectWindow::eventFilter(QObject *obj, QEvent *event) void BisectWindow::refreshIDList() { int id; + foundID.clear(); + ui->cbIDLower->clear(); + ui->cbIDUpper->clear(); for (int i = 0; i < modelFrames->count(); i++) { id = modelFrames->at(i).frameId(); @@ -137,7 +140,12 @@ void BisectWindow::handleCalculateButton() void BisectWindow::handleReplaceButton() { - + CANFrameModel *model; + model = MainWindow::getReference()->getCANFrameModel(); + model->clearFrames(); + model->insertFrames(splitFrames); + refreshFrameNumbers(); + refreshIDList(); } void BisectWindow::handleSaveButton() From f4a455b1b8f29f9304b81f8cde6b05910e737f77 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 17 Mar 2020 21:25:13 -0400 Subject: [PATCH 23/58] Slight changes to BLF loader, just to make debugging easier in the future --- blfhandler.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/blfhandler.cpp b/blfhandler.cpp index 9c3fb1a..b0a845a 100644 --- a/blfhandler.cpp +++ b/blfhandler.cpp @@ -67,7 +67,7 @@ bool BLFHandler::loadBLF(QString filename, QVector* frames) qDebug() << "Container is not compressed"; uncompressedData = fileData; } - if (objHeader.containerObj.compressionMethod == BLF_CONT_ZLIB_COMPRESSION) + else if (objHeader.containerObj.compressionMethod == BLF_CONT_ZLIB_COMPRESSION) { qDebug() << "Compressed container. Unpacking it."; fileData.prepend(objHeader.containerObj.uncompressedSize & 0xFF); @@ -76,7 +76,12 @@ bool BLFHandler::loadBLF(QString filename, QVector* frames) fileData.prepend((objHeader.containerObj.uncompressedSize >> 24) & 0xFF); uncompressedData += qUncompress(fileData); } + else + { + qDebug() << "Dunno what this is... " << objHeader.containerObj.compressionMethod; + } qDebug() << "Uncompressed size: " << uncompressedData.count(); + qDebug() << "Currently loaded frames at this point: " << frames->count(); pos = 0; //bool foundHeader = false; //first skip forward to find a header signature - usually not necessary @@ -91,7 +96,8 @@ bool BLFHandler::loadBLF(QString filename, QVector* frames) { memcpy(&obj.header.base, (uncompressedData.constData() + pos), sizeof(BLF_OBJ_HEADER_BASE)); memcpy(&obj.header.v1Obj, (uncompressedData.constData() + pos) + sizeof(BLF_OBJ_HEADER_BASE), sizeof(BLF_OBJ_HEADER_V1)); - qDebug() << "Pos: " << pos << " Type: " << obj.header.base.objType << "Obj Size: " << obj.header.base.objSize; + //if (obj.header.base.objType != 1) + //qDebug() << "Pos: " << pos << " Type: " << obj.header.base.objType << "Obj Size: " << obj.header.base.objSize; if (qFromLittleEndian(objHeader.base.sig) == 0x4A424F4C) { fileData = uncompressedData.mid(pos + sizeof(BLF_OBJ_HEADER_BASE) + sizeof(BLF_OBJ_HEADER_V1), obj.header.base.objSize - sizeof(BLF_OBJ_HEADER_BASE) - sizeof(BLF_OBJ_HEADER_V1)); @@ -109,7 +115,7 @@ bool BLFHandler::loadBLF(QString filename, QVector* frames) frame.setFrameType(QCanBusFrame::RemoteRequestFrame); } else { frame.setFrameType(QCanBusFrame::DataFrame); - for (int i = 0; i < 8; i++) bytes[i] = canObject.data[i]; + for (int i = 0; i < canObject.dlc; i++) bytes[i] = canObject.data[i]; } frame.setPayload(bytes); //Should we divide by a thousand or a million? Unsure here. It appears some logs are stamped in microseconds and some in milliseconds? @@ -130,7 +136,7 @@ bool BLFHandler::loadBLF(QString filename, QVector* frames) frame.setFrameType(QCanBusFrame::RemoteRequestFrame); } else { frame.setFrameType(QCanBusFrame::DataFrame); - for (int i = 0; i < 8; i++) bytes[i] = canObject2.data[i]; + for (int i = 0; i < canObject2.dlc; i++) bytes[i] = canObject2.data[i]; } //Should we divide by a thousand or a million? Unsure here. It appears some logs are stamped in microseconds and some in milliseconds? frame.setTimeStamp(QCanBusFrame::TimeStamp(0, obj.header.v1Obj.uncompSize / 1000.0)); //uncompsize field also used for timestamp oddly enough @@ -138,7 +144,7 @@ bool BLFHandler::loadBLF(QString filename, QVector* frames) } else { - //qDebug() << "Not a can frame! ObjType: " << obj.header.objType; + qDebug() << "Not a can frame! ObjType: " << obj.header.base.objType; if (obj.header.base.objType > 0xFFFF) return false; } From 27b5d156b7f7672c818b95c9d7128e14cd3595e2 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 17 Mar 2020 21:26:17 -0400 Subject: [PATCH 24/58] Bug fixes for ISOTP and UDS interpretation. --- bus_protocols/isotp_handler.cpp | 39 +++++++++++++-------------------- bus_protocols/isotp_handler.h | 2 +- bus_protocols/uds_handler.cpp | 20 ++++++++--------- re/isotp_interpreterwindow.cpp | 2 +- 4 files changed, 27 insertions(+), 36 deletions(-) diff --git a/bus_protocols/isotp_handler.cpp b/bus_protocols/isotp_handler.cpp index c589cdc..c741866 100644 --- a/bus_protocols/isotp_handler.cpp +++ b/bus_protocols/isotp_handler.cpp @@ -210,6 +210,7 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) } } qDebug() << "Emitting single frame ISOTP message"; + msg.setPayload(dataBytes); emit newISOMessage(msg); break; case 1: //first frame of a multi-frame message @@ -239,7 +240,7 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) } msg.lastSequence = -1; msg.setPayload(dataBytes); - messageBuffer.append(msg); + messageBuffer.insert(msg.frameId(), msg); //The sending ID is set to the last ID we used to send from this class which is //very likely to be correct. But, caution, there is a chance that it isn't. Beware. if (issueFlowMsgs && lastSenderID > 0) @@ -258,13 +259,9 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) break; case 2: //subsequent frames for multi-frame messages pMsg = nullptr; - for (int i = 0; i < messageBuffer.length(); i++) + if (messageBuffer.contains(ID)) { - if (messageBuffer[i].frameId() == ID) - { - pMsg = &messageBuffer[i]; - break; - } + pMsg = &messageBuffer[ID]; } if (!pMsg) return; if (!pMsg->isMultiframe) return; //if we didn't get a frame type 1 (start of multiframe) first then ignore this frame. @@ -319,26 +316,20 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) void ISOTP_HANDLER::checkNeedFlush(uint64_t ID) { - for (int i = 0; i < messageBuffer.length(); i++) + ISOTP_MESSAGE *msg; + if (messageBuffer.contains(ID)) { - if (messageBuffer[i].frameId() == ID) + msg = &messageBuffer[ID]; + if (msg->reportedLength <= msg->payload().count()) { - //used to pass by reference but now newISOMessage should pass by value which makes it easier to use cross thread - if (messageBuffer[i].frameId() > 0x600 && messageBuffer[i].frameId() < 0x630) - { - if (messageBuffer[i].reportedLength <= messageBuffer[i].payload().count()) - { - qDebug() << "Flushing full frame" << QString::number(messageBuffer[i].frameId(), 16) << " " << messageBuffer[i].reportedLength << " " << messageBuffer[i].payload().count(); - } - else - { - qDebug() << "Flushing a partial frame " << QString::number(messageBuffer[i].frameId(), 16) << " " << messageBuffer[i].reportedLength << " " << messageBuffer[i].payload().count(); - } - } - if (messageBuffer[i].reportedLength > 0) emit newISOMessage(messageBuffer[i]); - messageBuffer.removeAt(i); - return; + qDebug() << "Flushing full frame" << QString::number(msg->frameId(), 16) << " " << msg->reportedLength << " " << msg->payload().count(); } + else + { + qDebug() << "Flushing a partial frame " << QString::number(msg->frameId(), 16) << " " << msg->reportedLength << " " << msg->payload().count(); + } + if (msg->reportedLength > 0) emit newISOMessage(*msg); + messageBuffer.remove(ID); } } diff --git a/bus_protocols/isotp_handler.h b/bus_protocols/isotp_handler.h index f45e0ba..68d49b1 100644 --- a/bus_protocols/isotp_handler.h +++ b/bus_protocols/isotp_handler.h @@ -35,7 +35,7 @@ signals: void newISOMessage(ISOTP_MESSAGE msg); private: - QList messageBuffer; + QHash messageBuffer; QList sendingFrames; QList filters; const QVector *modelFrames; diff --git a/bus_protocols/uds_handler.cpp b/bus_protocols/uds_handler.cpp index d218c5d..48f2d99 100644 --- a/bus_protocols/uds_handler.cpp +++ b/bus_protocols/uds_handler.cpp @@ -409,7 +409,7 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) if (dataLen > 1) { buildString.append("Data payload: "); - for (int j = 1; j < dataLen; j++) buildString.append(Utility::formatHexNum(data[j]) + " "); + for (int j = 2; j < dataLen; j++) buildString.append(Utility::formatHexNum(data[j]) + " "); } } else @@ -417,8 +417,8 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) buildString.append("Key sending for security level: " + QString::number(msg.subFunc - 1)); if (dataLen > 1) //and it sure as hell should be! { - buildString.append("KEY: "); - for (int j = 1; j < dataLen; j++) buildString.append(Utility::formatHexNum(data[j]) + " "); + buildString.append(" KEY: "); + for (int j = 2; j < dataLen; j++) buildString.append(Utility::formatHexNum(data[j]) + " "); } } break; @@ -429,7 +429,7 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) if (dataLen > 1) //be kinda pointless if it weren't { buildString.append("SEED: "); - for (int j = 1; j < dataLen; j++) buildString.append(Utility::formatHexNum(data[j]) + " "); + for (int j = 2; j < dataLen; j++) buildString.append(Utility::formatHexNum(data[j]) + " "); } } else @@ -484,13 +484,13 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) break; case UDS_SERVICES::ROUTINE_CTRL: buildString.append("Routine Control: " + getLongDesc(UDS_ROUTINE_SUB, msg.subFunc)); - if (dataLen > 2) + if (dataLen > 3) { int routineID; - routineID = (data[1] * 256 + data[2]); + routineID = (data[2] * 256 + data[3]); buildString.append("\nRoutine ID: " + Utility::formatHexNum(routineID)); } - if (dataLen > 3) + if (dataLen > 4) { buildString.append("\nParameter bytes to routine: "); for (int i = 4; i < dataLen; i++) @@ -501,13 +501,13 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) break; case UDS_SERVICES::ROUTINE_CTRL + 0x40: buildString.append("Routine Control: " + getLongDesc(UDS_ROUTINE_SUB, msg.subFunc)); - if (dataLen > 2) + if (dataLen > 3) { int routineID; - routineID = (data[1] * 256 + data[2]); + routineID = (data[2] * 256 + data[3]); buildString.append("\nRoutine ID: " + Utility::formatHexNum(routineID)); } - if (dataLen > 3) + if (dataLen > 4) { buildString.append("\nBytes returned by routine: "); for (int i = 4; i < dataLen; i++) diff --git a/re/isotp_interpreterwindow.cpp b/re/isotp_interpreterwindow.cpp index 50ff657..d8a15cf 100644 --- a/re/isotp_interpreterwindow.cpp +++ b/re/isotp_interpreterwindow.cpp @@ -73,7 +73,7 @@ void ISOTP_InterpreterWindow::showEvent(QShowEvent* event) qApp->processEvents(); - decoder->updatedFrames(-2); + decoder->rapidFrames(nullptr, *modelFrames); progress.cancel(); From 43979a6d5849df4fae742dab525faa173a97fca0 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Thu, 2 Apr 2020 19:53:35 -0400 Subject: [PATCH 25/58] Many fixes to the IsoTP Viewer and UDS internal code. --- bus_protocols/isotp_handler.cpp | 29 +++++++++---- bus_protocols/isotp_handler.h | 2 + bus_protocols/uds_handler.cpp | 76 +++++++++++++++++++++++++++++---- re/isotp_interpreterwindow.cpp | 5 ++- 4 files changed, 93 insertions(+), 19 deletions(-) diff --git a/bus_protocols/isotp_handler.cpp b/bus_protocols/isotp_handler.cpp index c741866..553bd91 100644 --- a/bus_protocols/isotp_handler.cpp +++ b/bus_protocols/isotp_handler.cpp @@ -7,6 +7,7 @@ ISOTP_HANDLER::ISOTP_HANDLER() isReceiving = false; issueFlowMsgs = false; processAll = false; + sendPartialMessages = false; lastSenderBus = 0; lastSenderID = 0; @@ -106,9 +107,11 @@ void ISOTP_HANDLER::updatedFrames(int numFrames) { if (numFrames == -1) //all frames deleted. Kill the display { + messageBuffer.clear(); } else if (numFrames == -2) //all new set of frames. Reset { + messageBuffer.clear(); for (int i = 0; i < modelFrames->length(); i++) processFrame(modelFrames->at(i)); } else //just got some new frames. See if they are relevant. @@ -125,7 +128,7 @@ void ISOTP_HANDLER::rapidFrames(const CANConnection* conn, const QVector(frame.payload().constData()); - qDebug() << frame.payload().count(); + //qDebug() << frame.payload().count(); //int dataLen = frame.payload().count(); frameType = 0; @@ -196,17 +199,19 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) msg.setTimeStamp(frame.timeStamp()); msg.isMultiframe = false; if (useExtendedAddressing) - { + { for (int j = 0; j < frameLen; j++) { - dataBytes.append(data[j+2]); + if (frame.payload().count() > (j+2)) + dataBytes.append(data[j+2]); } } else { for (int j = 0; j < frameLen; j++) { - dataBytes.append(data[j+1]); + if (frame.payload().count() > (j+1)) + dataBytes.append(data[j+1]); } } qDebug() << "Emitting single frame ISOTP message"; @@ -216,6 +221,7 @@ void ISOTP_HANDLER::processFrame(const CANFrame &frame) case 1: //first frame of a multi-frame message checkNeedFlush(ID); msg.bus = frame.bus; + if (frame.payload().count() < 8) return; //MUST have all 8 data bytes in this first frame. msg.setExtendedFrameFormat( frame.hasExtendedFrameFormat() ); msg.setFrameId(ID); msg.setTimeStamp(frame.timeStamp()); @@ -323,12 +329,17 @@ void ISOTP_HANDLER::checkNeedFlush(uint64_t ID) if (msg->reportedLength <= msg->payload().count()) { qDebug() << "Flushing full frame" << QString::number(msg->frameId(), 16) << " " << msg->reportedLength << " " << msg->payload().count(); + if (msg->reportedLength > 0) emit newISOMessage(*msg); } else { - qDebug() << "Flushing a partial frame " << QString::number(msg->frameId(), 16) << " " << msg->reportedLength << " " << msg->payload().count(); - } - if (msg->reportedLength > 0) emit newISOMessage(*msg); + if (sendPartialMessages) + { + qDebug() << "Flushing a partial frame " << QString::number(msg->frameId(), 16) << " " << msg->reportedLength << " " << msg->payload().count(); + if (msg->reportedLength > 0) emit newISOMessage(*msg); + } + else qDebug() << "Have a partial message but sending of such is disabled. Throwing it away"; + } messageBuffer.remove(ID); } } @@ -389,4 +400,4 @@ void ISOTP_HANDLER::clearAllFilters() filters.clear(); } - +void setEmitPartials(bool mode); diff --git a/bus_protocols/isotp_handler.h b/bus_protocols/isotp_handler.h index 68d49b1..690e05e 100644 --- a/bus_protocols/isotp_handler.h +++ b/bus_protocols/isotp_handler.h @@ -19,6 +19,7 @@ public: ~ISOTP_HANDLER(); void setExtendedAddressing(bool mode); void setReception(bool mode); //set whether to accept and forward frames or not + void setEmitPartials(bool mode); void sendISOTPFrame(int bus, int ID, QByteArray data); void setProcessAll(bool state); void setFlowCtrl(bool state); @@ -45,6 +46,7 @@ private: int framesUntilFlow; bool processAll; bool issueFlowMsgs; + bool sendPartialMessages; QTimer frameTimer; uint32_t lastSenderID; uint32_t lastSenderBus; diff --git a/bus_protocols/uds_handler.cpp b/bus_protocols/uds_handler.cpp index 48f2d99..2ece46b 100644 --- a/bus_protocols/uds_handler.cpp +++ b/bus_protocols/uds_handler.cpp @@ -33,6 +33,14 @@ static QVector UDS_ROUTINE_SUB = { {3,"GET_ROUTINE_RESULTS", "Get results from routine specified by ID"}, }; +static QVector UDS_FILE_MODEOFOP = { + {1, "ADDFILE", "Add file to file system"}, + {2, "DELETEFILE", "Add file to file system"}, + {3, "REPLACEFILE", "Add file to file system"}, + {4, "READFILE", "Add file to file system"}, + {5, "READDIR", "Add file to file system"} +}; + static QVector UDS_SERVICE_DESC = { {1, "OBDII_SHOW_CURRENT", "OBDII - Show current data"}, {2, "OBDII_SHOW_FREEZE", "OBDII - Show freeze data"}, @@ -328,6 +336,7 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) QString buildString; int dataSize; int addrSize; + int compType, encType; const unsigned char *data = reinterpret_cast(msg.payload().constData()); int dataLen = msg.payload().length(); @@ -480,20 +489,36 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) buildString.append(Utility::formatHexNum(data[i]) + " "); } break; - case UDS_SERVICES::WRITE_BY_ID: + case UDS_SERVICES::WRITE_BY_ID: + if (dataLen > 3) + { + int writeID = (data[1] * 256 + data[2]); + buildString.append("ID to write to: " + Utility::formatHexNum(writeID) + "\nPayload: "); + for (int i = 3; i < dataLen; i++) + { + buildString.append(Utility::formatHexNum(data[i]) + " "); + } + } + break; + case UDS_SERVICES::WRITE_BY_ID + 0x40: + if (dataLen > 2) + { + int writeID = (data[1] * 256 + data[2]); + buildString.append("ID written to: " + Utility::formatHexNum(writeID)); + } break; case UDS_SERVICES::ROUTINE_CTRL: buildString.append("Routine Control: " + getLongDesc(UDS_ROUTINE_SUB, msg.subFunc)); if (dataLen > 3) { int routineID; - routineID = (data[2] * 256 + data[3]); + routineID = (data[1] * 256 + data[2]); buildString.append("\nRoutine ID: " + Utility::formatHexNum(routineID)); } if (dataLen > 4) { buildString.append("\nParameter bytes to routine: "); - for (int i = 4; i < dataLen; i++) + for (int i = 3; i < dataLen; i++) { buildString.append(Utility::formatHexNum(data[i]) + " "); } @@ -501,28 +526,63 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) break; case UDS_SERVICES::ROUTINE_CTRL + 0x40: buildString.append("Routine Control: " + getLongDesc(UDS_ROUTINE_SUB, msg.subFunc)); - if (dataLen > 3) + if (dataLen > 2) { int routineID; - routineID = (data[2] * 256 + data[3]); + routineID = (data[1] * 256 + data[2]); buildString.append("\nRoutine ID: " + Utility::formatHexNum(routineID)); } - if (dataLen > 4) + if (dataLen > 3) { buildString.append("\nBytes returned by routine: "); - for (int i = 4; i < dataLen; i++) + for (int i = 3; i < dataLen; i++) { buildString.append(Utility::formatHexNum(data[i]) + " "); } } break; case UDS_SERVICES::REQUEST_DOWNLOAD: - break; case UDS_SERVICES::REQUEST_UPLOAD: + compType = data[1] >> 4; + encType = data[1] & 0xF; + buildString.append("Compression Type: " + QString(compType) + "\n"); + buildString.append("Encryption Type: " + QString(encType) + "\n"); + //subfunc byte specifies address and length format, then address, then size + dataSize = data[2] >> 4; + addrSize = data[2] & 0xF; + if (dataLen > (dataSize + addrSize)) + { + buildString.append("Address: 0x"); + for (int i = 0; i < addrSize; i++) buildString.append(QString::number(data[3 + i], 16).toUpper().rightJustified(2,'0')); + buildString.append("\nSize: 0x"); + for (int i = 0; i < dataSize; i++) buildString.append(QString::number(data[3 + i + addrSize], 16).toUpper().rightJustified(2,'0')); + } + else + { + buildString.append("Message has insufficient bytes to properly decode address and size!"); + } + break; + case UDS_SERVICES::REQUEST_DOWNLOAD + 0x40: + case UDS_SERVICES::REQUEST_UPLOAD + 0x40: + dataSize = data[1] >> 4; + buildString.append("\nMax Size of data block: 0x"); + for (int i = 0; i < dataSize; i++) buildString.append(QString::number(data[2 + i], 16).toUpper().rightJustified(2,'0')); break; case UDS_SERVICES::TRANSFER_DATA: + case UDS_SERVICES::TRANSFER_DATA + 0x40: + buildString.append("\nBlock Sequence: " + QString(data[1]) + "\nPayload: "); + for (int i = 2; i < dataLen; i++) + { + buildString.append(Utility::formatHexNum(data[i]) + " "); + } break; case UDS_SERVICES::REQ_TRANS_EXIT: + case UDS_SERVICES::REQ_TRANS_EXIT + 0x40: + buildString.append("\nPayload: "); + for (int i = 1; i < dataLen; i++) + { + buildString.append(Utility::formatHexNum(data[i]) + " "); + } break; case UDS_SERVICES::REQ_FILE_TRANS: break; diff --git a/re/isotp_interpreterwindow.cpp b/re/isotp_interpreterwindow.cpp index d8a15cf..9d5e440 100644 --- a/re/isotp_interpreterwindow.cpp +++ b/re/isotp_interpreterwindow.cpp @@ -73,7 +73,8 @@ void ISOTP_InterpreterWindow::showEvent(QShowEvent* event) qApp->processEvents(); - decoder->rapidFrames(nullptr, *modelFrames); + decoder->updatedFrames(-2); + //decoder->rapidFrames(nullptr, *modelFrames); progress.cancel(); @@ -130,7 +131,7 @@ void ISOTP_InterpreterWindow::writeSettings() void ISOTP_InterpreterWindow::interpretCapturedFrames() { clearList(); - decoder->rapidFrames(nullptr, *modelFrames); + decoder->updatedFrames(-2); } void ISOTP_InterpreterWindow::listFilterItemChanged(QListWidgetItem *item) From ccab975a70f16cc3aaa435b9927cc80f33831cf9 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Thu, 2 Apr 2020 19:55:04 -0400 Subject: [PATCH 26/58] Fixes for loading tesla json files (if you somehow get some) --- dbc/dbchandler.cpp | 29 +++------------------ dbc/dbchandler.h | 2 +- dbc/dbcloadsavewindow.cpp | 54 ++++++++++++++++++++++++--------------- ui/dbcloadsavewindow.ui | 7 ----- 4 files changed, 39 insertions(+), 53 deletions(-) diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index 15b2207..a8d09da 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -1505,28 +1505,12 @@ DBCFile* DBCHandler::loadDBCFile(int idx) return nullptr; } -DBCFile* DBCHandler::loadJSONFile(int idx) +DBCFile* DBCHandler::loadJSONFile(QString filename) { - if (idx > -1 && idx < loadedFiles.count()) removeDBCFile(idx); - - QString filename; - QFileDialog dialog; QSettings settings; DBCFile *thisFile; DBC_MESSAGE *pMsg; - QStringList filters; - filters.append(QString(tr("JSON File (*.json *.JSON)"))); - - dialog.setDirectory(settings.value("DBC/LoadSaveDirectory", dialog.directory().path()).toString()); - dialog.setFileMode(QFileDialog::ExistingFile); - dialog.setNameFilters(filters); - dialog.setViewMode(QFileDialog::Detail); - - if (dialog.exec() == QDialog::Accepted) - { - filename = dialog.selectedFiles()[0]; - //right now there is only one file type that can be loaded here so just do it. createBlankFile(); thisFile = &loadedFiles.last(); QFile *inFile = new QFile(filename); @@ -1543,7 +1527,7 @@ DBCFile* DBCHandler::loadJSONFile(int idx) QJsonDocument jsonDoc = QJsonDocument::fromJson(wholeFileData); if (jsonDoc.isNull()) { - qDebug() << "Couldn't load the json file, shit the bed, don't know why I did it."; + qDebug() << "Couldn't load and parse the JSON file for some reason."; return nullptr; } qDebug() << "Loaded JSON"; @@ -1594,8 +1578,8 @@ DBCFile* DBCHandler::loadJSONFile(int idx) sig.name = QString(sigIter.key().toUtf8()); sig.factor = sigObj.find("scale").value().toDouble(); sig.bias = sigObj.find("offset").value().toDouble(); - sig.max = sigObj.find("max").value().toInt(); - sig.min = sigObj.find("min").value().toInt(); + sig.max = sigObj.find("max").value().toDouble(); + sig.min = sigObj.find("min").value().toDouble(); sig.startBit = sigObj.find("start_position").value().toInt(); sig.unitName = sigObj.find("units").value().toString(); sig.signalSize = sigObj.find("width").value().toInt(); @@ -1669,12 +1653,7 @@ DBCFile* DBCHandler::loadJSONFile(int idx) } } } - - settings.setValue("DBC/LoadSaveDirectory", dialog.directory().path()); return thisFile; - } - - return nullptr; } void DBCHandler::removeDBCFile(int idx) diff --git a/dbc/dbchandler.h b/dbc/dbchandler.h index 0dfd3fe..f7b6c25 100644 --- a/dbc/dbchandler.h +++ b/dbc/dbchandler.h @@ -113,7 +113,7 @@ public: DBCFile* getFileByIdx(int idx); DBCFile* getFileByName(QString name); int createBlankFile(); - DBCFile* loadJSONFile(int); + DBCFile* loadJSONFile(QString); static DBCHandler *getReference(); private: diff --git a/dbc/dbcloadsavewindow.cpp b/dbc/dbcloadsavewindow.cpp index 5cdedd9..cd47371 100644 --- a/dbc/dbcloadsavewindow.cpp +++ b/dbc/dbcloadsavewindow.cpp @@ -1,6 +1,7 @@ #include "dbcloadsavewindow.h" #include "ui_dbcloadsavewindow.h" #include +#include #include #include #include "helpwindow.h" @@ -67,7 +68,6 @@ DBCLoadSaveWindow::DBCLoadSaveWindow(const QVector *frames, QWidget *p connect(ui->btnNewDBC, &QAbstractButton::clicked, this, &DBCLoadSaveWindow::newFile); connect(ui->tableFiles, &QTableWidget::cellChanged, this, &DBCLoadSaveWindow::cellChanged); connect(ui->tableFiles, &QTableWidget::cellDoubleClicked, this, &DBCLoadSaveWindow::cellDoubleClicked); - connect(ui->btnLoadJSON, &QAbstractButton::clicked, this, &DBCLoadSaveWindow::loadJSON); editorWindow = new DBCMainEditor(frames, this); currentlyEditingFile = nullptr; @@ -153,7 +153,38 @@ void DBCLoadSaveWindow::newFile() void DBCLoadSaveWindow::loadFile() { - DBCFile *file = dbcHandler->loadDBCFile(-1); + DBCFile *file = nullptr; + QString filename; + QFileDialog dialog; + QSettings settings; + + QStringList filters; + filters.append(QString(tr("DBC File (*.dbc)"))); + filters.append(QString(tr("Tesla JSON File (*.json)"))); + + dialog.setDirectory(settings.value("DBC/LoadSaveDirectory", dialog.directory().path()).toString()); + dialog.setFileMode(QFileDialog::ExistingFile); + dialog.setNameFilters(filters); + dialog.setViewMode(QFileDialog::Detail); + + if (dialog.exec() == QDialog::Accepted) + { + filename = dialog.selectedFiles()[0]; + //right now there is only one file type that can be loaded here so just do it. + settings.setValue("DBC/LoadSaveDirectory", dialog.directory().path()); + + if (dialog.selectedNameFilter() == filters[0]) + { + if (!filename.contains('.')) filename += ".dbc"; + file = dbcHandler->loadDBCFile(filename); + } + if (dialog.selectedNameFilter() == filters[1]) + { + if (!filename.contains('.')) filename += ".json"; + file = dbcHandler->loadJSONFile(filename); + } + } + if(file) { inhibitCellProcessing=true; int idx = ui->tableFiles->rowCount(); @@ -187,24 +218,7 @@ void DBCLoadSaveWindow::loadFile() void DBCLoadSaveWindow::loadJSON() { - DBCFile *file = dbcHandler->loadJSONFile(-1); - if(file) { - int idx = ui->tableFiles->rowCount(); - ui->tableFiles->insertRow(ui->tableFiles->rowCount()); - ui->tableFiles->setItem(idx, 0, new QTableWidgetItem(file->getFullFilename())); - ui->tableFiles->setItem(idx, 1, new QTableWidgetItem("-1")); - DBC_ATTRIBUTE *attr = file->findAttributeByName("isj1939dbc"); - QTableWidgetItem *item = new QTableWidgetItem(""); - ui->tableFiles->setItem(idx, 2, item); - if (attr && attr->defaultValue > 0) - { - item->setCheckState(Qt::Checked); - } - else - { - item->setCheckState(Qt::Unchecked); - } - } + } void DBCLoadSaveWindow::saveFile() diff --git a/ui/dbcloadsavewindow.ui b/ui/dbcloadsavewindow.ui index 135a4d5..4041084 100644 --- a/ui/dbcloadsavewindow.ui +++ b/ui/dbcloadsavewindow.ui @@ -24,13 +24,6 @@ - - - - Load Tesla JSON (shhhh....) - - - From 503e662febe544e79b491c19eeb462e9c06a57a8 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Thu, 2 Apr 2020 20:43:02 -0400 Subject: [PATCH 27/58] Fixed a problem with Canalyzer ASC loading. --- framefileio.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/framefileio.cpp b/framefileio.cpp index a931a63..828a56c 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -1311,13 +1311,24 @@ bool FrameFileIO::loadCanalyzerASC(QString filename, QVector* frames) thisFrame.setFrameId(tokens[2].toUInt(nullptr, 16)); thisFrame.setExtendedFrameFormat(false); } - QByteArray bytes(tokens[5].toInt(), 0); - if (thisFrame.payload().length() > 8) return false; - if (thisFrame.payload().length() < 0) return false; + + int payloadLen = tokens[5].toInt(); + QByteArray bytes(payloadLen, 0); + + if (payloadLen > 8) + { + qDebug() << "Payload length too long. Original line: " << line; + return false; + } + if (payloadLen < 0) + { + qDebug() << "Payload length negative! Original line: " << line; + return false; + } thisFrame.isReceived = tokens[3].toUpper().contains("RX"); thisFrame.bus = tokens[1].toInt(); if (tokens[4] == "r") thisFrame.setFrameType(QCanBusFrame::RemoteRequestFrame); - for (int d = 6; d < (6 + static_cast(thisFrame.payload().length())); d++) + for (int d = 6; d < (6 + payloadLen); d++) { if (tokens.count() > d) { @@ -1325,8 +1336,10 @@ bool FrameFileIO::loadCanalyzerASC(QString filename, QVector* frames) } else //expected byte wasn't there to read. Set it zero and set error flag { - bytes[d - 6] = 0; - foundErrors = true; + bytes[d - 6] = 0; + foundErrors = true; + qDebug() << "D:" << d << " Count:" << tokens.count(); + qDebug() << "Expected byte missing! Original line: " << line; } } thisFrame.setPayload(bytes); From 9c143c2b12029fa311ba4922f9c94e80b7301741 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Thu, 2 Apr 2020 21:47:20 -0400 Subject: [PATCH 28/58] Add two new buttons to expand or collapse all signals. --- mainwindow.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ mainwindow.h | 2 ++ ui/mainwindow.ui | 40 ++++++++++++++++++++++++++++++++++------ utility.h | 4 ++++ 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 08a096e..ab8e7c5 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -129,6 +129,8 @@ MainWindow::MainWindow(QWidget *parent) : connect(ui->actionSignal_Viewer, &QAction::triggered, this, &MainWindow::showSignalViewer); connect(ui->actionSave_Continuous_Logfile, &QAction::triggered, this, &MainWindow::handleContinousLogging); connect(ui->actionTemporal_Graph, &QAction::triggered, this, &MainWindow::showTemporalGraphWindow); + connect(ui->btnExpandAll, &QAbstractButton::clicked, this, &MainWindow::expandAllRows); + connect(ui->btnCollapseAll, &QAbstractButton::clicked, this, &MainWindow::collapseAllRows); connect(CANConManager::getInstance(), &CANConManager::framesReceived, model, &CANFrameModel::addFrames); @@ -382,6 +384,48 @@ void MainWindow::headerClicked(int logicalIndex) model->sortByColumn(logicalIndex); } +void MainWindow::expandAllRows() +{ + bool goAhead = false; + int numRows = ui->canFramesView->model()->rowCount(); + + if (numRows > 20000) + { + QMessageBox::StandardButton confirmDialog; + confirmDialog = QMessageBox::question(this, "Really?", "It's not recommended to use this\non more than 20000 frames.\nIt can take a long time.\n\nYou have been warned!\nStill do it?", + QMessageBox::Yes|QMessageBox::No); + + if (confirmDialog == QMessageBox::Yes) goAhead = true; + } + else goAhead = true; + + if (goAhead) + { + ui->canFramesView->resizeRowsToContents(); + } +} + +void MainWindow::collapseAllRows() +{ + bool goAhead = false; + int numRows = ui->canFramesView->model()->rowCount(); + + if (numRows > 50000) + { + QMessageBox::StandardButton confirmDialog; + confirmDialog = QMessageBox::question(this, "Really?", "It's not recommended to use this\non more than 50000 frames.\nIt can take a long time.\n\nYou have been warned!\nStill do it?", + QMessageBox::Yes|QMessageBox::No); + + if (confirmDialog == QMessageBox::Yes) goAhead = true; + } + else goAhead = true; + + if (goAhead) + { + for (int i = 0; i < numRows; i++) ui->canFramesView->setRowHeight(i, normalRowHeight); + } +} + void MainWindow::gridClicked(QModelIndex idx) { //qDebug() << "Grid Clicked"; diff --git a/mainwindow.h b/mainwindow.h index e780f88..d3a45dd 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -63,6 +63,8 @@ private slots: void showGraphingWindow(); void showFrameDataAnalysis(); void clearFrames(); + void expandAllRows(); + void collapseAllRows(); void showPlaybackWindow(); void showFlowViewWindow(); void showFrameSenderWindow(); diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index 1bda8d1..46acf44 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -191,6 +191,23 @@ + + + + true + + + Overwrite Mode + + + + + + + Qt::Horizontal + + + @@ -199,12 +216,23 @@ - - - true - + - Overwrite Mode + Expand All Rows + + + + + + + Collapse All Rows + + + + + + + Qt::Horizontal @@ -265,7 +293,7 @@ 0 0 800 - 22 + 23 diff --git a/utility.h b/utility.h index 090b0a3..ab2d44d 100644 --- a/utility.h +++ b/utility.h @@ -194,6 +194,8 @@ public: for (int bitpos = 0; bitpos < sigSize; bitpos++) { if (bit < 64) { + int bytePos = bit / 8; + if (bytePos >= data.count()) return 0; //error! if (data[bit / 8] & (1 << (bit % 8))) result += (1ULL << bitpos); } @@ -206,6 +208,8 @@ public: for (int bitpos = 0; bitpos < sigSize; bitpos++) { if (bit < 64) { + int bytePos = bit / 8; + if (bytePos >= data.count()) return 0; //error! if (data[bit / 8] & (1 << (bit % 8))) result += (1ULL << (sigSize - bitpos - 1)); } From 2a1144cfe984513a591a13a32e47b3671ad32835 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 13 Jul 2020 20:11:25 -0400 Subject: [PATCH 29/58] Another attempt to fix windows build --- .travis.yml | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5004cc0..20fefc9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,13 @@ language: cpp -# Any time a commit is done to either master or WIP we do a CI build on all three platforms to make sure it really builds. The linux version will be saved to the continuous tag -# and thus available for testing. All three platforms will get a proper release upon a tagged commit. +# Any time a commit is done to either master or WIP we do a CI build on all three platforms to make +# sure it really builds. The linux version will be saved to the continuous tag and thus available +# for testing. All three platforms will get a proper release upon a tagged commit. + +env: + global: + - QT_INSTALL_DIR=~/Qt + - QT_VERSION=5.15.0 matrix: include: @@ -65,14 +71,16 @@ matrix: draft: false - os: windows + env: + QMAKE_PATH=${QT_INSTALL_DIR}/${QT_VERSION}/msvc2017_64/bin/qmake.exe + QDEPLOY_PATH=${QT_INSTALL_DIR}/${QT_VERSION}/msvc2017_64/bin/windeployqt.exe before_install: - - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" - - curl -vLO http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe - - ./qt-unified-windows-x86-online.exe --verbose --script qt-installer-windows.qs + - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" + - ./install-qt.sh -d ${QT_INSTALL_DIR} --version ${QT_VERSION} --toolchain win64_msvc2017_64 qtbase qttools qtconnectivity qtimageformats qtserialport qtserialbus qtdoc script: - - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" '&&' cd '&&' "C:/Qt/5.12.3/msvc2017/bin/qmake" CONFIG+=RELEASE SavvyCAN.pro '&&' cd '&&' nmake + - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" '&&' cd '&&' ${QMAKE_PATH} CONFIG+=RELEASE SavvyCAN.pro '&&' cd '&&' nmake - ls - - cmd.exe //C cd release '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars32.bat" '&&' "C:/Qt/5.12.3/msvc2017/bin/windeployqt.exe" ./SavvyCAN.exe + - cmd.exe //C cd release '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" '&&' ${QDEPLOY_PATH} ./SavvyCAN.exe - cd release - ls - rm *.cpp @@ -81,13 +89,13 @@ matrix: - mkdir help - cp -R ../help/*.* ./help - cp -r ../examples . - - 7z a -tzip SavvyCAN32.zip -r * + - 7z a -tzip SavvyCAN.zip -r * deploy: provider: releases skip_cleanup: true api_key: $GITHUB_TOKEN - file: SavvyCAN32.zip + file: SavvyCAN.zip on: tags: true draft: false From c827e4cdd6f2bdca5c90ba085904ab539f406e66 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 13 Jul 2020 20:17:13 -0400 Subject: [PATCH 30/58] Add the qt installer script I forgot in last commit. --- install-qt.sh | 319 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 install-qt.sh diff --git a/install-qt.sh b/install-qt.sh new file mode 100644 index 0000000..be953e7 --- /dev/null +++ b/install-qt.sh @@ -0,0 +1,319 @@ +#!/usr/bin/env bash +############################################################################# +## +## Copyright (C) 2019 Richard Weickelt. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qbs. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# +set -eu + +function help() { + cat < + Root directory where to install the components. + Maps to C:/Qt on Windows, /opt/Qt on Linux, /usr/local/Qt on Mac + by default. + + -f, --force + Force download and do not attempt to re-use an existing installation. + + --host + The host operating system. Can be one of linux_x64, mac_x64, + windows_x86. Auto-detected by default. + + --target + The desired target platform. Can be one of desktop, android, ios. + The default value is desktop. + + --toolchain + The toolchain that has been used to build the binaries. + Possible values depend on --host and --target, respectively: + + linux_x64 + android + any, android_armv7, android_arm64_v8a + desktop + gcc_64 (default) + + mac_x64 + android + any, android_armv7, android_arm64_v8a + desktop + clang_64 (default), + ios + ios + + windows_x86 + android + any, android_armv7, android_arm64_v8a + desktop + win64_mingw73, win64_msvc2017_64 (default) + + --version + The desired Qt version. Currently supported are all versions + above 5.9.0. + +EOF +} + +TARGET_PLATFORM=desktop +COMPONENTS= +VERSION= +FORCE_DOWNLOAD=false +MD5_TOOL=md5sum + +case "$OSTYPE" in + *linux*) + HOST_OS=linux_x64 + INSTALL_DIR=/opt/Qt + TOOLCHAIN=gcc_64 + ;; + *darwin*) + HOST_OS=mac_x64 + INSTALL_DIR=/usr/local/Qt + TOOLCHAIN=clang_64 + MD5_TOOL="md5 -r" + ;; + msys) + HOST_OS=windows_x86 + INSTALL_DIR=/c/Qt + TOOLCHAIN=win64_msvc2015_64 + ;; + *) + HOST_OS= + INSTALL_DIR= + ;; +esac + +while [ $# -gt 0 ]; do + case "$1" in + --directory|-d) + INSTALL_DIR="$2" + shift + ;; + --force|-f) + FORCE_DOWNLOAD=true + ;; + --host) + HOST_OS="$2" + shift + ;; + --target) + TARGET_PLATFORM="$2" + shift + ;; + --toolchain) + TOOLCHAIN=$(echo $2 | tr '[A-Z]' '[a-z]') + shift + ;; + --version) + VERSION="$2" + shift + ;; + --help|-h) + help + exit 0 + ;; + *) + COMPONENTS="${COMPONENTS} $1" + ;; + esac + shift +done + +if [ -z "${HOST_OS}" ]; then + echo "No --host specified or auto-detection failed." >&2 + exit 1 +fi + +if [ -z "${INSTALL_DIR}" ]; then + echo "No --directory specified or auto-detection failed." >&2 + exit 1 +fi + +if [ -z "${VERSION}" ]; then + echo "No --version specified." >&2 + exit 1 +fi + +if [ -z "${COMPONENTS}" ]; then + echo "No components specified." >&2 + exit 1 +fi + +case "$TARGET_PLATFORM" in + android) + ;; + ios) + ;; + desktop) + ;; + *) + echo "Error: TARGET_PLATFORM=${TARGET_PLATFORM} is not valid." >&2 + exit 1 + ;; +esac + +HASH=$(echo "${OSTYPE} ${TARGET_PLATFORM} ${TOOLCHAIN} ${VERSION} ${INSTALL_DIR}" | ${MD5_TOOL} | head -c 16) +HASH_FILEPATH="${INSTALL_DIR}/${HASH}.manifest" +INSTALLATION_IS_VALID=false +if ! ${FORCE_DOWNLOAD} && [ -f "${HASH_FILEPATH}" ]; then + INSTALLATION_IS_VALID=true + while read filepath; do + if [ ! -e "${filepath}" ]; then + INSTALLATION_IS_VALID=false + break + fi + done <"${HASH_FILEPATH}" +fi + +if ${INSTALLATION_IS_VALID}; then + echo "Already installed. Skipping download." + exit 0 +fi + +DOWNLOAD_DIR=`mktemp -d 2>/dev/null || mktemp -d -t 'install-qt'` + +# +# The repository structure is a mess. Try different URL variants +# +function compute_url(){ + local COMPONENT=$1 + local CURL="curl -s -L" + local BASE_URL="http://download.qt.io/online/qtsdkrepository/${HOST_OS}/${TARGET_PLATFORM}" + + if [[ "${COMPONENT}" =~ "qtcreator" ]]; then + + REMOTE_BASE="tools_qtcreator/qt.tools.qtcreator" + REMOTE_PATH="$(${CURL} ${BASE_URL}/${REMOTE_BASE}/ | grep -o -E "${VERSION}[0-9\-]*${COMPONENT}\.7z" | tail -1)" + + if [ ! -z "${REMOTE_PATH}" ]; then + echo "${BASE_URL}/${REMOTE_BASE}/${REMOTE_PATH}" + return 0 + fi + + else + REMOTE_BASES=( + # New repository format (>=5.9.6) + "qt5_${VERSION//./}/qt.qt5.${VERSION//./}.${TOOLCHAIN}" + "qt5_${VERSION//./}/qt.qt5.${VERSION//./}.${COMPONENT}.${TOOLCHAIN}" + # Multi-abi Android since 5.14 + "qt5_${VERSION//./}/qt.qt5.${VERSION//./}.${TARGET_PLATFORM}" + "qt5_${VERSION//./}/qt.qt5.${VERSION//./}.${COMPONENT}.${TARGET_PLATFORM}" + # Older repository format (<5.9.0) + "qt5_${VERSION//./}/qt.${VERSION//./}.${TOOLCHAIN}" + "qt5_${VERSION//./}/qt.${VERSION//./}.${COMPONENT}.${TOOLCHAIN}" + ) + + for REMOTE_BASE in ${REMOTE_BASES[*]}; do + REMOTE_PATH="$(${CURL} ${BASE_URL}/${REMOTE_BASE}/ | grep -o -E "[[:alnum:]_.\-]*7z" | grep "${COMPONENT}" | tail -1)" + if [ ! -z "${REMOTE_PATH}" ]; then + echo "${BASE_URL}/${REMOTE_BASE}/${REMOTE_PATH}" + return 0 + fi + done + fi + + echo "Could not determine a remote URL for ${COMPONENT} with version ${VERSION}">&2 + exit 1 +} + +mkdir -p ${INSTALL_DIR} +rm -f "${HASH_FILEPATH}" + +for COMPONENT in ${COMPONENTS}; do + + URL="$(compute_url ${COMPONENT})" + echo "Downloading ${COMPONENT}..." >&2 + curl --progress-bar -L -o ${DOWNLOAD_DIR}/package.7z ${URL} >&2 + 7z x -y -o${INSTALL_DIR} ${DOWNLOAD_DIR}/package.7z >/dev/null 2>&1 + 7z l -ba -slt -y ${DOWNLOAD_DIR}/package.7z | tr '\\' '/' | sed -n -e "s|^Path\ =\ |${INSTALL_DIR}/|p" >> "${HASH_FILEPATH}" 2>/dev/null + rm -f ${DOWNLOAD_DIR}/package.7z + + # + # conf file is needed for qmake + # + if [ "${COMPONENT}" == "qtbase" ]; then + if [[ "${TOOLCHAIN}" =~ "win64_mingw" ]]; then + SUBDIR="${TOOLCHAIN/win64_/}_64" + elif [[ "${TOOLCHAIN}" =~ "win32_mingw" ]]; then + SUBDIR="${TOOLCHAIN/win32_/}_32" + elif [[ "${TOOLCHAIN}" =~ "win64_msvc" ]]; then + SUBDIR="${TOOLCHAIN/win64_/}" + elif [[ "${TOOLCHAIN}" =~ "win32_msvc" ]]; then + SUBDIR="${TOOLCHAIN/win32_/}" + elif [[ "${TOOLCHAIN}" =~ "any" ]] && [[ "${TARGET_PLATFORM}" == "android" ]]; then + SUBDIR="android" + else + SUBDIR="${TOOLCHAIN}" + fi + + CONF_FILE="${INSTALL_DIR}/${VERSION}/${SUBDIR}/bin/qt.conf" + echo "[Paths]" > ${CONF_FILE} + echo "Prefix = .." >> ${CONF_FILE} + + # Adjust the license to be able to run qmake + # sed with -i requires intermediate file on Mac OS + PRI_FILE="${INSTALL_DIR}/${VERSION}/${SUBDIR}/mkspecs/qconfig.pri" + sed -i.bak 's/Enterprise/OpenSource/g' "${PRI_FILE}" + sed -i.bak 's/licheck.*//g' "${PRI_FILE}" + rm "${PRI_FILE}.bak" + + # Print the directory so that the caller can + # adjust the PATH variable. + echo $(dirname "${CONF_FILE}") + elif [[ "${COMPONENT}" =~ "qtcreator" ]]; then + echo "${INSTALL_DIR}/Tools/QtCreator/bin" + fi + +done From fb2f8841698eb40227054cfb36f9b52f5ec0ddba Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 13 Jul 2020 20:27:04 -0400 Subject: [PATCH 31/58] Another attempt to fix windows build. Again... --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 20fefc9..6d8441b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ language: cpp env: global: - QT_INSTALL_DIR=~/Qt - - QT_VERSION=5.15.0 + - QT_VERSION=5.14.2 matrix: include: @@ -76,7 +76,7 @@ matrix: QDEPLOY_PATH=${QT_INSTALL_DIR}/${QT_VERSION}/msvc2017_64/bin/windeployqt.exe before_install: - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" - - ./install-qt.sh -d ${QT_INSTALL_DIR} --version ${QT_VERSION} --toolchain win64_msvc2017_64 qtbase qttools qtconnectivity qtimageformats qtserialport qtserialbus qtdoc + - ./install-qt.sh -d ${QT_INSTALL_DIR} --version ${QT_VERSION} --toolchain win64_msvc2017_64 qtbase qttools qtconnectivity qtimageformats qtserialport qtserialbus script: - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" '&&' cd '&&' ${QMAKE_PATH} CONFIG+=RELEASE SavvyCAN.pro '&&' cd '&&' nmake - ls From e6e538c98976e47f13c4a8003d368ad4c85544f2 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 13 Jul 2020 20:36:23 -0400 Subject: [PATCH 32/58] More hopefully windows fixes --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6d8441b..87a7d66 100644 --- a/.travis.yml +++ b/.travis.yml @@ -78,9 +78,12 @@ matrix: - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" - ./install-qt.sh -d ${QT_INSTALL_DIR} --version ${QT_VERSION} --toolchain win64_msvc2017_64 qtbase qttools qtconnectivity qtimageformats qtserialport qtserialbus script: - - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" '&&' cd '&&' ${QMAKE_PATH} CONFIG+=RELEASE SavvyCAN.pro '&&' cd '&&' nmake + - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" + - ${QMAKE_PATH} CONFIG+=RELEASE SavvyCAN.pro + - cmd.exe //C nmake - ls - - cmd.exe //C cd release '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" '&&' ${QDEPLOY_PATH} ./SavvyCAN.exe + - cd release + - ${QDEPLOY_PATH} ./SavvyCAN.exe - cd release - ls - rm *.cpp From 90918f6d8c1c0ad6193859ca9e55ce310b123864 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 13 Jul 2020 20:53:04 -0400 Subject: [PATCH 33/58] Try number 1000 for a windows build. Getting closer. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 87a7d66..1bd9e3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -72,8 +72,8 @@ matrix: - os: windows env: - QMAKE_PATH=${QT_INSTALL_DIR}/${QT_VERSION}/msvc2017_64/bin/qmake.exe - QDEPLOY_PATH=${QT_INSTALL_DIR}/${QT_VERSION}/msvc2017_64/bin/windeployqt.exe + QMAKE_PATH=${QT_INSTALL_DIR}/${QT_VERSION}/msvc2017/bin/qmake.exe + QDEPLOY_PATH=${QT_INSTALL_DIR}/${QT_VERSION}/msvc2017/bin/windeployqt.exe before_install: - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" - ./install-qt.sh -d ${QT_INSTALL_DIR} --version ${QT_VERSION} --toolchain win64_msvc2017_64 qtbase qttools qtconnectivity qtimageformats qtserialport qtserialbus From 717228a0917aac4eef787bbb6d06c1f4ebaa0295 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 15 Jul 2020 18:23:18 -0400 Subject: [PATCH 34/58] Added searching to DBC window to make it easier to find messages. Replaced deprecated random number calls with new method. --- dbc/dbchandler.cpp | 14 ++++++++ dbc/dbchandler.h | 1 + dbc/dbcmaineditor.cpp | 79 +++++++++++++++++++++++++++++++++++++++++ dbc/dbcmaineditor.h | 1 + dbc/dbcsignaleditor.cpp | 7 ++-- re/fuzzingwindow.cpp | 11 +++--- re/newgraphdialog.cpp | 7 ++-- ui/dbcmaineditor.ui | 38 ++++++++++++++++++++ 8 files changed, 144 insertions(+), 14 deletions(-) diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index a8d09da..10cacbd 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -151,6 +151,20 @@ DBC_MESSAGE* DBCMessageHandler::findMsgByName(QString name) return nullptr; } +//allow for finding a message just by part of the name +DBC_MESSAGE* DBCMessageHandler::findMsgByPartialName(QString name) +{ + if (messages.count() == 0) return nullptr; + for (int i = 0; i < messages.count(); i++) + { + if (messages[i].name.contains(name, Qt::CaseInsensitive)) + { + return &messages[i]; + } + } + return nullptr; +} + bool DBCMessageHandler::addMessage(DBC_MESSAGE &msg) { messages.append(msg); diff --git a/dbc/dbchandler.h b/dbc/dbchandler.h index f7b6c25..5e83525 100644 --- a/dbc/dbchandler.h +++ b/dbc/dbchandler.h @@ -40,6 +40,7 @@ public: DBC_MESSAGE *findMsgByID(uint32_t id); DBC_MESSAGE *findMsgByIdx(int idx); DBC_MESSAGE *findMsgByName(QString name); + DBC_MESSAGE *findMsgByPartialName(QString name); bool addMessage(DBC_MESSAGE &msg); bool removeMessage(DBC_MESSAGE *msg); bool removeMessageByIndex(int idx); diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index df2e906..f7e3386 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "helpwindow.h" @@ -50,6 +51,9 @@ DBCMainEditor::DBCMainEditor( const QVector *frames, QWidget *parent) connect(ui->MessagesTable, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomMenuMessage(QPoint))); ui->MessagesTable->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->btnSearch, &QAbstractButton::clicked, this, &DBCMainEditor::handleSearch); + connect(ui->lineSearch, &QLineEdit::returnPressed, this, &DBCMainEditor::handleSearch); + sigEditor = new DBCSignalEditor(this); installEventFilter(this); @@ -179,6 +183,81 @@ void DBCMainEditor::deleteCurrentMessage() } } +void DBCMainEditor::handleSearch() +{ + uint32_t msgId = Utility::ParseStringToNum(ui->lineSearch->text()); + if (ui->radioID->isChecked()) + { + DBC_MESSAGE *msg = dbcFile->messageHandler->findMsgByID(msgId); + if (msg) + { + DBC_NODE *node = msg->sender; + if (node) + { + QString nodeName = node->name; + for (int i = 0; i < ui->NodesTable->rowCount(); i++) + { + QTableWidgetItem *item = ui->NodesTable->item(i, 0); + if (item->text() == nodeName) + { + item->setSelected(true); + ui->NodesTable->scrollToItem(item); + onCellClickedNode(i, 0); + for (int j = 0; j < ui->MessagesTable->rowCount(); j++) + { + QTableWidgetItem *msgItem = ui->MessagesTable->item(j, 0); + if (!msgItem) break; + if (Utility::ParseStringToNum(msgItem->text()) == msgId) + { + msgItem->setSelected(true); + ui->MessagesTable->scrollToItem(msgItem); + onCellClickedMessage(j, 0); + break; + } + } + break; + } + } + } + } + } + if (ui->radioName->isChecked()) + { + DBC_MESSAGE *msg = dbcFile->messageHandler->findMsgByPartialName(ui->lineSearch->text()); + if (msg) + { + DBC_NODE *node = msg->sender; + if (node) + { + QString nodeName = node->name; + for (int i = 0; i < ui->NodesTable->rowCount(); i++) + { + QTableWidgetItem *item = ui->NodesTable->item(i, 0); + if (item->text() == nodeName) + { + item->setSelected(true); + ui->NodesTable->scrollToItem(item); + onCellClickedNode(i, 0); + for (int j = 0; j < ui->MessagesTable->rowCount(); j++) + { + QTableWidgetItem *msgItem = ui->MessagesTable->item(j, 1); + if (!msgItem) break; + if (msgItem->text().contains(ui->lineSearch->text(), Qt::CaseInsensitive)) + { + msgItem->setSelected(true); + ui->MessagesTable->scrollToItem(msgItem); + onCellClickedMessage(j, 0); + break; + } + } + break; + } + } + } + } + } +} + void DBCMainEditor::onCellChangedNode(int row,int col) { if (inhibitCellChanged) return; diff --git a/dbc/dbcmaineditor.h b/dbc/dbcmaineditor.h index b92815d..c369c2c 100644 --- a/dbc/dbcmaineditor.h +++ b/dbc/dbcmaineditor.h @@ -29,6 +29,7 @@ private slots: void onCustomMenuMessage(QPoint); void deleteCurrentNode(); void deleteCurrentMessage(); + void handleSearch(); private: Ui::DBCMainEditor *ui; diff --git a/dbc/dbcsignaleditor.cpp b/dbc/dbcsignaleditor.cpp index 6c579ec..4cd27b6 100644 --- a/dbc/dbcsignaleditor.cpp +++ b/dbc/dbcsignaleditor.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "helpwindow.h" @@ -15,8 +16,6 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : readSettings(); - qsrand(QDateTime::currentMSecsSinceEpoch()); - dbcHandler = DBCHandler::getReference(); dbcMessage = nullptr; currentSignal = nullptr; @@ -345,7 +344,7 @@ void DBCSignalEditor::onCustomMenuValues(QPoint point) void DBCSignalEditor::addNewSignal() { - int num = qrand() % 70000; + int num = QRandomGenerator::global()->bounded(70000); QString newName = "SIGNAL" + QString::number(num); DBC_SIGNAL newSig; newSig.name = newName; @@ -373,7 +372,7 @@ void DBCSignalEditor::addNewSignal() void DBCSignalEditor::cloneSignal() { - int num = qrand() % 100; + int num = QRandomGenerator::global()->bounded(100); int idx = ui->signalsList->currentRow(); if (idx < 0) return; diff --git a/re/fuzzingwindow.cpp b/re/fuzzingwindow.cpp index b2c016b..f510f7a 100644 --- a/re/fuzzingwindow.cpp +++ b/re/fuzzingwindow.cpp @@ -2,6 +2,7 @@ #include "ui_fuzzingwindow.h" #include "utility.h" #include +#include #include "mainwindow.h" #include "helpwindow.h" #include "connections/canconmanager.h" @@ -250,12 +251,12 @@ void FuzzingWindow::calcNextID() if (rangeIDSelect) { int range = endID - startID; - if (range != 0) currentID = startID + qrand() % range; + if (range != 0) currentID = startID + QRandomGenerator::global()->bounded(range); else currentID = startID; } else //IDs by filter so pick a random selected ID from the filter list { - currentIdx = qrand() % selectedIDs.length(); + currentIdx = QRandomGenerator::global()->bounded(selectedIDs.length()); currentID = selectedIDs[currentIdx]; } } @@ -277,7 +278,7 @@ void FuzzingWindow::calcNextBitPattern() thisBit = bitGrid[byt * 8 + bit]; if (thisBit == 1) { - if ((qrand() % 2) == 1) currentBytes[byt] |= (1 << bit); + if ((QRandomGenerator::global()->bounded(2)) == 1) currentBytes[byt] |= (1 << bit); } if (thisBit == 2) currentBytes[byt] |= (1 << bit); } @@ -391,12 +392,12 @@ void FuzzingWindow::toggleFuzzing() if (rangeIDSelect) { int range = endID - startID; - if (range != 0) currentID = startID + qrand() % range; + if (range != 0) currentID = startID + QRandomGenerator::global()->bounded(range); else currentID = startID; } else //IDs by filter so pick a random selected ID from the filter list { - currentIdx = qrand() % selectedIDs.length(); + currentIdx = QRandomGenerator::global()->bounded(selectedIDs.length()); currentID = selectedIDs[currentIdx]; } } diff --git a/re/newgraphdialog.cpp b/re/newgraphdialog.cpp index 541458c..f9917c7 100644 --- a/re/newgraphdialog.cpp +++ b/re/newgraphdialog.cpp @@ -1,6 +1,7 @@ #include "newgraphdialog.h" #include "ui_newgraphdialog.h" #include +#include #include "utility.h" #include "helpwindow.h" @@ -15,13 +16,9 @@ NewGraphDialog::NewGraphDialog(DBCHandler *handler, QWidget *parent) : connect(ui->colorSwatch, SIGNAL(clicked(bool)), this, SLOT(colorSwatchClick())); connect(ui->btnAddGraph, SIGNAL(clicked(bool)), this, SLOT(addButtonClicked())); - // Seed the random generator with current time - QTime time = QTime::currentTime(); - qsrand((uint)time.msec()); - QPalette p = ui->colorSwatch->palette(); //using 160 instead of 255 so that colors are always at least a little dark - p.setColor(QPalette::Button, QColor(qrand() % 160,qrand() % 160,qrand() % 160)); + p.setColor(QPalette::Button, QColor(QRandomGenerator::global()->bounded(160),QRandomGenerator::global()->bounded(160), QRandomGenerator::global()->bounded(160))); ui->colorSwatch->setPalette(p); connect(ui->cbMessages, SIGNAL(currentIndexChanged(int)), this, SLOT(loadSignals(int))); diff --git a/ui/dbcmaineditor.ui b/ui/dbcmaineditor.ui index dedb4a2..126452b 100644 --- a/ui/dbcmaineditor.ui +++ b/ui/dbcmaineditor.ui @@ -40,6 +40,44 @@ + + + + + + Find DBC Message: + + + + + + + + + + Msg ID + + + true + + + + + + + Msg Name + + + + + + + Search + + + + + From db47013f82c6ae3b9a376e2617d35b0e96f91d3c Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 15 Jul 2020 18:24:58 -0400 Subject: [PATCH 35/58] A few extra things filled out in UDS decoding --- bus_protocols/uds_handler.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bus_protocols/uds_handler.cpp b/bus_protocols/uds_handler.cpp index 2ece46b..b2dd193 100644 --- a/bus_protocols/uds_handler.cpp +++ b/bus_protocols/uds_handler.cpp @@ -545,8 +545,12 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) case UDS_SERVICES::REQUEST_UPLOAD: compType = data[1] >> 4; encType = data[1] & 0xF; - buildString.append("Compression Type: " + QString(compType) + "\n"); - buildString.append("Encryption Type: " + QString(encType) + "\n"); + buildString.append("Compression Type: " + QString::number(compType)); + if (compType == 0) buildString.append(" (none)"); + buildString.append("\n"); + buildString.append("Encryption Type: " + QString::number(encType)); + if (encType == 0) buildString.append(" (none)"); + buildString.append("\n"); //subfunc byte specifies address and length format, then address, then size dataSize = data[2] >> 4; addrSize = data[2] & 0xF; @@ -570,7 +574,7 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) break; case UDS_SERVICES::TRANSFER_DATA: case UDS_SERVICES::TRANSFER_DATA + 0x40: - buildString.append("\nBlock Sequence: " + QString(data[1]) + "\nPayload: "); + buildString.append("\nBlock Sequence: " + QString::number(data[1], 16) + "\nPayload: "); for (int i = 2; i < dataLen; i++) { buildString.append(Utility::formatHexNum(data[i]) + " "); From 1cb0351c867a2591d7398fab4d8f56c21a29df42 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 15 Jul 2020 18:25:31 -0400 Subject: [PATCH 36/58] Fix gvret code to work with single bus adapters (such as Macchina A0), small change to UDP receiver --- connections/connectionwindow.cpp | 4 ++-- connections/gvretserial.cpp | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/connections/connectionwindow.cpp b/connections/connectionwindow.cpp index 3a5fabf..e63e72a 100644 --- a/connections/connectionwindow.cpp +++ b/connections/connectionwindow.cpp @@ -70,7 +70,7 @@ ConnectionWindow::ConnectionWindow(QWidget *parent) : rxBroadcast = new QUdpSocket(this); //Need to make sure it tries to share the address in case there are //multiple instances of SavvyCAN running. - rxBroadcast->bind(QHostAddress::AnyIPv4, 17222, QAbstractSocket::ShareAddress); + rxBroadcast->bind(QHostAddress::Any, 17222, QAbstractSocket::ShareAddress); connect(rxBroadcast, SIGNAL(readyRead()), this, SLOT(readPendingDatagrams())); @@ -85,7 +85,7 @@ void ConnectionWindow::readPendingDatagrams() if (!remoteDeviceIP.contains(datagram.senderAddress().toString())) { remoteDeviceIP.append(datagram.senderAddress().toString()); - //qDebug() << "Add new remote IP " << datagram.senderAddress().toString(); + qDebug() << "Add new remote IP " << datagram.senderAddress().toString(); } } } diff --git a/connections/gvretserial.cpp b/connections/gvretserial.cpp index 4275f0f..4da0f3a 100644 --- a/connections/gvretserial.cpp +++ b/connections/gvretserial.cpp @@ -819,11 +819,14 @@ void GVRetSerial::procRXChar(unsigned char c) qDebug() << "Baud 0 = " << can0Baud; qDebug() << "Baud 1 = " << can1Baud; mBusData[0].mBus.setSpeed(can0Baud); - mBusData[1].mBus.setSpeed(can1Baud); mBusData[0].mBus.setActive(can0Enabled); - mBusData[1].mBus.setActive(can1Enabled); mBusData[0].mConfigured = true; - mBusData[1].mConfigured = true; + if (mBusData.count() > 1) + { + mBusData[1].mBus.setSpeed(can1Baud); + mBusData[1].mBus.setActive(can1Enabled); + mBusData[1].mConfigured = true; + } can0Baud |= 0x80000000; if (can0Enabled) can0Baud |= 0x40000000; From 13e1763c099b3b3d3eb250c0bb7d6d736ec080fb Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 15 Jul 2020 18:26:23 -0400 Subject: [PATCH 37/58] Changed qt install script to be executable --- install-qt.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 install-qt.sh diff --git a/install-qt.sh b/install-qt.sh old mode 100644 new mode 100755 From 7a2a976fd9bc1a8af298fdb22c41cec646b54425 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 15 Jul 2020 18:40:25 -0400 Subject: [PATCH 38/58] Another attempt to maybe fix windows build. --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1bd9e3e..0b4bc97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -78,12 +78,10 @@ matrix: - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" - ./install-qt.sh -d ${QT_INSTALL_DIR} --version ${QT_VERSION} --toolchain win64_msvc2017_64 qtbase qttools qtconnectivity qtimageformats qtserialport qtserialbus script: - - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" - - ${QMAKE_PATH} CONFIG+=RELEASE SavvyCAN.pro - - cmd.exe //C nmake + - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" && cd "c:\Users\travis\build\collin80\savvycan" && cd && "c:/Users/travis/Qt/5.14.2/msvc2017/bin/qmake" CONFIG+=RELEASE SavvyCAN.pro && nmake - ls - cd release - - ${QDEPLOY_PATH} ./SavvyCAN.exe + - cmd.exe //C "C:\Users\travis\Qt\5.14.2\msvc2017\bin\windeployqt.exe" ./SavvyCAN.exe - cd release - ls - rm *.cpp From 59fd74f245d9e1eaad491d65f150f7dd0f0d65cf Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 15 Jul 2020 19:34:16 -0400 Subject: [PATCH 39/58] Another try at fixing windows --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0b4bc97..1419c2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -78,10 +78,10 @@ matrix: - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" - ./install-qt.sh -d ${QT_INSTALL_DIR} --version ${QT_VERSION} --toolchain win64_msvc2017_64 qtbase qttools qtconnectivity qtimageformats qtserialport qtserialbus script: - - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" && cd "c:\Users\travis\build\collin80\savvycan" && cd && "c:/Users/travis/Qt/5.14.2/msvc2017/bin/qmake" CONFIG+=RELEASE SavvyCAN.pro && nmake + - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" '&&' cd "c:/Users/travis/build/collin80/savvycan" '&&' cd '&&' "c:/Users/travis/Qt/5.14.2/msvc2017/bin/qmake" CONFIG+=RELEASE SavvyCAN.pro '&&' nmake - ls - cd release - - cmd.exe //C "C:\Users\travis\Qt\5.14.2\msvc2017\bin\windeployqt.exe" ./SavvyCAN.exe + - cmd.exe //C 'C:/Users/travis/Qt/5.14.2/msvc2017/bin/windeployqt.exe' './SavvyCAN.exe' - cd release - ls - rm *.cpp From ccbdacde56ee18a5ebbd2f6708685bcb8298f952 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 15 Jul 2020 20:18:29 -0400 Subject: [PATCH 40/58] Changed another graph on the Frame Info screen to logarithmic mode. This makes it far easier to see bits that were set only limited times. Now they show up plainly even if the other bits are seen tens of thousands of times --- re/frameinfowindow.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/re/frameinfowindow.cpp b/re/frameinfowindow.cpp index 656549a..03ab8e6 100644 --- a/re/frameinfowindow.cpp +++ b/re/frameinfowindow.cpp @@ -39,6 +39,12 @@ FrameInfoWindow::FrameInfoWindow(const QVector *frames, QWidget *paren ui->graphHistogram->xAxis->setRange(0, 63); ui->graphHistogram->yAxis->setRange(0, 100); + QSharedPointer graphHistoLogTicker(new QCPAxisTickerLog); + ui->graphHistogram->yAxis->setTicker(graphHistoLogTicker); + ui->graphHistogram->yAxis2->setTicker(graphHistoLogTicker); + ui->graphHistogram->yAxis->setNumberFormat("eb"); // e = exponential, b = beautiful decimal powers + ui->graphHistogram->yAxis->setNumberPrecision(0); //log ticker always picks powers of 10 so no need or use for precision + ui->graphHistogram->axisRect()->setupFullAxesBox(); ui->graphHistogram->xAxis->setLabel("Bits"); @@ -63,6 +69,12 @@ FrameInfoWindow::FrameInfoWindow(const QVector *frames, QWidget *paren ui->timeHistogram->xAxis->setRange(0, numIntervalHistBars); ui->timeHistogram->yAxis->setRange(0, 100); + QSharedPointer logTicker(new QCPAxisTickerLog); + ui->timeHistogram->yAxis->setTicker(logTicker); + ui->timeHistogram->yAxis2->setTicker(logTicker); + ui->timeHistogram->yAxis->setScaleType(QCPAxis::stLogarithmic); + ui->timeHistogram->yAxis->setNumberFormat("eb"); // e = exponential, b = beautiful decimal powers + ui->timeHistogram->yAxis->setNumberPrecision(0); //log ticker always picks powers of 10 so no need or use for precision ui->timeHistogram->axisRect()->setupFullAxesBox(); ui->timeHistogram->xAxis->setLabel("Interval (ms)"); @@ -557,7 +569,8 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) graphBrush.setStyle(Qt::SolidPattern); ui->graphHistogram->graph()->setPen(Qt::NoPen); ui->graphHistogram->graph()->setBrush(graphBrush); - ui->graphHistogram->yAxis->setRange(0, maxY * 1.02); + ui->graphHistogram->yAxis->setRange(0.8, maxY * 1.2); + ui->graphHistogram->yAxis->setScaleType(QCPAxis::stLogarithmic); ui->graphHistogram->axisRect()->setupFullAxesBox(); ui->graphHistogram->replot(); @@ -575,12 +588,6 @@ void FrameInfoWindow::updateDetailsWindow(QString newID) ui->timeHistogram->addGraph(); ui->timeHistogram->graph()->setData(timeGraphX, timeGraphY); ui->timeHistogram->graph()->setLineStyle(QCPGraph::lsStepLeft); //connect points with lines - ui->timeHistogram->yAxis->setScaleType(QCPAxis::stLogarithmic); - QSharedPointer logTicker(new QCPAxisTickerLog); - ui->timeHistogram->yAxis->setTicker(logTicker); - ui->timeHistogram->yAxis2->setTicker(logTicker); - ui->timeHistogram->yAxis->setNumberFormat("eb"); // e = exponential, b = beautiful decimal powers - ui->timeHistogram->yAxis->setNumberPrecision(0); //log ticker always picks powers of 10 so no need or use for precision //QBrush graphBrush; graphBrush.setColor(Qt::red); graphBrush.setStyle(Qt::SolidPattern); From 067292108de05faca2c689c5ebcef994a7dbda07 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 15 Jul 2020 21:25:28 -0400 Subject: [PATCH 41/58] Fixed several problems with Sniffer window. Incremented version number --- config.h | 2 +- re/sniffer/SnifferDelegate.cpp | 2 +- re/sniffer/snifferitem.cpp | 2 +- re/sniffer/sniffermodel.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config.h b/config.h index 163928a..3bbbd8a 100644 --- a/config.h +++ b/config.h @@ -1,7 +1,7 @@ #ifndef CONFIG #define CONFIG -#define VERSION 199 +#define VERSION 200 //try to keep this in sync. //SavvyCAN will complain if you connect a GVRET board with a revision diff --git a/re/sniffer/SnifferDelegate.cpp b/re/sniffer/SnifferDelegate.cpp index f9358d6..34cdd5e 100644 --- a/re/sniffer/SnifferDelegate.cpp +++ b/re/sniffer/SnifferDelegate.cpp @@ -116,7 +116,7 @@ void SnifferDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio painter->setOpacity(1.0); painter->setPen(QApplication::palette().color(QPalette::Text)); painter->setFont(mainFont); - painter->drawText(QRect(viewport.left(), viewport.top() + xSector + yOffset, xSpan, mainFontInfo->pixelSize()), Qt::AlignCenter, Utility::formatNumber((char)val)); + painter->drawText(QRect(viewport.left(), viewport.top() + xSector + yOffset, xSpan, mainFontInfo->pixelSize()), Qt::AlignCenter, Utility::formatNumber((unsigned char)val)); } QSize SnifferDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const diff --git a/re/sniffer/snifferitem.cpp b/re/sniffer/snifferitem.cpp index 1194ee9..5ab1a50 100644 --- a/re/sniffer/snifferitem.cpp +++ b/re/sniffer/snifferitem.cpp @@ -9,7 +9,7 @@ SnifferItem::SnifferItem(const CANFrame& pFrame, quint32 seq): const unsigned char *data = reinterpret_cast(pFrame.payload().constData()); int dataLen = pFrame.payload().length(); - for (int i = 0; i < 0; i++) { + for (int i = 0; i < dataLen; i++) { mNotch[i] = 0; mMarker.data[i] = 0; mMarker.dataTimestamp[i] = 0; diff --git a/re/sniffer/sniffermodel.cpp b/re/sniffer/sniffermodel.cpp index 667c791..d700d9c 100644 --- a/re/sniffer/sniffermodel.cpp +++ b/re/sniffer/sniffermodel.cpp @@ -70,7 +70,7 @@ QVariant SnifferModel::data(const QModelIndex &index, int role) const int data = item->getData(col-tc::DATA_0); if(data >= 0) { - return QString("%1").arg(data, 2, 16, QLatin1Char('0')).toUpper(); + return QString("%1").arg((uint8_t)data, 2, 16, QLatin1Char('0')).toUpper(); } } break; From fc6dc4ea55644b2c5e42c85e985ab9b96e6eab56 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 19 Jul 2020 20:29:14 -0400 Subject: [PATCH 42/58] Vast reworking of DBC editing system. Currently not functional but the overall graphical layout is more or less complete. --- README.md | 15 + SavvyCAN.pro | 7 +- dbc/dbcmaineditor.cpp | 450 +++++-------------- dbc/dbcmaineditor.h | 25 +- dbc/dbcmessageeditor.cpp | 185 ++++++++ dbc/dbcmessageeditor.h | 38 ++ dbc/dbcsignaleditor.cpp | 142 +----- dbc/dbcsignaleditor.h | 9 +- icons.qrc | 9 + images/message.png | Bin 0 -> 7543 bytes images/message_new.png | Bin 0 -> 8088 bytes images/multiplexed-signal.png | Bin 0 -> 8626 bytes images/multiplexor-signal.png | Bin 0 -> 9142 bytes images/node.png | Bin 0 -> 8145 bytes images/node_new.png | Bin 0 -> 9203 bytes images/signal.png | Bin 0 -> 9451 bytes images/signal_new.png | Bin 0 -> 10643 bytes images/skull.png | Bin 0 -> 10140 bytes images/sourceImages/noun_Death_3194193.png | Bin 0 -> 22807 bytes images/sourceImages/noun_Death_3194193.xcf | Bin 0 -> 4578 bytes images/sourceImages/noun_message_1977390.png | Bin 0 -> 17503 bytes images/sourceImages/noun_message_1977390.xcf | Bin 0 -> 3554 bytes images/sourceImages/noun_nodes_2397529.png | Bin 0 -> 19083 bytes images/sourceImages/noun_nodes_2397529.xcf | Bin 0 -> 4359 bytes images/sourceImages/noun_signal_1402315.png | Bin 0 -> 20260 bytes images/sourceImages/noun_signal_1402315.xcf | Bin 0 -> 4610 bytes images/sourceImages/noun_signal_1716563.png | Bin 0 -> 23003 bytes images/sourceImages/noun_signal_1716563.xcf | Bin 0 -> 4675 bytes images/sourceImages/noun_signal_1771815.png | Bin 0 -> 16934 bytes images/sourceImages/noun_signal_1771815.xcf | Bin 0 -> 4248 bytes ui/dbcmaineditor.ui | 96 +++- ui/dbcsignaleditor.ui | 10 +- 32 files changed, 462 insertions(+), 524 deletions(-) create mode 100644 dbc/dbcmessageeditor.cpp create mode 100644 dbc/dbcmessageeditor.h create mode 100644 images/message.png create mode 100644 images/message_new.png create mode 100644 images/multiplexed-signal.png create mode 100644 images/multiplexor-signal.png create mode 100644 images/node.png create mode 100644 images/node_new.png create mode 100644 images/signal.png create mode 100644 images/signal_new.png create mode 100644 images/skull.png create mode 100644 images/sourceImages/noun_Death_3194193.png create mode 100644 images/sourceImages/noun_Death_3194193.xcf create mode 100644 images/sourceImages/noun_message_1977390.png create mode 100644 images/sourceImages/noun_message_1977390.xcf create mode 100644 images/sourceImages/noun_nodes_2397529.png create mode 100644 images/sourceImages/noun_nodes_2397529.xcf create mode 100644 images/sourceImages/noun_signal_1402315.png create mode 100644 images/sourceImages/noun_signal_1402315.xcf create mode 100644 images/sourceImages/noun_signal_1716563.png create mode 100644 images/sourceImages/noun_signal_1716563.xcf create mode 100644 images/sourceImages/noun_signal_1771815.png create mode 100644 images/sourceImages/noun_signal_1771815.xcf diff --git a/README.md b/README.md index 9040884..4566633 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,18 @@ when you installed Qt. ### What to do if `qmake` fails with error `Project ERROR: Unknown module(s) in QT: qml serialbus help` on Ubuntu? : `sudo apt install libqt5serialbus5-dev qtdeclarative5-dev qttools5-dev` + +### Used Items Requiring Attribution + +nodes by Adrien Coquet from the Noun Project + +message by Vectorstall from the Noun Project + +signal by shashank singh from the Noun Project + +signal by juli from the Noun Project + +signal by yudi from the Noun Project + +Death by Adrien Coquet from the Noun Project + diff --git a/SavvyCAN.pro b/SavvyCAN.pro index 605b90b..33c8cb6 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -15,7 +15,7 @@ DEFINES += QCUSTOMPLOT_USE_OPENGL TARGET = SavvyCAN TEMPLATE = app -SOURCES += main.cpp\ +SOURCES += main.cpp\ mainwindow.cpp \ canframemodel.cpp \ utility.cpp \ @@ -39,6 +39,7 @@ SOURCES += main.cpp\ re/sniffer/snifferitem.cpp \ re/sniffer/sniffermodel.cpp \ re/sniffer/snifferwindow.cpp \ + dbc/dbcmessageeditor.cpp \ dbc/dbc_classes.cpp \ dbc/dbchandler.cpp \ dbc/dbcloadsavewindow.cpp \ @@ -73,7 +74,7 @@ SOURCES += main.cpp\ HEADERS += mainwindow.h \ can_structs.h \ - canframemodel.h \ + canframemodel.h \ utility.h \ qcustomplot.h \ frameplaybackwindow.h \ @@ -103,6 +104,7 @@ HEADERS += mainwindow.h \ dbc/dbcloadsavewindow.h \ dbc/dbcmaineditor.h \ dbc/dbcsignaleditor.h \ + dbc/dbcmessageeditor.h \ re/discretestatewindow.h \ re/filecomparatorwindow.h \ re/flowviewwindow.h \ @@ -132,6 +134,7 @@ HEADERS += mainwindow.h \ filterutility.h FORMS += ui/candatagrid.ui \ + ui/dbcmessageeditor.ui \ ui/connectionwindow.ui \ ui/dbcloadsavewindow.ui \ ui/dbcmaineditor.ui \ diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index f7e3386..f4a851d 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -20,41 +20,30 @@ DBCMainEditor::DBCMainEditor( const QVector *frames, QWidget *parent) dbcHandler = DBCHandler::getReference(); referenceFrames = frames; - QStringList headers; - headers << "Node Name" << "Comment"; - ui->NodesTable->setColumnCount(2); - ui->NodesTable->setColumnWidth(0, 240); - ui->NodesTable->setColumnWidth(1, 700); - ui->NodesTable->setHorizontalHeaderLabels(headers); - ui->NodesTable->horizontalHeader()->setStretchLastSection(true); - - QStringList headers2; - headers2 << "Msg ID" << "Msg Name" << "Data Len" << "Signals" << "Fg" << "Bg" << "Comment"; - ui->MessagesTable->setColumnCount(headers2.count()); - ui->MessagesTable->setColumnWidth(0, 80); - ui->MessagesTable->setColumnWidth(1, 200); - ui->MessagesTable->setColumnWidth(2, 80); - ui->MessagesTable->setColumnWidth(3, 80); - ui->MessagesTable->setColumnWidth(4, 40); - ui->MessagesTable->setColumnWidth(5, 40); - ui->MessagesTable->setColumnWidth(6, 300); - ui->MessagesTable->setHorizontalHeaderLabels(headers2); - ui->MessagesTable->horizontalHeader()->setStretchLastSection(true); - - connect(ui->NodesTable, SIGNAL(cellChanged(int,int)), this, SLOT(onCellChangedNode(int,int))); - connect(ui->NodesTable, SIGNAL(cellClicked(int,int)), this, SLOT(onCellClickedNode(int,int))); - connect(ui->NodesTable, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomMenuNode(QPoint))); - ui->NodesTable->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(ui->MessagesTable, SIGNAL(cellChanged(int,int)), this, SLOT(onCellChangedMessage(int,int))); - connect(ui->MessagesTable, SIGNAL(cellClicked(int,int)), this, SLOT(onCellClickedMessage(int,int))); - connect(ui->MessagesTable, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomMenuMessage(QPoint))); - ui->MessagesTable->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->btnSearch, &QAbstractButton::clicked, this, &DBCMainEditor::handleSearch); connect(ui->lineSearch, &QLineEdit::returnPressed, this, &DBCMainEditor::handleSearch); + connect(ui->treeDBC, &QTreeWidget::doubleClicked, this, &DBCMainEditor::onTreeDoubleClicked); sigEditor = new DBCSignalEditor(this); + msgEditor = new DBCMessageEditor(this); + + nodeIcon = QIcon(":/icons/images/node.png"); + messageIcon = QIcon(":/icons/images/message.png"); + signalIcon = QIcon(":/icons/images/signal.png"); + multiplexedSignalIcon = QIcon(":/icons/images/multiplexed-signal.png"); + multiplexorSignalIcon = QIcon(":/icons/images/multiplexor-signal.png"); + + //ui->btnDelete->setFixedSize(32,32); + ui->btnDelete->setIconSize(QSize(32, 32)); + + //ui->btnNewNode->setFixedSize(32,32); + ui->btnNewNode->setIconSize(QSize(32, 32)); + + //ui->btnNewMessage->setFixedSize(32,32); + ui->btnNewMessage->setIconSize(QSize(32, 32)); + + //ui->btnNewSignal->setFixedSize(32,32); + ui->btnNewSignal->setIconSize(QSize(32, 32)); installEventFilter(this); } @@ -63,13 +52,7 @@ void DBCMainEditor::showEvent(QShowEvent* event) { QDialog::showEvent(event); - inhibitCellChanged = true; - refreshNodesTable(); - if (dbcFile->dbc_nodes.count() > 0) - refreshMessagesTable(&dbcFile->dbc_nodes.at(0)); - - currRow = 0; - inhibitCellChanged = false; + refreshTree(); } DBCMainEditor::~DBCMainEditor() @@ -132,30 +115,20 @@ void DBCMainEditor::writeSettings() } } -void DBCMainEditor::onCustomMenuNode(QPoint point) -{ - QMenu *menu = new QMenu(this); - menu->setAttribute(Qt::WA_DeleteOnClose); - - menu->addAction(tr("Delete currently selected node"), this, SLOT(deleteCurrentNode())); - - menu->popup(ui->NodesTable->mapToGlobal(point)); - -} - -void DBCMainEditor::onCustomMenuMessage(QPoint point) +void DBCMainEditor::onCustomMenuTree(QPoint point) { QMenu *menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); menu->addAction(tr("Delete currently selected message"), this, SLOT(deleteCurrentMessage())); - menu->popup(ui->MessagesTable->mapToGlobal(point)); + //menu->popup(ui->MessagesTable->mapToGlobal(point)); } -void DBCMainEditor::deleteCurrentNode() +void DBCMainEditor::deleteCurrentTreeItem() { + /* int thisRow = ui->NodesTable->currentRow(); QTableWidgetItem* thisItem = ui->NodesTable->item(thisRow, 0); if (!thisItem) return; @@ -168,11 +141,12 @@ void DBCMainEditor::deleteCurrentNode() refreshMessagesTable(&dbcFile->dbc_nodes[0]); ui->NodesTable->selectRow(0); inhibitCellChanged = false; - } + } */ } -void DBCMainEditor::deleteCurrentMessage() -{ +//void DBCMainEditor::deleteCurrentMessage() +//{ + /* int thisRow = ui->MessagesTable->currentRow(); QTableWidgetItem* thisItem = ui->MessagesTable->item(thisRow, 0); if (!thisItem) return; @@ -181,10 +155,12 @@ void DBCMainEditor::deleteCurrentMessage() ui->MessagesTable->removeRow(thisRow); dbcFile->messageHandler->removeMessageByIndex(thisRow); } -} + */ +//} void DBCMainEditor::handleSearch() { + /* uint32_t msgId = Utility::ParseStringToNum(ui->lineSearch->text()); if (ui->radioID->isChecked()) { @@ -255,228 +231,50 @@ void DBCMainEditor::handleSearch() } } } - } + } */ } -void DBCMainEditor::onCellChangedNode(int row,int col) +//Double clicking is interpreted as a desire to edit the given item. +void DBCMainEditor::onTreeDoubleClicked(const QModelIndex &index) { - if (inhibitCellChanged) return; - if (row == ui->NodesTable->rowCount() - 1) - { - //if this was the last row then it's new and we need to both - //create another potentially new record underneath and also - //add a node entry in the nodes list and store the record - //That is, so long as the node name was filled out - if (col == 0) - { - DBC_NODE newNode; - QString newName = ui->NodesTable->item(row, col)->text().simplified().replace(' ', '_'); - qDebug() << "new name: " << newName; - if (newName.length() == 0) return; - if (dbcFile->findNodeByName(newName) != nullptr) //duplicates an existing node! - { - QMessageBox msg; - msg.setParent(nullptr); - msg.setText("An existing node with that name already exists! Aborting!"); - msg.exec(); - return; - } - newNode.name = newName; - dbcFile->dbc_nodes.append(newNode); - qDebug() << "# of nodes now " << dbcFile->dbc_nodes.count(); - QTableWidgetItem *widgetName = new QTableWidgetItem(newName); - inhibitCellChanged = true; - ui->NodesTable->setItem(row, col, widgetName); - ui->NodesTable->insertRow(ui->NodesTable->rowCount()); - inhibitCellChanged = false; - } - } - else - { - if (col == 0) - { - DBC_NODE *oldNode = dbcFile->findNodeByIdx(row); - QString nodeName = ui->NodesTable->item(row, col)->text().simplified().replace(' ', '_'); - if (oldNode == nullptr) return; - if (row != 0) oldNode->name = nodeName; - else nodeName = oldNode->name; - inhibitCellChanged = true; - QTableWidgetItem *widgetName = new QTableWidgetItem(nodeName); - ui->NodesTable->setItem(row, col, widgetName); - inhibitCellChanged = false; - } - if (col == 1) //must have been col 1 then - { - QString nodeName = ui->NodesTable->item(row, 0)->text().simplified().replace(' ', '_'); - qDebug() << "searching for node " << nodeName; - DBC_NODE *thisNode = dbcFile->findNodeByName(nodeName); - if (thisNode == nullptr) return; - thisNode->comment = ui->NodesTable->item(row, col)->text().simplified(); - qDebug() << "New comment: " << thisNode->comment; - } - } - ui->NodesTable->setCurrentCell(row, col); -} + Q_UNUSED(index) -void DBCMainEditor::onCellChangedMessage(int row,int col) -{ - QTableWidgetItem* item = nullptr; - bool ret = false; - DBC_MESSAGE *msg = nullptr; - uint msgID; - - if (inhibitCellChanged) return; - - DBC_NODE *node = dbcFile->findNodeByIdx(ui->NodesTable->currentRow()); - if (node == nullptr) - { - qDebug() << "No node set?!? This is bad!"; - return; - } - - item = ui->MessagesTable->item(row, 0); - if(!item) return; - - msgID = Utility::ParseStringToNum2(item->text(), &ret); - msg = dbcFile->messageHandler->findMsgByID(msgID); - - switch(col) - { - case 0: //msg id - { - /* sanity checks */ - if(!ret) - { - /* bad message id */ - ui->MessagesTable->item(row, 0)->setText(""); - return; - } - if (msg != nullptr) - { - QMessageBox msg; - msg.setParent(nullptr); - msg.setText("An existing msg with that ID already exists! Aborting!"); - msg.exec(); - - ui->MessagesTable->item(row, 0)->setText(""); - return; - } - - /* insert row */ - DBC_MESSAGE newMsg; - newMsg.ID = msgID; - newMsg.name = ""; - newMsg.sender = node; - newMsg.len = 0; - newMsg.fgColor = QColor(dbcFile->findAttributeByName("GenMsgForegroundColor")->defaultValue.toString()); - newMsg.bgColor = QColor(dbcFile->findAttributeByName("GenMsgBackgroundColor")->defaultValue.toString()); - - for (int i = 0; i < referenceFrames->length(); i++) - { - if ((uint) referenceFrames->at(i).frameId() == msgID) - { - newMsg.len = static_cast(referenceFrames->at(i).payload().length()); - break; - } - } - dbcFile->messageHandler->addMessage(newMsg); - - /* insert message in table */ - inhibitCellChanged = true; - - item = ui->MessagesTable->item(row, 0); - item->setFlags(item->flags() & ~Qt::ItemIsEditable); - item->setText(Utility::formatCANID(msgID)); - - for(int i=1 ; i < ui->MessagesTable->columnCount(); i++) - { - item = ui->MessagesTable->item(row, i); - item->setFlags(item->flags() | Qt::ItemIsEditable); - } - - /* set length */ - item = ui->MessagesTable->item(row, 2); - item->setText(QString::number(newMsg.len)); - - inhibitCellChanged = false; - - /* insert a new row */ - insertBlankRow(); - break; - } - case 1: //msg name - { - QString msgName = ui->MessagesTable->item(row, 1)->text().simplified().replace(' ', '_'); - if (msgName.length() == 0) return; - if( ret && (msg!=nullptr) ) - msg->name = msgName; - break; - } - case 2: //data length - { - bool parseOk = false; - uint msgLen = ui->MessagesTable->item(row, col)->text().toUInt(&parseOk); - - /* sanity checks */ - if(!parseOk) - { - ui->MessagesTable->item(row, col)->setText(""); - return; - } - if (msgLen > 8) - { - msgLen = 8; - ui->MessagesTable->item(row, col)->setText(QString::number(msgLen)); - } - - if( ret && (msg!=nullptr) ) - msg->len = msgLen; - break; - } - case 3: //signals (number) - we don't handle anything here. User cannot directly change this value - break; - case 6: //comment - { - QString msgComment = ui->MessagesTable->item(row, col)->text().simplified(); - if( ret && (msgComment!=nullptr) ) - msg->comment = msgComment; - break; - } - } - - ui->MessagesTable->setCurrentCell(row, col); -} - -void DBCMainEditor::onCellClickedNode(int row, int col) -{ - //reload messages list if the currently selected row has changed. - qDebug() << "Clicked at row " << row << " col " << col; - if (row != currRow) - { - currRow = row; - QTableWidgetItem *item = ui->NodesTable->item(currRow, 0); - QString nodeName; - if (item == nullptr) return; - - nodeName = item->text(); - qDebug() << "Trying to find node with name " << nodeName; - DBC_NODE *node = dbcFile->findNodeByName(nodeName); - //qDebug() << "Address of node: " << (int)node; - inhibitCellChanged = true; - refreshMessagesTable(node); - inhibitCellChanged = false; - } -} - -void DBCMainEditor::onCellClickedMessage(int row, int col) -{ - QTableWidgetItem* thisItem = ui->MessagesTable->item(row, col); - QTableWidgetItem* firstCol = ui->MessagesTable->item(row, 0); + QTreeWidgetItem* firstCol = ui->treeDBC->currentItem(); bool ret = false; DBC_MESSAGE *msg; + DBC_SIGNAL *sig; uint32_t msgID; + QString idString; - if (col == 3) //3 is the signals field. If clicked we go to the signals dialog + qDebug() << firstCol->data(0, Qt::UserRole) << " - " << firstCol->text(0); + + switch (firstCol->data(0, Qt::UserRole).toInt()) + { + case 1: //a node + break; + case 2: //a message + idString = firstCol->text(0).split(" ")[0]; + msgID = static_cast(Utility::ParseStringToNum(idString)); + msg = dbcFile->messageHandler->findMsgByID(msgID); + msgEditor->setMessageRef(msg); + msgEditor->setFileIdx(fileIdx); + msgEditor->setWindowModality(Qt::WindowModal); + msgEditor->exec(); //blocks this window from being active until we're done + break; + case 3: //a signal + idString = firstCol->parent()->text(0).split(" ")[0]; + msgID = static_cast(Utility::ParseStringToNum(idString)); + msg = dbcFile->messageHandler->findMsgByID(msgID); + sig = msg->sigHandler->findSignalByName(firstCol->text(0).split(" ")[0]); + sigEditor->setSignalRef(sig); + sigEditor->setMessageRef(msg); + sigEditor->setFileIdx(fileIdx); + sigEditor->setWindowModality(Qt::WindowModal); + sigEditor->exec(); //blocks this window from being active until we're done + break; + } + +/* if (col == 3) //3 is the signals field. If clicked we go to the signals dialog { QTableWidgetItem* msg = ui->MessagesTable->item(row, 0); if(msg) { @@ -546,15 +344,17 @@ void DBCMainEditor::onCellClickedMessage(int row, int col) msg->attributes.append(newVal); } } - } + } */ } -void DBCMainEditor::refreshNodesTable() -{ - ui->NodesTable->clearContents(); //first step, clear out any previous crap - ui->NodesTable->setRowCount(0); - int rowIdx; +/* + * Recreate the whole tree with pretty icons and custom user roles that give the rest of code an easy way to figure out whether a given tree node + * is a node, message, or signal. +*/ +void DBCMainEditor::refreshTree() +{ + ui->treeDBC->clear(); if (dbcFile->findNodeByName("Vector__XXX") == nullptr) { @@ -564,82 +364,40 @@ void DBCMainEditor::refreshNodesTable() dbcFile->dbc_nodes.append(newNode); } - for (int x = 0; x < dbcFile->dbc_nodes.count(); x++) - { - DBC_NODE node = dbcFile->dbc_nodes.at(x); - QTableWidgetItem *nodeName = new QTableWidgetItem(node.name); - QTableWidgetItem *nodeComment = new QTableWidgetItem(node.comment); - rowIdx = ui->NodesTable->rowCount(); - ui->NodesTable->insertRow(rowIdx); - ui->NodesTable->setItem(rowIdx, 0, nodeName); - ui->NodesTable->setItem(rowIdx, 1, nodeComment); - } - - //insert a fresh entry at the bottom that contains nothing - ui->NodesTable->insertRow(ui->NodesTable->rowCount()); - ui->NodesTable->selectRow(0); -} - -void DBCMainEditor::refreshMessagesTable(const DBC_NODE *node) -{ - ui->MessagesTable->clearContents(); - ui->MessagesTable->setRowCount(0); - - qDebug() << "In refreshMessagesTable"; - - int rowIdx; - - if (node != nullptr) + foreach (DBC_NODE node, dbcFile->dbc_nodes) { + QTreeWidgetItem *nodeItem = new QTreeWidgetItem(); + QString nodeInfo = node.name; + if (node.comment.count() > 0) nodeInfo.append(" - ").append(node.comment); + nodeItem->setText(0, nodeInfo); + nodeItem->setIcon(0, nodeIcon); + nodeItem->setData(0, Qt::UserRole, 1); for (int x = 0; x < dbcFile->messageHandler->getCount(); x++) { DBC_MESSAGE *msg = dbcFile->messageHandler->findMsgByIdx(x); - if (msg->sender == node) + if (msg->sender->name == node.name) { - //many of these are simplistic first versions just to test functionality. - QTableWidgetItem *msgID = new QTableWidgetItem(Utility::formatCANID(msg->ID)); - QTableWidgetItem *msgName = new QTableWidgetItem(msg->name); - QTableWidgetItem *msgLen = new QTableWidgetItem(QString::number(msg->len)); - QTableWidgetItem *msgSignals = new QTableWidgetItem(QString::number(msg->sigHandler->getCount())); - QTableWidgetItem *fgColor = new QTableWidgetItem(""); - if (msg->fgColor.isValid()) fgColor->setBackground(msg->fgColor); - else fgColor->setBackground(QColor(dbcFile->findAttributeByName("GenMsgForegroundColor")->defaultValue.toString())); - QTableWidgetItem *bgColor = new QTableWidgetItem(""); - if (msg->bgColor.isValid()) bgColor->setBackground(msg->bgColor); - else bgColor->setBackground(QColor(dbcFile->findAttributeByName("GenMsgBackgroundColor")->defaultValue.toString())); - - QTableWidgetItem *msgComment = new QTableWidgetItem(msg->comment); - - rowIdx = ui->MessagesTable->rowCount(); - ui->MessagesTable->insertRow(rowIdx); - ui->MessagesTable->setItem(rowIdx, 0, msgID); - ui->MessagesTable->setItem(rowIdx, 1, msgName); - ui->MessagesTable->setItem(rowIdx, 2, msgLen); - ui->MessagesTable->setItem(rowIdx, 3, msgSignals); - ui->MessagesTable->setItem(rowIdx, 4, fgColor); - ui->MessagesTable->setItem(rowIdx, 5, bgColor); - ui->MessagesTable->setItem(rowIdx, 6, msgComment); - //note that there is a sending node field in the structure but we're - //not displaying it. It has to be the node we selected from the node list - //so no need to show that here. + QTreeWidgetItem *msgItem = new QTreeWidgetItem(nodeItem); + QString msgInfo = Utility::formatCANID(msg->ID) + " " + msg->name; + if (msg->comment.count() > 0) msgInfo.append(" - ").append(msg->comment); + msgItem->setText(0, msgInfo); + msgItem->setIcon(0, messageIcon); + msgItem->setData(0, Qt::UserRole, 2); + for (int i = 0; i < msg->sigHandler->getCount(); i++) + { + DBC_SIGNAL *sig = msg->sigHandler->findSignalByIdx(i); + QTreeWidgetItem *sigItem = new QTreeWidgetItem(msgItem); + QString sigInfo = sig->name; + if (sig->comment.count() > 0) sigInfo.append(" - ").append(sig->comment); + sigItem->setText(0, sigInfo); + if (sig->isMultiplexed) sigItem->setIcon(0, multiplexedSignalIcon); + else if (sig->isMultiplexor) sigItem->setIcon(0, multiplexorSignalIcon); + else sigItem->setIcon(0, signalIcon); + sigItem->setData(0, Qt::UserRole, 3); + } } } + ui->treeDBC->addTopLevelItem(nodeItem); } - - //insert blank record that can be used to add new messages - insertBlankRow(); -} - -void DBCMainEditor::insertBlankRow() -{ - int rowIdx = ui->MessagesTable->rowCount(); - ui->MessagesTable->insertRow(rowIdx); - for(int i=1 ; i < ui->MessagesTable->columnCount(); i++) - { - QTableWidgetItem *item = new QTableWidgetItem(""); - if (i == 4) item->setBackground(QApplication::palette().color(QPalette::WindowText)); //foreground color - if (i == 5) item->setBackground(QApplication::palette().color(QPalette::Base)); - item->setFlags(item->flags() & ~Qt::ItemIsEditable); - ui->MessagesTable->setItem(rowIdx, i, item); - } + ui->treeDBC->sortItems(0, Qt::SortOrder::AscendingOrder); //sort the display list for ease in viewing by mere mortals, helps me a lot. } diff --git a/dbc/dbcmaineditor.h b/dbc/dbcmaineditor.h index c369c2c..3d2447e 100644 --- a/dbc/dbcmaineditor.h +++ b/dbc/dbcmaineditor.h @@ -3,8 +3,10 @@ #include #include +#include #include "dbchandler.h" #include "dbcsignaleditor.h" +#include "dbcmessageeditor.h" #include "utility.h" namespace Ui { @@ -21,14 +23,9 @@ public: void setFileIdx(int idx); private slots: - void onCellChangedNode(int,int); - void onCellClickedNode(int, int); - void onCellClickedMessage(int, int); - void onCellChangedMessage(int,int); - void onCustomMenuNode(QPoint); - void onCustomMenuMessage(QPoint); - void deleteCurrentNode(); - void deleteCurrentMessage(); + void onTreeDoubleClicked(const QModelIndex &index); + void onCustomMenuTree(QPoint); + void deleteCurrentTreeItem(); void handleSearch(); private: @@ -36,19 +33,21 @@ private: DBCHandler *dbcHandler; const QVector *referenceFrames; DBCSignalEditor *sigEditor; - int currRow; + DBCMessageEditor *msgEditor; DBCFile *dbcFile; int fileIdx; - bool inhibitCellChanged; + QIcon nodeIcon; + QIcon messageIcon; + QIcon signalIcon; + QIcon multiplexorSignalIcon; + QIcon multiplexedSignalIcon; - void refreshNodesTable(); - void refreshMessagesTable(const DBC_NODE *node); + void refreshTree(); void showEvent(QShowEvent* event); void closeEvent(QCloseEvent *event); bool eventFilter(QObject *obj, QEvent *event); void readSettings(); void writeSettings(); - void insertBlankRow(); }; diff --git a/dbc/dbcmessageeditor.cpp b/dbc/dbcmessageeditor.cpp new file mode 100644 index 0000000..1ccdcb5 --- /dev/null +++ b/dbc/dbcmessageeditor.cpp @@ -0,0 +1,185 @@ +#include "dbcmessageeditor.h" +#include "ui_dbcmessageeditor.h" + +#include +#include +#include +#include "helpwindow.h" +#include "utility.h" + +DBCMessageEditor::DBCMessageEditor(QWidget *parent) : + QDialog(parent), + ui(new Ui::DBCMessageEditor) +{ + ui->setupUi(this); + + readSettings(); + + dbcHandler = DBCHandler::getReference(); + dbcMessage = nullptr; + + connect(ui->btnTextColor, &QAbstractButton::clicked, + [=]() + { + QColor newColor = QColorDialog::getColor(dbcMessage->fgColor); + + dbcMessage->fgColor = newColor; + DBC_ATTRIBUTE_VALUE *val = dbcMessage->findAttrValByName("GenMsgForegroundColor"); + if (val) + { + val->value = newColor.name(); + } + else + { + DBC_ATTRIBUTE_VALUE newVal; + newVal.attrName = "GenMsgForegroundColor"; + newVal.value = newColor.name(); + dbcMessage->attributes.append(newVal); + } + generateSampleText(); + }); + + connect(ui->btnBackgroundColor, &QAbstractButton::clicked, + [=]() + { + QColor newColor = QColorDialog::getColor(dbcMessage->bgColor); + + dbcMessage->bgColor = newColor; + DBC_ATTRIBUTE_VALUE *val = dbcMessage->findAttrValByName("GenMsgBackgroundColor"); + if (val) + { + val->value = newColor.name(); + } + else + { + DBC_ATTRIBUTE_VALUE newVal; + newVal.attrName = "GenMsgBackgroundColor"; + newVal.value = newColor.name(); + dbcMessage->attributes.append(newVal); + } + generateSampleText(); + }); + + installEventFilter(this); +} + +DBCMessageEditor::~DBCMessageEditor() +{ + removeEventFilter(this); + delete ui; +} + +void DBCMessageEditor::closeEvent(QCloseEvent *event) +{ + Q_UNUSED(event); + writeSettings(); +} + +bool DBCMessageEditor::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::KeyRelease) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) + { + case Qt::Key_F1: + HelpWindow::getRef()->showHelp("messageeditor.html"); + break; + } + return true; + } else { + // standard event processing + return QObject::eventFilter(obj, event); + } + return false; +} + +void DBCMessageEditor::setFileIdx(int idx) +{ + if (idx < 0 || idx > dbcHandler->getFileCount() - 1) return; + dbcFile = dbcHandler->getFileByIdx(idx); + + for (int x = 0; x < dbcFile->dbc_nodes.count(); x++) + { + ui->comboSender->addItem(dbcFile->dbc_nodes[x].name); + } +} + +void DBCMessageEditor::readSettings() +{ + QSettings settings; + if (settings.value("Main/SaveRestorePositions", false).toBool()) + { + resize(settings.value("DBCMessageEditor/WindowSize", QSize(340, 400)).toSize()); + move(settings.value("DBCMessageEditor/WindowPos", QPoint(100, 100)).toPoint()); + } +} + +void DBCMessageEditor::writeSettings() +{ + QSettings settings; + + if (settings.value("Main/SaveRestorePositions", false).toBool()) + { + settings.setValue("DBCMessageEditor/WindowSize", size()); + settings.setValue("DBCMessageEditor/WindowPos", pos()); + } +} + + +void DBCMessageEditor::setMessageRef(DBC_MESSAGE *msg) +{ + dbcMessage = msg; +} + +void DBCMessageEditor::showEvent(QShowEvent* event) +{ + QDialog::showEvent(event); + + refreshView(); +} + +void DBCMessageEditor::refreshView() +{ + ui->lineComment->setText(dbcMessage->comment); + ui->lineFrameID->setText(Utility::formatCANID(dbcMessage->ID)); + ui->lineMsgName->setText(dbcMessage->name); + ui->lineFrameLen->setText(QString::number(dbcMessage->len)); + for (int i = 0; i < ui->comboSender->count(); i++) + { + if (ui->comboSender->itemText(i) == dbcMessage->sender->name) + { + ui->comboSender->setCurrentIndex(i); + break; + } + } + + generateSampleText(); +} + +void DBCMessageEditor::generateSampleText() +{ + QBrush fg, bg; + + if (dbcMessage->fgColor.isValid()) fg = QBrush(dbcMessage->fgColor); + else fg = QBrush(QColor(dbcFile->findAttributeByName("GenMsgForegroundColor")->defaultValue.toString())); + if (dbcMessage->bgColor.isValid()) bg = QBrush(dbcMessage->bgColor); + else bg = QBrush(QColor(dbcFile->findAttributeByName("GenMsgBackgroundColor")->defaultValue.toString())); + + ui->listSample->clear(); + QListWidgetItem *item = new QListWidgetItem("Test String"); + item->setForeground(fg); + item->setBackground(bg); + ui->listSample->addItem(item); + item = new QListWidgetItem("0x20F TestMsg"); + item->setForeground(fg); + item->setBackground(bg); + ui->listSample->addItem(item); + item = new QListWidgetItem("20 FF 10 A1 BB CC 4D"); + item->setForeground(fg); + item->setBackground(bg); + ui->listSample->addItem(item); + item = new QListWidgetItem("1024.3434"); + item->setForeground(fg); + item->setBackground(bg); + ui->listSample->addItem(item); +} diff --git a/dbc/dbcmessageeditor.h b/dbc/dbcmessageeditor.h new file mode 100644 index 0000000..2685fcc --- /dev/null +++ b/dbc/dbcmessageeditor.h @@ -0,0 +1,38 @@ +#ifndef DBCMESSAGEEDITOR_H +#define DBCMESSAGEEDITOR_H + +#include +#include "dbc_classes.h" +#include "dbchandler.h" + +namespace Ui { +class DBCMessageEditor; +} + +class DBCMessageEditor : public QDialog +{ + Q_OBJECT + +public: + explicit DBCMessageEditor(QWidget *parent = nullptr); + ~DBCMessageEditor(); + void showEvent(QShowEvent*); + void setMessageRef(DBC_MESSAGE *msg); + void setFileIdx(int idx); + +private: + Ui::DBCMessageEditor *ui; + + DBCHandler *dbcHandler; + DBC_MESSAGE *dbcMessage; + DBCFile *dbcFile; + + void closeEvent(QCloseEvent *event); + bool eventFilter(QObject *obj, QEvent *event); + void readSettings(); + void writeSettings(); + void refreshView(); + void generateSampleText(); +}; + +#endif // DBCMESSAGEEDITOR_H diff --git a/dbc/dbcsignaleditor.cpp b/dbc/dbcsignaleditor.cpp index 4cd27b6..f8cbe6d 100644 --- a/dbc/dbcsignaleditor.cpp +++ b/dbc/dbcsignaleditor.cpp @@ -34,10 +34,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : ui->comboType->addItem("DOUBLE PRECISION"); ui->comboType->addItem("STRING"); - connect(ui->signalsList, SIGNAL(currentRowChanged(int)), this, SLOT(clickSignalList(int))); connect(ui->bitfield, SIGNAL(gridClicked(int,int)), this, SLOT(bitfieldClicked(int,int))); - connect(ui->signalsList, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomMenuSignals(QPoint))); - ui->signalsList->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->valuesTable, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomMenuValues(QPoint))); ui->valuesTable->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->valuesTable, SIGNAL(cellChanged(int,int)), this, SLOT(onValuesCellChanged(int,int))); @@ -155,7 +152,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : QString tempNameStr = ui->txtName->text().simplified().replace(' ', '_'); if (tempNameStr.length() > 0) currentSignal->name = tempNameStr; //need to update the list too. - ui->signalsList->currentItem()->setText(currentSignal->name); + //ui->signalsList->currentItem()->setText(currentSignal->name); }); connect(ui->txtMultiplexValue, &QLineEdit::editingFinished, @@ -228,9 +225,6 @@ bool DBCSignalEditor::eventFilter(QObject *obj, QEvent *event) case Qt::Key_F1: HelpWindow::getRef()->showHelp("signaleditor.html"); break; - case Qt::Key_F2: - cloneSignal(); - break; } return true; } else { @@ -278,12 +272,18 @@ void DBCSignalEditor::setMessageRef(DBC_MESSAGE *msg) dbcMessage = msg; } +void DBCSignalEditor::setSignalRef(DBC_SIGNAL *sig) +{ + currentSignal = sig; +} + + void DBCSignalEditor::showEvent(QShowEvent* event) { QDialog::showEvent(event); - currentSignal = nullptr; - refreshSignalsList(); + fillSignalForm(currentSignal); + fillValueTable(currentSignal); } void DBCSignalEditor::onValuesCellChanged(int row,int col) @@ -317,21 +317,6 @@ void DBCSignalEditor::onValuesCellChanged(int row,int col) } } -void DBCSignalEditor::onCustomMenuSignals(QPoint point) -{ - QMenu *menu = new QMenu(this); - menu->setAttribute(Qt::WA_DeleteOnClose); - - menu->addAction(tr("Add a new signal"), this, SLOT(addNewSignal())); - if (ui->signalsList->currentRow() != -1) - { - menu->addAction(tr("Clone currently selected signal"), this, SLOT(cloneSignal())); - menu->addAction(tr("Delete currently selected signal"), this, SLOT(deleteCurrentSignal())); - } - - menu->popup(ui->signalsList->mapToGlobal(point)); -} - void DBCSignalEditor::onCustomMenuValues(QPoint point) { QMenu *menu = new QMenu(this); @@ -342,86 +327,6 @@ void DBCSignalEditor::onCustomMenuValues(QPoint point) menu->popup(ui->valuesTable->mapToGlobal(point)); } -void DBCSignalEditor::addNewSignal() -{ - int num = QRandomGenerator::global()->bounded(70000); - QString newName = "SIGNAL" + QString::number(num); - DBC_SIGNAL newSig; - newSig.name = newName; - newSig.bias = 0.0; - newSig.factor = 1.0; - newSig.intelByteOrder = true; - newSig.max = 0.0; - newSig.min = 0.0; - newSig.receiver = dbcFile->findNodeByIdx(0); - newSig.signalSize = 1; - newSig.startBit = 0; - newSig.valType = UNSIGNED_INT; - newSig.isMultiplexed = false; - newSig.isMultiplexor = false; - newSig.multiplexValue = 0; - newSig.parentMessage = dbcMessage; - - dbcMessage->sigHandler->addSignal(newSig); - - /* add item at the end of the list and select it */ - /* this will call clickSignalList */ - ui->signalsList->addItem(newName); - ui->signalsList->setCurrentRow(ui->signalsList->count()-1); -} - -void DBCSignalEditor::cloneSignal() -{ - int num = QRandomGenerator::global()->bounded(100); - - int idx = ui->signalsList->currentRow(); - if (idx < 0) return; - - DBC_SIGNAL *oldSig = dbcMessage->sigHandler->findSignalByIdx(idx); - if (!oldSig) return; - - QString newName = oldSig->name + QString::number(num); - DBC_SIGNAL newSig; - newSig.name = newName; - newSig.bias = oldSig->bias; - newSig.factor = oldSig->factor; - newSig.intelByteOrder = oldSig->intelByteOrder; - newSig.max = oldSig->max; - newSig.min = oldSig->min; - newSig.receiver = oldSig->receiver; - newSig.signalSize = oldSig->signalSize; - newSig.startBit = oldSig->startBit; - newSig.valType = oldSig->valType; - newSig.valList.append(oldSig->valList); - newSig.isMultiplexed = oldSig->isMultiplexed; - newSig.isMultiplexor = oldSig->isMultiplexor; //maybe should force this false since you can't have two or more! - newSig.multiplexValue = oldSig->multiplexValue; - newSig.parentMessage = dbcMessage; - - dbcMessage->sigHandler->addSignal(newSig); - - /* add item at the end of the list */ - ui->signalsList->addItem(newName); - - //unlike adding a signal we don't want to select the new signal here. - //ui->signalsList->setCurrentRow(ui->signalsList->count()-1); -} - -void DBCSignalEditor::deleteCurrentSignal() -{ - int currIdx = ui->signalsList->currentRow(); - - //if(currIdx==ui->signalsList->count()-1) - //return; - - if (currIdx > -1) - { - delete(ui->signalsList->item(currIdx)); - dbcMessage->sigHandler->removeSignal(currIdx); - currentSignal = nullptr; - } -} - void DBCSignalEditor::deleteCurrentValue() { int currIdx = ui->valuesTable->currentRow(); @@ -432,25 +337,6 @@ void DBCSignalEditor::deleteCurrentValue() } } -void DBCSignalEditor::refreshSignalsList() -{ - ui->signalsList->clear(); - - - for (int x = 0; x < dbcMessage->sigHandler->getCount(); x++) - { - DBC_SIGNAL *sig = dbcMessage->sigHandler->findSignalByIdx(x); - ui->signalsList->addItem(sig->name); - } - - if( ui->signalsList->count()>0 ) { - /* click first element */ - ui->signalsList->setCurrentRow(0); - } - else - clickSignalList(-1); -} - /* fillSignalForm also handles group "enabled" state */ /* WARNING: fillSignalForm can be called recursively since it is in the listener of cbIntelFormat */ void DBCSignalEditor::fillSignalForm(DBC_SIGNAL *sig) @@ -598,16 +484,6 @@ void DBCSignalEditor::fillValueTable(DBC_SIGNAL *sig) inhibitCellChanged = false; } -void DBCSignalEditor::clickSignalList(int row) -{ - //qDebug() << ui->signalsList->item(row)->text(); - - DBC_SIGNAL *thisSig = (row<0) ? nullptr : dbcMessage->sigHandler->findSignalByName(ui->signalsList->item(row)->text()); - currentSignal = thisSig; - fillSignalForm(thisSig); - fillValueTable(thisSig); -} - void DBCSignalEditor::bitfieldClicked(int x, int y) { int bit = (7 - x) + (y * 8); diff --git a/dbc/dbcsignaleditor.h b/dbc/dbcsignaleditor.h index e807f58..2b6e94f 100644 --- a/dbc/dbcsignaleditor.h +++ b/dbc/dbcsignaleditor.h @@ -16,19 +16,15 @@ class DBCSignalEditor : public QDialog public: explicit DBCSignalEditor(QWidget *parent = 0); void setMessageRef(DBC_MESSAGE *msg); - void showEvent(QShowEvent*); void setFileIdx(int idx); + void setSignalRef(DBC_SIGNAL *sig); + void showEvent(QShowEvent*); ~DBCSignalEditor(); private slots: - void clickSignalList(int); void bitfieldClicked(int x, int y); void onValuesCellChanged(int row,int col); - void onCustomMenuSignals(QPoint); void onCustomMenuValues(QPoint); - void addNewSignal(); - void cloneSignal(); - void deleteCurrentSignal(); void deleteCurrentValue(); private: @@ -39,7 +35,6 @@ private: DBCFile *dbcFile; bool inhibitCellChanged; - void refreshSignalsList(); void fillSignalForm(DBC_SIGNAL *sig); void fillValueTable(DBC_SIGNAL *sig); void generateUsedBits(); diff --git a/icons.qrc b/icons.qrc index cd68926..49ac17c 100644 --- a/icons.qrc +++ b/icons.qrc @@ -6,5 +6,14 @@ images/PlayHS.png images/ReverseHS.png images/StopHS.png + images/signal.png + images/node.png + images/message.png + images/multiplexed-signal.png + images/multiplexor-signal.png + images/skull.png + images/signal_new.png + images/node_new.png + images/message_new.png diff --git a/images/message.png b/images/message.png new file mode 100644 index 0000000000000000000000000000000000000000..985a134351f93b28d7820ed7ff3a739ccb121e24 GIT binary patch literal 7543 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 mF`%jArh$uz5F^G-&3^#-1Y{CnV^J;u0000 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 literal 0 HcmV?d00001 diff --git a/images/multiplexor-signal.png b/images/multiplexor-signal.png new file mode 100644 index 0000000000000000000000000000000000000000..afd11cdfeab4c3200d5c430b3dfecfc505170eed GIT binary patch literal 9142 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 AdjJ3c literal 0 HcmV?d00001 diff --git a/images/node.png b/images/node.png new file mode 100644 index 0000000000000000000000000000000000000000..f2a52f5f139c5409fadb66fb5a03eec1d4102c63 GIT binary patch literal 8145 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 z z|I%n*pij!qE&`aStE;n}7Tddb?=|1G((vDGLQ6!v-hWF;-Au+uw>5SjfFF@u;qiD@ zIC)tp6uN4ygTWvcEn0M4fI#vBfKve40W?Y>6+D_^fj+6M>JdqBmT&s9qQkvfJ$C@O zBs8AEOcgp5MVYlb9071jQIsz>ZrnI+_a#+TRo7H~PEAc+7sPMcv?-z}$^~KWIcUmi z(^MpcTk4H`pU>C7fB*g>J1?rPuD<5`(9n=85uvsPgFzfWe%#hGm6er2(fe*K_h%JH zNzWu%9tZ^fxN6m^s2x9GbaXWSRr>%F6BAssW({r3D>gDRQcv<1Nh?ToTjf7^rcI{eI;wvgDVx66x-6VIA{GrNyJ9UDEiEldLqh|+UN6?J zT^lPcEgdf{Ej^Q!mDLWQ6*Kk2hGq1MnG|SB=8DBVUWG5KMYCiVDdBkl2Y}E4IMhUX z(-P(N%aZ;ffHovJLy%@xxF8^eWZ6(;>ntpP(H2W<38D=koH_}1ilw`dq zFd(jOMSOB;H&7moh3lv4kB$=)#%GHwkSS+T-2#S7R!K9M>YdbXce}8PwRw2JS zu+A=oF>|c-U$B30Qs1ucgOghIaOvo*K4UwL#7Acbg!AXMggYhP2>>eRVhLjYtB0qf r(dd%2w6sOIjt@_}wod=A9-sdYEk8xoXL9|*00000NkvXXu0mjfZ_9q4 literal 0 HcmV?d00001 diff --git a/images/node_new.png b/images/node_new.png new file mode 100644 index 0000000000000000000000000000000000000000..75ae0aaebb02a064dbd5607cb7eced310b6b413d GIT binary patch literal 9203 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 JPDHLkV1jN2+1~&F literal 0 HcmV?d00001 diff --git a/images/signal.png b/images/signal.png new file mode 100644 index 0000000000000000000000000000000000000000..dbfc7141571076ef3e47d4318fec0d63f27547ff GIT binary patch literal 9451 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Š!hh^?U#YGV1fh0`8Pxm#vt4#yG5v8X>k@gd{lL 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 literal 0 HcmV?d00001 diff --git a/images/signal_new.png b/images/signal_new.png new file mode 100644 index 0000000000000000000000000000000000000000..3581a8520984992bf874399a53f763511644eadd GIT binary patch literal 10643 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 literal 0 HcmV?d00001 diff --git a/images/skull.png b/images/skull.png new file mode 100644 index 0000000000000000000000000000000000000000..01453fc776c2f662e1a78228f607aeec54ac1bcb GIT binary patch literal 10140 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 DTjwYh literal 0 HcmV?d00001 diff --git a/images/sourceImages/noun_Death_3194193.xcf b/images/sourceImages/noun_Death_3194193.xcf new file mode 100644 index 0000000000000000000000000000000000000000..18605baf3b70d9271aabccb78e24ddc8a31be2dc GIT binary patch literal 4578 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 literal 0 HcmV?d00001 diff --git a/images/sourceImages/noun_message_1977390.png b/images/sourceImages/noun_message_1977390.png new file mode 100644 index 0000000000000000000000000000000000000000..5fd6b42b1d8073b291fcdcfe5092a74d59f236f6 GIT binary patch literal 17503 zcmeIacTkgG@HZL~iu9(^L8MzKB1KwMKnPVp5JkFz^bVmW5fu<=0qI2{fPnPgqarpG z4ZTND=@6v%dm!KYzIW!Gx&Pgn`^V3bVIb!@XV31QJ)iyTK1AI#&|;+Hrh`Brj0kNF zV+aJ=a`bhA`sg=JuIk_$rO$Q5trOs1$cg)r;AdJ-?K?gY2tE7J7X>6elM@0t13_q9 zyM@eL7z;?fZ(3WiQZFVg&2WWY=#0@xjcZpmG$>jB1f61e`}ht6@dJ|aHp?GbyQ`WX zLSoRgF!2+);zkKCu?f=n_@|1}jqPy%Z&~Ttb4zo@)h>N%>j&A6a0bg)n@WEDR?UW4 z)y&#hVZ{IZ{~rzfziOZeqW+Wbrgb`YOpf6K^K8-&orvX8%A4I%2{Pyggg z5SGB}*BPh`cn07D^<; zmaeCDQWX2EoggWalZu-r7U#d9d$;vh_fI~f^-NNK&pcY{M!y*So7c;MqNoGF(u!g#X|;to6DA7l0FHsPMLy-4J?*+1ImEul@Q1qHK4 zfX3L$2@ll}N~D#qm@uL^Z59_E-X6CU*)+vE{;!m36#C}$7%Kwn{7|xjYxZF|-5*Vt zKgw)JJO5L4j?_P6@LHMw*ToTGqJxctCmhUBJ%Z84d;t4!EYJwJY8lA1uP|hJqJ?MK zSO_uK)q^pFn&c@H?%Uf45CecEsDCP8eyO?oVAL;lUPKfZroiG@tv}wa^|=4sg23r6 z6b9ie2|a0;ksuu7!AA0$obNaP)?9|5s5`5+J>mox$|6Z2jL(-)vz5 zne^)r3u|7aBYZx<3d@b~Hay!>>T?ga%>MRr^j`9tdxp=J@RSRXdvq%2n}o2qwn~m7 zd6g>-0_-bEGfdjye+N1a^Ob+rL6GXPF@lJ5Us!SJES4#v!{56qWrSmvoG01adp4+A zN^6A~b0zR8QRV7cbtuzzgmBCsCh%E?|LNJfPgvaB3VBbamG#05f5Sf(nuJt-QMHw3 zw*iWgTdlUjh)_pmBK73FLA{$*AI}ZKVDe2jwh8N%-P#d;NVfG#U4rm>90!z{&BVUA z%uV;)xIDJgX#Hh{oTvWNO}D~}&~eknzhDez6B->=ba5cw@mml=HAx6FKpB?tt98U} zg4(Rul^%i3)q@?RGz58zR@}&wGC<}(`lCbqjCYi6K{YQ*;i+G^3L=aj$DpGlON7KK z_H?cu4>2GPKv!k=vi*lk@g~h9Dp(vunW8$fh;J29>mn@HV*EBJ0pCrz^o7(s?Bs(9 zSDpYv_?1s0lTy=Vcm_k0!mj*!LDyeG8Ky$oWN0tXs-xUGA>I4)G*)>19ScduZmvVR z=*BnRJk-&0>z6RMslu?o0_sM>2s7crMeSXXFoS!cij1jh1_D_m9jbXhC^N-Jq-r~ zSfQ`!wPxR%)LRgyPpm~(Us@|4+t62nck5y)d(g`)3bBW=&gI=R13?l+jQg4RM2yt^ z8T;nN5T$19Z7lT|HJ+*?W+JC>=M-|M(MpF~ifA=O$U~mUA1mItO)%Qm}W$B`^xGvQIyz{fF6z?`s0u z{Se%j)$v{(&%58_|KO)&Kbjl2CSAVnM5_YZ3Va$?KVm>t;239&{Ze_3WHQL`5F`gI>Z*V#P7b^8lVA5NsEQNqSuZjtPI{5vZa35A7l8(m&syb;tj;!1? zBRXxX-@7)uZp{7APn*byi5C6wv3P_Qk6SR(F>d*clXK7O9HG~C*}L&ty)}v2%#sbs zO=`DaJ0GA7GZzX@!k%>0Cyaf~?HpQ;@7xjhD@q|;Ew+HRM>GW2Su?u6fNLP;8r{$? zQCt=ru;niTlMQobQu!F;kCb78q%$(GRbHE%BZ;~fXhsdYQg_Z&pbl6@j6ViRFg|Y* zCHdnSI~ab@WAgq*h$Dvh7%!Z4xX{W!D36MCD{%qxT+d=hx)FKpsOM~n^Z4E)q1Vlz zZJMK7f$|k*8{%RG-`97>7~N{;&M`!lsU)#O{um3^8oyQkcnRkVN0` zOJNc#9%@IujDjiuW;Vh$)d_jh%h>;c5@WX27Y1MU>J}waOMn=&B;s%e#fkLVI3ZHx zLqieAB70*TwvYzzC}RMob$0y0hV&txv!h-U!ABfP&3=iJSDicWY-i_r%r|eEJD6)F zDaNG?cF=O_OoJ}G8p9r^xd>3?<+fS8Q%6h$oW|jPTore?xm5lzXD?+oWk_bjwS#iJ z=uln~?&mj3Ng;RMJD8QMK>qKqB&t}@1Q6|8rDkS|rgYj9KC^Wg!@ zd4-vE<8$e>42CrBVh+R{nwE2@NbDNQ3f3j&(3L^8kg57k#hl7IWLEI&Q`TSeb?8vki7y*{s3}K9KvQ>2u;SU*!dXt^?%L$!> z*gI1BkJ9kxgWz6YSNGTl&w^E`KbhhCI-Wa4V>bUXSBjBW>9?X((a^&*Fb^sp4Z*kJ zdF?u@BG4RQh$DH(PgSmlsMPC~EXd#k7LmR|Bm7yr&I%s%;=hPNlYZSI8&T3=Kh<}k z0viUHHC4f1CS&8$HMfls1=lQ1-M8~n0v)9pZRt2k2 zu2ih*7tZ5ZJa@-$DD|DmjU3fb`fwe4qZ;NZD`VdaB}Oi=c2-&GP|0G%96Sh`4_K-G z2ogT{VlNYP(OQvQ8B0_fU-)fR$U2#p1n2$VF5qQ9|`{-DX z(X>8GybM2^4DpV(bo;;*$oi8DE=PAT0CoGz~t9XhXK!u96~SY^mP`a$7yASIZx^B<#=?$bdXr2z$KHy%@`=i&x7( z3FP9`NY0Z7_%=Rd5co8i{Ardxx}D7=@+rnp8fh(~6K=;wa+>T+(IuTcM;7632?Q=$ zCcgjdZBwKMH)RQ|UC{mBHieZiJy6#}QBQt>v+ijxQ4qtU75E3S4G&m2V24@o4m;qg z!1&u5Qj~|8;4pugY3vxE`3wISVSe&>YQ=IsG>YA%;xyFbGkBI^JhuMQvI5x%Ve=HI zvjv`vFfj^~DlLg3IulGeFw`D}xma-01F{){?az_?CZ7#9ns?yB>Loq-y{0;DYumrL zTYYS$;CXQyR+$eAH*A{0>T~!FoC)pO&ZkfC7ON5FWHU8vZczPYF&X5CG95`zi)1B9 z%hm%<`1o zKjS1*v;bSmVY}$qkL!zMLG1OZ4bGI!STeXKf%EZ=pXQ#S$qhx#}zS;g=c>QqC$ zmE&`F9RX>E1h6E|ucB`Q=dUBLaZsr&osoKdi__EmfF&QZL1$d1`Fo2GUk2oqBTv%a zMR{PLca4`imgXtY7-KR5FBbWAz8%Wr7*}=h-!!s(iZKn7NF`|yqX{!aomZS}8kF*hR9(HdEi_1fAj{n# zCQm7u#$9RB_@ogTD%-mO)mH$voUg+M_bxb>p z$<+vq)UknIxY2pCc?Irh*^Dh1F-elA;XO|=akeb=fZtp>ILSDzpW?fSB;1g?z#-1N zf*e`?T;Vp?&@*b~N7?~da#d&>5>hkoo#0J_YuL6XJ#&DjIKhb&c61BjczR&IQ@`No zh{(?$uHb!EBVJRJSGhL=8DKMc2<{=^g}npgT3?<-^0sgfUP47W+Z~^>6gPe11v|PW zX_BSQ-3{^lgQ`oU(8+&G3^wZvM$W?IX+jb)-R!qRq$uUP`15-Oaar(bdXIN2l^;(6lI_BDurKhfyjR62NW}FF<9L4A+q8 zu*KVV{O-@f%l?aa?L|IK>(vsTS7e?9a&B=-18*u)c#58i`dZjdk#RrHc(LNx@#mdP ziH;qoW^?3r&b)A?_Eho;^re$l4Nh`SAunTn1U*j8pGSj>N8&-%70**Ljn$M};Kp@+ zM0S)N<;MUhYW!;BL7D-@>ZtNGM5Ja0QYuQLBInS@v#4-p-?)nq%tcE<#^d1{MQk3T zi`A@+-m;{m1=iyKA~JC>zQ(~_Lr{R)K)96a!g?Oa1|q-4tzy=zw2IuxTMB=`eS{P> z_g!sDfzPp5%aej9|4s7Xb%ye8)sy9UyXI~qBlBTj$z}wcWw*eyo~km96(^8d2P~$n z$NEQlwnC8csn-u7M^GVOM)NOzAHNC|M@ROQ@QeRu`t698AWYs3V)mNwz6W#7f@_#| z9oxoiZ995GG|S?``t`NIg?6fbU_)=RHmPRm)}`1KXd57dXM&TseSe;R5pw$WIf=Am zExd8?IP1F$J3sINFkSS7E;S}v(fhv&@x@?} zWhvd>^M?IRv4+F`7_uCd3=|*P6`UG) zrQN?rf28qsfcxenhiEt`mb(OUcOuEsx${C)G`b#7g%JaRZgPUuND!Ibaahbm`CIZC zEU_?zx(ho@R-RA<2WC!o`E2M0>+Yx@PUtJQ%5h+-D6U}gl$2qQwP??DLTRM?_@9N8;4#Hb};s)udqL0 z>9aQ!DfQv0H2g+ml>_vGCy!z3rdSekc=Ydtrwte*M3cB=bGlM$jBz0WjCn^MBWVaZ zKy+z5p!H-pI;DRuQHDHTyK`ef{Wc^6V0IC*a$oc~xAJe>?Vjn?LfjTa;djDPPfP!0e{{%=ZjN54?H9WMB4^2dsZNkNFsx*3m`n-z|{a zw5CUtwA)C~!l(n!e?<2DxqM5sa%9>MVBSR4>wa^7xNdQ~i3UdGU2u35$?nx!C*vt0 z!=7PFq`YTXo1;gqrZsxtDOh28j_fTFhvY#?ccQlL~5KTG$ z8xk7d@Sw1ay6%fW?u|-0&xa#8x&6?Pj&ik)@ms|ElXdfr_+hYgr^E3TT;dM$ zw`-%n7gTJGJoZ=YSRTX!Y0+3e!gZaoMl8ryUq6PApJ zcBWYxIfNghAgaoGG~8)TJ*mFt&nucms4y7!%=aQ|t~i=6s`;T?4Paufet#*u&Eloo z8q@DinVls2(2199#kn)ycdGn{nK7nSn$}qW>an)B-f&^2I83-05h=Fam{NI8Iroz#@gwo-1blx*|7xBO`K7Y8-~sLZ#} zzWd)j!KMe$YM9-NynXh;+$MdwvWwj9vPw(5i-)YiJ*<5hWSfQ6Q+tZ8Bo<{ZS`n|9 zx>d+5FphGtSWAt};-44Gr}pCJLQRdC)9`bS78JMH;Zbv%7AIQr)4%l-M!4IBl)hWl zl%T(r47m~xi-O=UdeucMcKSflHz{V=>TXR@aI0cJIei$}J<||0wnUL_`F0~Qxj0CG zJn-jH>L;o*kmzo*xA<}b&H%E4!uitF@hN7ER|p@H+tcL7vNfcoalTYs#B@0a8}y0N z44$TwF085lBaHdYSZ_`Mo5D&PE7G`-hYfkji~XA>K0!Y;DjSK4a*j)Pkv46$)|nGz zH{Z8YB^q|HvAs1SDgXG$R6MQJP&Ki4rnxz&uX zk=!YNYrK9ScJ$o(a3|h6D&se=pjdJ+1g&gWUN!O!UNN~>0!X%J=X0~@^vdslEfwEk&P{pnOn3d}y4tZ& zM_1)Zu$gE~%%zWtl)N0A_EJY|+btiZPv*IXf!pTHcDK@s#|MPh<7wYy=1S>sdq|WQ z^Fh15v1s`QQz}-xTfCe-6U~h>2;h(_&MqGBJ|1~5Hzg~``hdkEh}+YJC-g%87=udw z7sksJ0>bV?O#nkzX}!jhW8fFTf1QAarTNY5rN;x|U{OGn7H1hY{qezl;z=B31Z>d% zLI{0lJ@|rQ6_$Y2y2QA}!0?}g0e;EYx2zZprc4ADTZCDDSZ2Y94qPOA*eI%m%Q1IF1xG|0c3o` zVvl>X0Me?!?dFpr{O1n0oN7!hfZ$>OJz-%;?S9q)TE&9Y1?*7eMNd_)#M+f%Ieh<6 zZh3zKPUt7=$gC2=P&{Kk2 zgug!yj{O>?414GX+F>b7UzJwc1P|goM8oalHr}Ng{KO4zQn!!G@n*ko?vrD8dgMon z6c&ZXEy`K54}O(mp3JQiu>lm_3)ezgFVSu>c>|Hu2dvNa)$J)uS94&fkc`rCsiA9T#G)agTjb= z6s}cU!7noPjEY`U+7laf;#sAMWE4gJa$xyx&o#VI$3jm4EEtfSsy1XB53zQXG6u7Q zr2Vb~H_1}8?P5Fao0C5#!ZuD4SeGM1gVjhodxGcw!&~u72kQ^)L}Bmof%vAz@C$># zaJ4=kM`8ow&3bRT%)?Nk?(jpEKjVD{zy=RREBdLw3%awBOl_Pu#J?(J`6iJDa1ZK} zFArzoqUo)_+F1QoFgl+Qi`H6PZioFp@-do`Blh>?0YDRgNwP>>d1$L}KyR+m)rJC` zKY+9{$B+Z$gDfB)e1sApje?ZXe)tG$@;0=u?3yx>2S~RroMr?a1ms=-W^+BgSBD%G z4*;a8b5&QG6oGL7GZZwR;AL4xXd5ALOZ`v!lbWTeeP@6&peSThmn| zV7|$JL`@T<{k?IbupA*t_`&me(M>YqL`pAEcAYJV?}k}{OsI&hT&OgIC*gc}m}5(r zX>}585UXs9{0~qSx-BViaBdk~axTIwPQ4n!_X75P_LpNDWQUxfiut$7^E~XLi5tjI+ZS`J7 z@X+A z0MG6WH={pv{3e3<1da_XO#{lK^1xW-ExVm(Q%uwo|-GQvLFl3^`K-cfOF=NnbpjR7}w9QD1vj zy*?ymlDB=ZdCP9#AVeSRIVj4CeOF>gh*yui6iWGVL$mU?&Sqsq*0Z+xUl_ogk#hh~ z6gMGtkNe*9R+$&N<+HJ~c$;UkCo zz2!>e`rlk3yd}Hl>~#&n7OFuRKu@EyFn2nB?;KZw`pLX7ugfJ;pxv_xX_BMOKCqf| z9jhm+SKuL+jq!%~JgCC}LMygfl%zcQY+PaGyPk2<3(PWW%Aq4Mi(QPIro~n=f(-D9 zJE-OX!oK!bE8fno^HB-rBA{P+$huly1mzt)Oz_K>@V>@OMNK0ll6?4J?&o5yfkgzl=T68a%Vv|kwsR~1jV({Y!)#w`)AsV{M8(<-f@ zL-L@xE>O@Z6X(-XYPwJqCQ2b7<{r{`a!@fiY5&`??#nut@UC`rKkpIakH~FUIfP%x z;Y}m^pd2bP^y~4ErEwSS^Jg_qz69jdgSeZ<@FA*(tW+6Cep2zC;08G{0|f>Qd!&TY zdps4M8bpfTIKWO{AKhA&AUK^4C#SbdUEKJzj#C-SkRCu67L|jWIUdw02Jmx3c?$x^ zh9q^kfh$}ObBCej{BgM>Y%=-Uo>+zE|0;K=$m~LWA|);`LwBc{jf^o?8claJItG0O zDgq8z-jl7gs7wdQ+-r@;g*+_X#(kowUFYlLIL}FTDgKFD#QBbs3ARKsS6CeGhgpr} zDR0x^mYx4}yleiEAbbv}?}$N<>jnk|SkY{wss2E`Qe!v+IU>cm(-0l(=sH0SZ%8QV zmZOJImNSdY(=kx*$u%JBzu!u4l_#s@-p|? ziRB%q!s93a^Otn_lW2lFEw9{nOPwYW@1o-oHiL`}iK$T1an(*__OOLJRjVsCfTjY# zwCbaEc-AOmdAA%Xlz~>UW-Qwg2ln{$;9hLJR-R(~lUfdw*FM~QVo?03lGrq3(x`Tg zJ6c!fHK=|*xu!?#!&tK`d_u+jlNyh|#asd@WKrNhpfkT1oYcn&jNjM!QiOCG50By+ z1nf8n>B9QvzrCLyLb3toC}X?}>Y5MY*^nM~95(QyGFNY93go!(Nv|S?inQdyfhoR& z3hGEy^M}8Tcz+O%aAlg~#-W0wkP=<0Tkv}gs_y4N)tzE#Cbxa8 z<|8`g$Z9*^T`=1n&s(d}S*sBZj|f22A36ra+wr1Vtgxj;e9uw&Hq<4+M&65tm+9;O zD&Nas#3*t;{n+#$5yAC{3p@Sz1`0r<$Pg7kIex(b^A)|Sp|pze#hCXJcVKo>>G8O* z??;Pv&o6_soDl}_wjK8{rf zK|Dr|oZ13s$GWa#FV}?wAn?3z6Vm;i(?SAtCD4n2GC6K!{RYEJg#>q#3kc`#Sp3C~ z=R@>p1;=M4k3lELNzh_pf=;}&RynpoW0UpF9*etHp+@4E>|B;gaMpf7?y+bWB@N>n zxpoHJcj=^S-yy-SKn*v#!E2A#_+pVxd%rHiZRR88I8%d+`oypkJ*M9DWP~tnEcIz{ z{H#GTovXM5VzIMHy9kK_7u|7|x^oxZdOq?Rwx(qo#>@iRMozR;JmTCbwbDsPhv^Uj zGcCJTl(k`K6D_$bh2S2O2QcIL3Mfwd4jqOSr(i{23mn-!m}`gNzAIM|6cL&tTn!ygq$2f*N`gn{E5#SHmy|$yKlyE*bK7?^OC?c>b%(gzdRdQqM z@$GZwQfx>=Vm3p2N!A7^@O@W^6E6D5B)G&K*Nl)WId#-AH(ycRQA3rP9~2({OLfG_2OWCU5r#WhffBFTIqUV+x*-4+Mo`R^6fdc= zWOOxMe+rtzvc%XnFWSBMufuFf2=t#hIYL}+wV4Vd_MP4A%Grx@$4s`(5-u0GL>lfH zj5wkZeI9HGOn%J?zeZ5g&V73-= z-d3MTZIeZAund5K21`xQV421{Mqz5l?cPI!#wMrJ@}5{=R$)(N9pOV@mHtUPIenMK5Sk8+V_so+Dc0CmYKCr%(NVH1Pki1};C8r&6Q42UUSw zfc}MWz;d8<(ermdjx+I&JY;>!vzXphUNn4**jCWiV)LkbLe*ut^YngNz`1~St&#E_ zTU4U3MDu677ap2?s@=>@hV;NDKh@p;H+S+n{CW|)6*@6v=ij1N_U@yC>G9M?>#3-N zF69@^69z~Tgj?FlF${w!MIq!+T}davacW{0+6`f%KHxAXAeSsK);GJ|Uj5mxn4LI8 z48w1%Ru!NQ&lm=Pw>Ol07{11m_V~?xlBQMT{=`Ari^N*zt=4|FVdF>!P>0Gl@ZGq7 zElxNSn`AYQ`Rp}!-=8^wg(~mPBaEqKF@ZQc1$uM0%xP5mgAXen`fv})RCT-s8kJyx zQLLK{OhmuOtYRY2Pw?%SM6`bAR_h4F8Y1b@0+MIArywcJX8m+I*L?4DGxlc&yLDGc zro`kst7*#gAKk?&+ zwZX%ngZdkR-%;|a)7aB-0Znd&*fygE2(n_OX$W5qn)i3_R!4>XP2|~EitsJlne0S$ zQ&{yOLva2Z>uI<}f2!HlyC+7OGN~7?bOzUrEF1i@y+1Ls4KA%(5^M6Q=K9)|J$*E@ z+%QUEzopSeK4+g%C}N$%-wj;cukrm4mGW|t$Xb&>d{yE?`?5843mej5sy#(c)!%4pTQ{b@ z-1_HQCOdi>TerK5&^(cH;3f>{XDe&rs3K9=nQ^= zKuQ*Z6NRprKI-Al_D}utU1aot+mE%a7kO)LEvsjh7e@{+w=UgF&N)fZWwVX&C}bbE zuAHQLG!LtnQ*|uw>=vV_m){RrWi8(|?y(mVE3>~+H|saSyE1r9KQ2pVveKY_(Iwq# z)5T21&sLRidrMa=yXS_A+PGsFvS*5;V$aBqdpY1*j}MbakZVh5=xpFpef?Ie7WD98 zjoCClZeZDc^(}3=u+y1~qNkd8nt=2OysU-FL3F3R&%;nFVO$tWMZVjuD@4Lw?Ee(KbrcV?Plrq(}`Mv-!sy)SMv*UP&KpD0${uMllhEV!++Yl zJ(w@pY>E3Lwpl2zy77;-95~WJ-K=9?>3)d#SA+69jve+>p?@9aRNLmoy900Z_=F9t zt*&gW|DD*s(y68u_-3Gv;HIYd>xD2Qt~idQrZNBLi$nFEhHZ=gOymG_f~Xoz!gsy{ zIw#GjzotS;StS4A`YZY}aVpTp-W|8K@)v(U|9L3y6g0lA4@W^~&#&4NSpVj4xCb2+ zivCE+Kj>fE+2@8^ym(-}2xrg}SV;#;wTwAzuMZDYEbrAlt|`57 z;Pu0xz4;r>kz3Y1S23*ib*x-CKNhad@%H%x4q|8ws+(F(adPEDUdxg#LDLF=Ip;iU z1-XM=eqv$_%36PC?!cI!@%x~&`=c_qe73>v2BgA$sy2_y&8fmaX0N#I&gya~vcrCY zI(uwu(-!9zU^8_n1v?nJW*w%dbV()3*L{ig0;)P{xgM!sII>|&7}#|2`_b)xVSk~Y zZ|aIV&gV&r8VdJw!%rLKnW@$E(!&vufWZmA#bj1GwxF7oH^uFAVv z|8u7W?@(B=@bKp9SWTz$^|gxQ$x8pMfi*RnYS*!e zpWkVhd`Ge;)`IK!{`jGsTb8dU>+7YYf4b7r9P=-8EF-&Tj(6NFXSxoz=q*j^Jsr1< zuktIm(9iuiA5gzdWVJ#N@Xl?YW?czQMWvUmg=^#gq?mG-+wk{+&|#s{#_eP$ucu_~ zJ)5wWpSUtqlYLVTm9I9u+}1REMX3Dsa)F13N6?su#hQm3;oGUEEYd;tsev}j7~ksf z{O&6+KLlU4X7CsJ+LkUb|N1X7eG{Xcaj-vRlsz$97pm1WANKCqwD$75s@3~a?kjEJ zP?fs{?Lp1%{jvRhjNov3>*3sqM05YJe?oj~%_AY6;O68uPsHR@zL^=j zreyl75tnBzW&a^Vpv9#l`1x(=!s@-BB?1ZV@)bNzV?BRsb>kieS*`}XZWju@*nXY_ zB|K50&*0BdS30La$k*@QKH*f=yVn9P&vRc5A0BEeRn;rc-s|Nt56keYW?I(7eePPq ziBzSFHK!1ECW{>FkU7?RFPTf<=+Ccu=g5CqRUWJBxf518CaYbjv0#wSFfp)ZUXo$< zd^63EZzc09`u64QRl4MfWyx3TN-d>o-7yG-HS?WJYYHi+j$Jn5TEWM#(QE(r0l>)9 zD#kvru8-~K&tDIWSUlNv5y2rCvwk2naC$7Fa`~Je)mJlJ^Whsxwn+p2BMRCIx(zZ-=>S4=tgAwz0WIgZPpM42Q;mx|eUY1c1B%_t;66k&NxRkzT(I4KFpyrm zn*kG;!mkzqG**(%^%#Ye?*mucCY z>q^}_u7oh~zHUpkcA;ublXh)Z_i7X#frC^3l^+j~pY2T<__?#uwYbroaqL4_=V@dz zeE0$YwSAo@i8qq@k#-7RDM=tpe{ zV`J`+Hbm14?^}mEza(uu#KycxgM;qcSe!vf(kU^ek2n0q^Zs%QP}OlxH5wpOJfblH zli5K9XH{4yo?-I~;Z^9Ucw`m$!Cb+3h+Av#Go?j~Y5WO!rHjxHFH!bieUe z7sa=ur5wKt(1&n0Fe%bC&>}gJ-cN@84AvVxp0pWah52&*FR2=qc^gXBpZ;g?p1t*2 z&P%K0-}`^O6`Zf@zu!NDAP5%DuKTHWOWII3K#|GF)=+h<2VcdL!Oo4CbNexK)6pes zgw16rlTfGj5`+m7jDbTP%>oUn+{HS%q*!<^jM8TQtm+PqwEU!mpT~brYuSfp8trD9 z?0gSu^ne($4mdND9EoqPXX#TjBI6AIshNqxmv-~4cbQ7&LQbTLEk%#vzu~pGcdD|H zi83^M_2)J(kvNDYrf_e!na$?22(0Lj3X0`&=Ro(@|1#-R2!(C!x3|3~+UXl=Z@Oo5 zdXsXVuEo{OtV$Aet z{9-PHMzX6nfO{q4x{1Ys4#ULj`nPECLaN-%!WraNn=5@Ql#`imFcVtKsXJ6}%c;9ZEzY|W~Vy*k{K-47c%2J&1t^>Mx zc8sT znL?|VP%k{|b0O>!b>;7&dss)^S4-zz{(LXZ9|-?BB)s^u;QOha9>$kyOf9#wM)Xxa zFPyVX@mgzi!;jd`>d+RlqAvSJ+CtyY3}l-`4jir`#=B1q%K=06wn>Rjmw%}V6e5|u zIJoK^=3=}gnapIV$c0R_f}rkS9Fu7Qg4lwMRHv=&V%H2$&cWfIeLM=5>Z1xhiiEIh ztrr*#ViAOZ(`^c399Q>+uN?OL4w-FweyorHbMDA%Z>oNXa?UdZj*j098{O2piwbHz z(6C{-@DKul%^v+-0I3G4nElp^63vxL0fyd7Yu6jgVH4%C>@H5!$4ouWM7zu$emv0#ns-`kt;#!Ect>5|pupBa0mO_IPvM$q-XrNh+r;nYpa zpUNF7Gs){(IgtDVBF~(*%e9Y4#SK2C0I>HQZ}B{dY4Vg^%l)yHK1UOlpMuaF`^3GX zd#(F~k6gYj>Te`HnO8bUW_H?|pA~mT&Dvf1%-s3LnMjf(kF#gos=(x95!9a&l?u$< z2WDr0kUaFq{p z%+`N9FCGwp?n&qXRT2{G!Ba^;_;*g$(s5npsYRl{9MmdutUVn%ZnU#EFYKngq7!y{ zOS1Crhim^P8qHwiMrEc&2aFe%mOY}@BfEwAH}nTQ+gi*dEnPOz^i;W3v8@d=VKhZ@#6<(1r4meY$4V6Sw!BSCz!5jx&6Dd*NRwaL&{ z>p6y_ZuxO=SFW!;%DghS=)1s!xe-d0ft!ENc!s_bfS*rF5JtG*UsFhRBsS_|+F50n zI3fo=Z?cxx`mi5}$`Pry`1ixphgb<8cz!CS9j&EkNFMyYrEhm{JsR|!8ld$5gc-bf=!NE-x3IpU4=y`y9R(tv zgAZU>FGFuT#hV#4F|Hh#x&Tt%#36m)*9$Yk1y6&2t3D-tEZ}B!y3#B5)Uu-K8xR%S w=^`LQ>n7$obgo|W|GYN;|M45As(pb;PqQl~{5;j*FN8o4ng$wW*KHpCU%g8lq5uE@ literal 0 HcmV?d00001 diff --git a/images/sourceImages/noun_message_1977390.xcf b/images/sourceImages/noun_message_1977390.xcf new file mode 100644 index 0000000000000000000000000000000000000000..9dcab2481426f45c194ec8e479edaedf3605521b GIT binary patch literal 3554 zcmeHKYitx%6h6BweQX~>skIU$!wR(gXz2o_RQnPmhG?`AK@CP&+Uc^UyW4cPP3sSf zm?#)gLJUEJi6j^k#Yjv*{UM3`&=?C6NQj~mC4@)Ww)>ptojIO6I|ECi0sr}3ad(L!{>f$#p|090-aH!NE{3LBRbSsgK1+`$ZuZ?GN=X^VatTd!qf(L80DK4THgL z0inry{L83&+dp*s1?%D6Be8)%Pbd-;{7v*VdfS)%(J&MV#f4C$XS*2VGopQcF)=P! z;|uJ(dHZ0#=jwtu!eTto8;A!uPO-T5v~`Yz`-Nd~FcykN+Uxv{O?848>52BzmfGt! zKD(}AOz3QRCWBJk~7^ZU}@2`bA&I8iAg+ zXzjz zn(i>;h5s<4dwVn{z!BvvL!6W zVwC?a{??Y3m90&U1Cb#00YAyS6_M~$jwFk?-MNF_^h+S&ICoTP+oZr(U@s`Jy#=;V zV3!rxK{+HM`DOk*Hq(=I|0^9keIx$ffZ=TtCZN0nb&4v5r zipT1-E;6`K^R(tQz?kPW;J!tZW@2Y8=!ZJtJ#8WC%4-n#tOm64lnBw(0@M;O!p_{O z8c@ek+Xroz=A-)SO8AH&VL0wa_7zl&*CIOs@SPj;=q%LFErw6cYSh_>uHJQO}IRmdQD&UwsL7sk3do2zC7fX#Kqs^|P(6&&?tmFX79T zo-9Z9B`h5;vC&f0FEqnpR)V_rE-KkZic3FZq$FOhNq?So8wcL*2Cz1aNIoNdLRyg31@~Er%%cSwyt7IvvM-?d3^_}RFqWl+eP0CMID0EbU408SU-6)!on^F>i!E?KFSb{t9Z&bb zP=?~4+6wWs6F=qaj}>r`QNGl-q5F!HFAuVl04L_5ZXSgtXPorX+UyWC|5AhObR`_k zS3?;`%=19#PshJ}+b4%H`*Z=pHdIF4v?;Klv+&H$odLEc_FV~}a zp|lDx=tgs5N=TZCYs6(6sz4n^HHWIX5LG!z7bf;qfijGknL>qbL?tcL#f==rVY+7Z zG-k5%QBUcZHOPa76B{xK)fACr)?jJ6l`WXkBWpON6G@uXou*6N!a3<_nBpX<$|$Ai z(zkG;3UVXMbm`}412U0>g%jPeX=HLGlEO4Giv@d;C73DEjig>jGe>k}!Sp?)p`z0) zC5b90Jqst&OA^jWlI2h`iS#VIf~cCIk|1iL)zfB$jT0s17%f3Tt2)tl<<0RfHG>i} zlZK3nL|&a4C(eM6;b$%8(W{C&`(dp-pkkjD+cInwVY5p^+k%lvIOQ zPLlCvL=;I{!Yq$pN~7e&UyLj%p){AKl1szMRuN@MLzyU2O;J6Z8xy`@ruvDH0ejG!1R1ukN8P7nC_>R_TjoQ_s@W?4*J_R1{(YgEX2Rb literal 0 HcmV?d00001 diff --git a/images/sourceImages/noun_nodes_2397529.png b/images/sourceImages/noun_nodes_2397529.png new file mode 100644 index 0000000000000000000000000000000000000000..c14e1e09f1474d18db90715df52ca923ab8612b3 GIT binary patch literal 19083 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ȶ-dPfoaX8`-mtrkX;b0Mn4e?Y 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 Mp4pA^YfgXtU%#7-P5=M^ literal 0 HcmV?d00001 diff --git a/images/sourceImages/noun_nodes_2397529.xcf b/images/sourceImages/noun_nodes_2397529.xcf new file mode 100644 index 0000000000000000000000000000000000000000..8cfe1abe9e3a51284530ac3cd86f1a2b9f52efec GIT binary patch literal 4359 zcmeHJdr(y86+gRoUo0!~)U1l`MG#PVt4Ks7>tiMvMj0(yr)?aBW!(wv5?Bn3Iz*KHV!m1ad?AFz+R-|6`-%Vc8#llGtf z(eKW==l7l8cg}ad=e<^2L!+s+rp~lIzn}o9lt+Z)gCq$g3I)BANOa~GRKMgxIGIF6 zq9&OZ!AjC80Lm89@m30*V{53kT5_ySwpua)kMl-Wv$L(#+F zs7g6uE<7$*b4IoNE~hsaDyrg94KN!7Tt?D9^~fYxvUYVCmw`0RXcTsUBJNS}=X4Ju z4tD1(0oEp|kx6Zf0H29L++GN5ffR!*G`|4w)mX$|k#>>(Iv-Fw1yPd+s3EPUdLP;U zLHx_4Un&5W;U~`#u@LYn0)g}*U^c%F@!drbPJ8LL7IAehFh!Oc!pen`3fUDQ2g26- zD>Z;!YQ%3Eg^*g%aA&IgtmB<{Le0497!NLrW; z_($R@)PSo=pUwh&ob)Lz;M=4t=K)iHPAx;(T43pZLN|38fK9|5hyi?_bZa`WE&E(0t91jq3;>pY`|YrqbHLf zyy=VVR0wMedEj#?fXOP5bcKalq8>XLrfFSb1b_LdFk<**c|71gTFIkEz&%9mjRX7{ zt%lF|2;fhNc#bkJqy<~51T-e)unML}GEi@1e3Oh@O-Oij)xA_Nq`FHFsF;EhZQ}(Z zqJrj(SLuZ0@+z@6$@?y~E~C~r$$JmgyQp55bQgyR?}-Iw^zcOHOef?frSScsKg^KRwQO*B5W}*mlFcs$F49LLgkd4W(023hx;~@{@ zpa2a}gnC$vIw(dhi3Ung1Y1S6|$}!@Axqis|4LODmt=d+fxAJ(sTC5We)>y7qa`#WU}`vfEoUYy)FN~J&ZF;N?sX0M{K1e; z@`M5--hsXwe>;Al-Ym`lpZJ9H(4RiLbKe^bDW#aOAo)T8x2yNUTYGBF;&kw3ZtQ&b zliLHsAxTOIE2WWu$94TL-Mh?UGWixgb@P zr^>LNsZh@pKle zL(Ze@{xxC@u^YS2-}P}{HA|7%fD{_Kd2*Lo2g7>v&;Q&@NpbQKRFi8!3JhHStwYqp zh*yGF*vtqw()ZV{HKLlhC-$B1^UGW+s${>A_vVSF5;geewY>d>n>tjnIT0#& z@6w^oqCjqyhcEUA$W36P%mpRyol_kp0{F$oXXe0S7K0StlyNZ{ByoHR_Zu zQ-hMX_r%lmUnXvS{f1kPmJuO>_b%*R1yKC#={qAbF;6DklKbj!YDHLi@YDXFM7?K5 z%)@t1w~FHC*Kc@2JgF=)4~3;b|AjqfvGL?rJ~>9j8XCNEWUbit{#_nf$+G1TUdesq z*rwu+kM7BF$Be$-Kh&D{Uc67!C$Rabqyc%Qk*`kJS021HD0elV+&$D)-|5zMuUs7+ zMZ^|#eYpF-v5nWqxXC<_wnyS=KR7~P1PhjS~JN>&`frO=zL9l`1F&WTRPQ~oO#1TZj3mJJ>hjD t-$fJNREd*cb19SDkaT-adb=7Qc0H#)^oBQ0{)tGR_~Vi_?$;;<{sp6TI+g$c literal 0 HcmV?d00001 diff --git a/images/sourceImages/noun_signal_1402315.png b/images/sourceImages/noun_signal_1402315.png new file mode 100644 index 0000000000000000000000000000000000000000..4ea4e8dad35a14a6dbb3f012e17b41879b8a9481 GIT binary patch literal 20260 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 literal 0 HcmV?d00001 diff --git a/images/sourceImages/noun_signal_1402315.xcf b/images/sourceImages/noun_signal_1402315.xcf new file mode 100644 index 0000000000000000000000000000000000000000..9007239ac367de3a172fdc04d18a9009273c4199 GIT binary patch literal 4610 zcmeHJdr*|u6~BDn?g9%6M%4Hjam841e1U!t(jp?k1WPip1&oSD3F1O!!2t4bV#UEa zI(bkZNlmmm!Au$>$!Hph&fuffLu|#Dm6q zWe$7{A>2mrP%To=%F|_Ob7pCa^47vcSf1L*;*yH|98Gaye%{*Y>d1B4?85xQB2A>5 z%_`Eqkfn)of1;-Ye&Gk;XpQ^B;PT=PS=o67T1|8mw2JR(US-zCg1izA`;>>_{7hF>wgY$2R@xspe@ObNQf1GiuZGQYk!jS?{jcH8%waM3>;oW z!n_|zzU)CjL_$pLV{_(4{Yb>}pAoSnx3Hve_7kj3(s124o@j6O_&EyKJp_zvf4-1_{c#%aF!I_I8py*V5dtF&MVyBdvL)1qxEF~W9x;wMxxLaiF_VaM8>I)i zF@)~oDE&N~$fIQld%_6K;3@5bVU!EPvWJKqb`Qb^j!68H73zlx-Of|_{C~fu+B~s+k<04P!Vc6daCGuE1N9BL{bA(pI@+42}(_g<%>GjtmdE(%^DK*XF zh#b@jFXx01LM!0)6uckD5zPVeM@dO#X)`R-F=a;pglFMW3D-s}wi#2Mg}w*&*8(Yh z6}%mKIqYq~+eZ>Q8v2W{NAT3yh&H%5h&oen_D0OvrvOk2hY2GHO$LvIUI3l~-o#Vs zvTprnP@ACUgD(c33I02{YZ348gJC~Mor5Sb4Nn~(V*JSkJo&vJIt!F91&&L1ZPapT3G2KgSG(goGOd zsber4Q(edDSw_HhCtOG4n(AQCt6^9S;mZj)wkNFqRJJ|=(>eI;g~h)I=|02VXTng% z6X_j#afE`%vl~$*jkDnyjVc)kgYY6e3PXsjy2~bdlq0U71EKzeu7Z7)f=~_2^3%wv z8H8jE2_56K1~#a$;2p3O3EzX{od(@ z*7j?+Zu33H+qbT@w>F%9Z+CfS(j;Lbxvv)D*K9vj*U)yex8Gp0Sb5uhtHosK@4ea9 zPlG5cT8J%{k$n83{iNPn1z($H%?r> z(Py+dr4W}{a@vf2H!h!eqc}wvLoDiUZ1cz`yiMc-M!S1Sr5h;`iSNDO>(&c6r&Cpq2y<%oCv1*nv z)DBc7kD@9!YG>#&jT*D2Y(Mq&J%%dfF;wY6?|pr0dx{VMG=9_3E4>ysI?jt)daoSa z6fgJ_n`ZTai+32PkS9glPT}cXzn`q$`S$pSw!9YZ39Cr;beD0$Iw9A z`#OO5W+s}fRoUHI0@0;ahGjMi^2>`E1c%$nAIcOI|V-rri2Da_Ly>l}1G@HROIJCEt+ zi7(aPKyqH5jvPjpWV%uRk{DZgrrVvE8U0*lYxkMT*r#5**y~Q~HQIYGzV=k=u7;m&(k4le&MjZ61`F@P)jE~jJQi~UP~a7oE8f7)YX`}D5U z*7NDh$=GHWuK&AjTDq`Ju>$u#Y#ek+xb+Hg%DST+2F&!`sX7cDN7qdemhNxvbEoy5 zs;#ei|58Cn-g(-Sn3)YO$dhw(;zbvj;|P0T%bsoR8&DP_+i8*-YuFCEMk16fxnscmt;K$Se& z`+6~vIMz|5A>*Bv+N$TKh5~zLQ*C3H!HPyT8jZ^4v>Lh^Yd1YJm36U2 z6*cEN`b>6-cZIMnCfQAW9p`E)771a*BE;qH{iyM}-fWjt*pJF(k?dyu^~R6(=EezQ z(XYkK%9=B+U3!yk$SDQ6jFNN6X3}@Ho~fzK6ov8V#Br;t4xeqlc6Y#Nfsd1y{G7%i zo5eVA_geGW!&R%|gh$ArFgI1Vqo(dc+s)fO{RSg%8Z;XEdv4!syHHoNLzg;Nm_$s% z+=VO4_8h4@-_X?B&Uc#HTbmlr*B#kYwsPTIVG4d(Y_aLtr8{=L`Sub1nB~aZZ|>Sr fnw=gi2$8@XV&dZT71?~gJ$ps^;zaz~j3NI9=HHX0 literal 0 HcmV?d00001 diff --git a/images/sourceImages/noun_signal_1716563.png b/images/sourceImages/noun_signal_1716563.png new file mode 100644 index 0000000000000000000000000000000000000000..cb29efaaa46ba0cf20066dd480e4ce69d83419be GIT binary patch literal 23003 zcmeFZhgVZu)GwNVfCvN?1wjL%6bnTlw9r&k2whO5NKvGOP$dvL;een=r33{GO{7T& zG4v8eDPkz0cN7Fe3xbqT-^%&kyW{-}ciijHAt8J2J=a`w&H9`BnTe4O&k>;`5D0_^ zrF+8^0%56V{yWUh{7tQ!Hu&$5&vn$D!{ATw;YUxwXHG9&OCJb?i;wva3L&PRf2j+DLlhK%3K2A!ANLC3QmE{flH*&b1D zFEv#Du;{{(f8L*MxR--Fwawin!V!8-esgS7b#5dhq+~6$c-;nN{_~r2<7UkUsnlm< z__P>`@PGgQ?pfl5i>IkCG-!Y5o%1*D|2&KG**Ir^k#Q|J z7s)l`A%75z+P=KN+8Qsi=~SS_s%B6CB_Q0>4fhhpN9Wb;7ur8wBS0(;?MChTE04!yjLG*vK$yceEOiJbq4nFSc4F{ev)IdTt!=S( z2_<3lVr#;k%m9R}Zw|-kabMWP*Eoc7A@m+Wq)D&OI%xil*j;D4)d;utk>ZR>l+d$= z_E_s2SHYFB&=CpLy1xPgT8V9_OX@@kJugiVsnQ$ZK}5q))H_2dvxC8bUeY$v8$If% ztScF`k%Zt%UsbP7;cZFOZQesAl3MR2<^%0tFH65du+$MmDolMP(pz#ltuMl_nz<`X zT_vUowz!CvX(J=EuT|raSGyC*IQ-paRm9iS6<1U9EI~4VUPRsER!}9c`soFHbtWyeeGpK%pSH- zhQB>d|3c?A#Piv%FVVKR_BIA|IpPq9xv0=faA>=quPLxBss7UPHhEJ?~|x zUG8v{LDe*V2VQLxCtZH@5qz(C)$pkKExD3xX4dYt>=!==>df zN}|SqF1V+UoXFO>GIL}bWcS2o|0{&DGiwtn`V`(Be_#+DhiI6%wx6_TGA}3{B!wQc z)eGGf(rb9@7%Psh@nZ$&kj#_W*)n}aoKzvSJaA>S_Q+7JO^TwD47%H`2pY_35eJh$ zDsm_>7x0*k2|v({KJ`=)9YflzLz&8IYVRlQn#^*HTCuUzhfk&mxVkDAQ$f%~Ap@2BrhDZa8gve;ijAt15@2qM!_rXBd`DojPT z+{oeTROZ)#`pg>_?Ve_MdtC69pmbg*e7rs>q24D_O}4H5sKi!KX2nT*M7g}_2F`U5gU21JvL1H zy;=B8CPO13&vwr$Ms7c)h2!%+(}svpKgy$P#k?g+}fYcJ1%@ zq}`|pdLq{EYBuGzI_*gDy}GchCsaR25Q%gxS;I?P;qDQ>4GUK%?^av-1T2;zu48%( zep6XZihWOdbq$Qa&R+FnO^g%K!W2*c`Rahi2H$pP_?};Rzft@;JGqrrIYC4RQ>$r6U7yZGLAUM0nW(m(c`s9wio!UpB_+m_Ld zdIe)zaUzdgixk3n)R;FU9rKc5cvrRr-e-Qu+tH z2Ki10k2$%@7Nu62=Z7v3f}&kYhtE_>r6hdwWia!-(D7kR$*54nE8%`htt& zlm{ZO)%{QrSCRL^!LrcQ^(k&5Ww;b|=#_-Mp?!!w$@*Y!0pEeY?C|Wd=8^h~@OScI z?3Fn+GGaVq0&NHXmK2A6|9dc1$3^+mlfe>`t7_t))L?n!jJA(TdpcP!CqiG^UE8O1 zxhecST7uM3*ICLQ!?M9)%QMyxJob|x5*;TJhAFJ_MHgfv$G9wRg8Z_C?7V zjs&8sV3|%*l({R$Udx^{VPX6wIS-xKri)rb4`lC0wcF66S-T!0?OX$;80FQKfyQ|; zdjF0@?4C8XHXKE{K1U}?jTzJk+Zy|{;%T09U@$mNOoApQEX>gi=EfiQ)0PqXzNUjaU1(bmB0m>;U_P() z7jeEh4SDI}L*vy59YbG53XHASUSld~CMb}Lay*{5yPo^J;kOhJ5!lfOsYUS_{NMnT zqc%y2R@c`)lx9O2;*3vOx!XG)OGPb42hdqQg>$(Jzl2{a@@`2m7W^yMgK`~s z2b1FHj?%v3WqwtDdc==2*566}%E*lm?3)g=z3*I$6VSpaeIF7pV*yvY=fO-6<1&cwc!E`gwS7+h%!eEhI_F#jo-De&AA8%G-)B9+33G6tApr_^`6jCvoSyavtQlU`(hei^<^1` zpKj;nR_xQlE;>@bk&W9n%ji}tZbJ?k2%A|DVKy1|`@);0Xd(mNS~dk$QE1JN1r@PDSj7iN=dyUd#ntYW$@Y{?&+U zJnMEgrv?AIt?pgEZA>eCKop7<4YM(-mB}6Ub$lPJb;yHVi9Vu>S}NvF)epU*JNT7c z3lDnp;q-KX>X$mP+APnwCK>&{Kis$~!dIYoLCGb&IQQ)AjdY#Rqwh0qCrI(4rxoBq zOP!S^Q2q_7s$m4_R99?+HD2V(m~_d+)y6PdEw@Dj=Daw`=M>}GS(_$@WV>@*;pf{n zOR*aDv9st)>Jhu^7PXq)`g7l`$=Ky=xxv-5M=p{hB#1)uT%s8glXW*53Zo~`$l!l> z*OT8nZ}imTqZ8;hwJ{50H$h+@)*iOiyZ)=w=DWWjV6nN>NyN!ZL+o}!|O z7=uq=;xlz?ul6)d{`TW^xnR}m=Ir( zAPU5YpS&`l6JFKNMixq|vzV}`w70tLK2C}ksvmE+i&pHz)G5i^+f13RHs}wiA#t~< zM;JGurN76%t^&pNB=v;=#ci$J^)H@1ef*{pR!mD(SI#lisHknFn9w#2=_ERxw&-BM_FOKYQ(h{ zrqPcb!teT8Sle(*EmQmOlJRuw5BtWO{Cc%nOKoA*3BF#N{QM;L67RZ-+V~&Ep9UVi z2$B7xR!GOryR>+=Z%W_I``fK`@~6|^fJGAYwC``s z=ggU%#Dat6%&<#jwfg2VwX(a&8{KtO=&b$SDKmtY>EEi*1lU{GGw~p>_6;}A)&Ab2 z)(ib7Jv>iGuYqxbVOO5Njpp6EOw*VNGWS{PYa|in9l~TE++2UPQT$TnMR1qtdJ#xo zUQS$}db}#~`;JC-5ZCkJ_}QEyY?o_sg%!@ccOJVs=vcfiX;0ZGmR>HO2}&%WZ**%J zChH5$Z~EJ(VV}ln^zkiYpf88s3X=5D=xjRS3;{SDLsaJzPguz($<6y@=2v9zm z5Rh9!47<|&k;3Tx*Q;k%_X-;P(Bn3Sg+cL6GMJK-@Su;&9s$OZlNEOYR*8L|3SQt! zzhwpfw!2u^s~&2wSv0%tyRhs!0!Qs{7uAFgH<->4YBU+eq~SslHP{^vf`q)Mmv zjQy2cA_g4E5eM39Temwle*lga9B7c&K^K|>F1iX%?-gmCh!sk-8qL@(9k{Xg>_=(~ zN!1I^NbkR|^Fztqs*#P1o+<4mF1STitoL7^3SQOF?^_7`ogwi>t$y7KqAm(QH=`xk ziM~&NpkjC9qGt|6K5qFth1`^_eaFLR>Lu-H1(vmWRHs_D3#2zuzvE}ZiY4aR?Hh!* z?|m-ue>d{)d`fX$q1+ptiu3y0fyFf?hm>4Kr#>rX3wHEsT(;2kMmzx@5AEb zO1sY#s~)1a>Z~i&aEtsw`*}cZ+3#n-Ov_35syyxQ)UV`wy`n7U5B)i!bZ@WW*V+!O z4EtDve%o>KN=z1xETF;D$^&^A3zf6A!b9(TaUFq28KL6ytsw|wq37kW6Rqn#_18mD zjdqHI7L$GEIF7icP)vz&m8ZM6WE}k-t@fBjUFGIE3G*Pl&b84G5&`DKz! zgP!A?+w59N1BT>*;yUYrl=jWbw}Uo&)Y05A=MKW9ZIrL1W(kJWI5E~8i0;p_Wd_({ zB*HWXtuovvN8a)G8g%#WU9z!T&I+J@wEnI#xqDzPLb*KkVX75Cmi&@wJM+|TW zgBHqisigYJ^OL1iUscM$yMm{&@P)MtEsWWNgKXCCgq+6FX}-WML<9|2CYnXgIT!YM=z|JR6oa zXg)|?#97xpVFoaFYbPdk*7`)~D+7+g6!=6rgPn4|c4W^CWV%L&HdsoGIC}cF6)4sI z50JW!{Bkb@LOktLIl9#);8zQVOJ>4eOSr}g2P&i0xI z;{0*qGtZ@#t>tFyq9eS}MY^rMiW7f4)hf{1OFOgDrKo>3Vw$#frlEc*$NVw%#i(1+2YLmedu!Dus+c z?o~cL#Rtnojkw?p*Zfx8Vh-YBsl)-}eP+qKeOCKfg+}p*lABaj-&yzsax7w-qo6T_ z*MbNuXDKz{c>g7oyxA+7OAfYoCpM@m*RMC(9W_M2_xVbVT^hC1Gr(o~f|HZ3YGbig zM@UUq5d5WVn(?sPLkV~~u(ZHAsuPn2S{4n6B98{MBe=;npeP_6F70A#KD;Bw> z^`NHECEY)jBUQV1&CIhn!6nAA|NHWhGhM4e>in(LU|Y<_iKz0_Vq;R+BcJ6$bnAD%kzPKW}D$+*qHPLcUd7ss`dC_Lacljqa;OL4c`QprhQeL+K zUL;Dt*Mq(TKGC)f=46Z}4hVF!c4RfEi8WWUF?bzlfxfb7DXBK?n-^^6dWz)WbuYtl z5a+mql<$A5whym+>finB^n%Vdr8dw{7P2|5`oJORA3w$YE3f_endBn{Nu7qBjWa>7 zN}RFFKg%?dXNMM2+76^=O{2Sams}^Ijs#odeR^C;6MgJ!`v@^OR&TJg0c#{w#!EOj zn^{QEsEym@D1X!+ivKhRL|`fMd*bsai>nGf8nMBX zk~RW8XSf>5k~(>so!d4wEIsKHpPvP?Foa(hHE*L+5C*3c4vr=krM%xU>b*Xtw;H0? zAJDhAN$bsW*{C+$tjDGqDfFC*`1)dJE%^E-zfqqRnF6-2Ao0pFXdQxd5TXi0PYTd^=#XG|7fY(c(gBS^wFawBFtpS zxEE?Zg{DESg&o>3j!rIXg|tGu^f~g$y)J=&USZ=ncTQ2bf*tPecwFYlV?E*5g~iMH zd`|H##o|A^3Xk!d9BltY1)ionm+5tZu80!HC6$gi=eKVobm~J1G@H>N)|D>W(Q71Q z|FCqJiNz9bEa#c(>)rE-roI}K(G^ml7(+P6mDq&kXKfOjoE}+FCu*P0?NM(DdHi#{ zuFC77Ije`*WR~fAL+D7ZQhjqu1v_PS)WQ1@CA-SpAs~LPbJq+>j$Ax)hEp8@e*{rt z>4a_^o=c9jejnEQ4=T_UN#-ue8CD38@k)AagN{+`G$hZhZe{96%QB2z@{Q+zb=x%LKp6UBHC__eqgB$N={ zK#jLsk<3Xus&HyrxH!WJX>@m#Z%7vY@Wup-x;p$F#~=$ww)UYu^&c9Evv=y{l8wi4 zqj`zUXM)vjv#uMJ%tCp=LblwHS9vQTjWIYqy&b_a6!He|2B?S+X~6B-m7!r^Q7#5P zcvqDllvBN>by`eTB<+{5Pd|t1`(&@jNgrzDEXsh%o3=s=v$e6vj@ z^lUMwLD>4$pN`9d@2p)Cq1?lh&{F;Aqn>Y78w1)JiLj$4Ysok)^6E8*TH0M6$@*Uu z4khF03p2N-vawGE`_#9LKR+O6^ok-aIJW56!+Qss&1Ffi&oo!VK-UZhv^pkhVpC&V zDcA93zt?B$35geTH&gh+Q+h=c9iS9*eTUUzEIvl_JGTCcqBOt4?p`Xd0mp@+Sx!2M z=}@1zb^6aj|D)swtilsuS0a{NY(s?-=-I5;GPM-~c;?GFg~f)VR;3@~M=TV80=DL` zo{-P*rmz)EMOM|z*~}>N+SX+QtH9y~@-h9((2TbMv|HTF6+YGY|3h0Y5gtiSM3T*=nGq6o}E+m%5}JJ_xaSQMPeo3z($d7lxlq{P7w(x0g!EdF}x z3jb_mRnhBHT>@Lffb7)hMP)5x4wnbem0av$%J4!j)N1=CSpC}jAa3=`@V9Xu`0k=0do*1=_1PgZ`AJV`zJHeFOT!DMan{VY93v-n6+ z5=>6Q$iKrX6&$z>!1$G12RLuea3I>nHU;+9d=@He813?ab7cqn^dHys6%fyu#Nh(C z9)GqTut-$6Ivz2QY-^#0fWLiWgXhT6eoqphN}&XaqM_y^1q_`q$|O9b4ud2otIR#S zZu9?P0e(nhzOp9-oug>RyKkqVh6@_kVS zmq&o3<|_QL<#GL*d{osqRgXMDJ37Cp3M9!)TTLj&HLi`8)G9d&=TERzq);Fnfjm~J zRwP*n=yU3!`w1{Pox17TO7zN&DBS8u%?sxbB-QoCt2PU`Rx!6^*cGN2ggQ>YQSM!Y znPkZ=g{XW<@=8yJy|pc3Ta1VGd_JnB;4bxcq8w;H{+||k$@l;8O?+s1>}{6_j)$DP zuP-n)TtPXk@0DYjd-c?Mi*$E^T5=lgM>!Qy{+*L~E{c4q&?7bK!1y?*jtEXgV)3fA zTxCAU$UucJBC@T(iQZX&+v4?s5fki4eS73Tp1#^FJZ3*NxZTay3!VKPPJ_(%S8~&z zeQRf$k>Kwklu6MLq5t=4E2cIOISS?6S$%FH$!Es3;8z=hbiq$N-hw99idsvxK<06z zUTDh~pf8wQ))S^{_KJyB4}oKM`Oye&KHd#GT9f``$qnp6wDA4cFq1Zm*+X}M9T~8Y zSqAqmH}J=3jsyfOr@`J%@chyFepE}%y>Itp;cSY;|6)X_W4}~!QdF?Rx?E9Y*V|d? zv*A!)kA~pH=65z4?5>3$+5*^NgD9n=Jj20JRmXy?6?x4U088>8t5R_&j_w;$ z?*ancb)zQJ$qz>WGoA0iHk9adB!9A)__vtx^^;nK0}?Oc6b)or z5McdPvtONmU>DBR37&UACn{WlPJsJ6SSl!fP)!F+u>2L(=mX1+Cvf+|Lgs_+N0yx3XL}SY%y{9*_wWCs@!^@C1B(Q+CG0>W+zRsH*K@rN)6DpU~%OG za^B#mGDYZyRoy^#HC;<{-J_bQAfK z=Fw@TJS8ZO3vLF3Kis3RgIELOGp>8( zs<|nA+S_j2BgrxaALVTNNVfMJwoh}@S{Hp^U@pH03;z36O^E88S70S>AIC$TCfcYXOD=<~dB_pk}Bx@qa&&dD3t6tE&x4~n6Ri_7XY&8}G z2dv|q$LjLXh^eq7rVRm}jNQS?TplRwE53=xEq_%S1~_H=G}<=N<4pcWvUMwP51Hp4 zh`^lU@PKJ0tp3IMKL1!d6koZ8H zKVVAQULE5v3rcXGEd0K`4}h=t362Q-?0)&{XGuUbUJm{sXcH_IDFsa2jxV@==@ruz z0MF8-y^X!g^g2JJJ2*Wjx|KfBAGR_tJ#ImOWloH-oFM>rFbWobXU%&;QA>QW_ar#R zKY@Yn0NZ6L%M|Vi-FFVq)N*bG0Q>B7qS;zOD_5;v(b&KBgIN^c1c?0-6%#q>wd8v2 z(_bpU@V>R7Lanxp0Q+lT5BQ_D-k=Ky@@xuCHjT9Ymtm|0&N7bCS6B4o3{m=L%M>uV z(r4PLV}7JN*)dJ9EwJi)7r|Lq(&f0|BIWbXC9n#NOYCOtdtcd((}G>kF)6p{lMe!9 z9FlSD>w}?PD;tyJkfBTPcX7}2ad!Ta^~A%H^^U4Pikk(&y)@1O=e$RS_6<7&?uMiR zo5V>+3#P+Qo;}3e+=779$&LN$E`VaR%Oyr=;EFV~G4(%>ciBDdHSlgk&&0nJPS0!1 zz3q*}Wq}<`1sPj+1PDZ`*|zQ=BdvBsqH5TtmzgeUH!IAg^&QUp_W$evMhc^oxje1f zirriZC4a^~<=<#c8jSkQKQ#@!ebM*oL_{ATnPz&`$E(gKfmnTyl_9`#h?4brAcFjO zsd@KY)2Xdn$jI5nXfg^=jR{ytgHVeY_IGf1yeDq~UkFkMrbFvE)g@a?0r8#}F!v9F za#=tAeTIclhIPQ&h31{Jivfw}8<>eMg(J*B9Gu7g+RX=(AhNNgRI{h(fKUXh;&lT= zW=j{Qrm#;FqqYUn(^m5uSQY@Sxei#vd4MR;nuUuh>U8BxYl*uC1Vq2@a(HuDT?V28 zaX7`6_0J<05tw4k$EUTpEt(lc3z;XncLBB0f`G+QC9q57>Py8y%~m`0fFA5HMGj8L&}0r^k&Br(g9!J^n80I^ z6p%UtrDx`XLLI#mAeWJ*e+R82L3jJ4%IS@?tTo128+YrMTmweji_h> zC07h{W^U{Qu(7AX#sYm8eBu7N?xGhR`xIu&Iu-}ZZ1PrExioLXS9@*~QmGpavV?Na zB<5aytS@KACu#{lLRG)tr9=$=dO;W10G$e?3fqt*{Ce2*9->ZxyWb3$E9>jy(MEl{ zX+6r4^+HdydJO{e8a&q=>+$$)e)a3rU&M#uE0ERQS!$AqEF6;a{k~2_GszGcv$R7%OmOn)#u5Ko`msrr#u_0!6;%P*d%ykjN=ZE&*ZnmJRYMxdNmZNG^YGWTd~i zua;28OXpnHLucLqr3azh39J*6d`gv>9QITy@;+4L-B0)c09U+?Ji!-0il@nTCJxpU z0Zj9uGwFatwCRr8J?(>X6h;U z&5AG11~Qq;Dbfzal^8V41mt>xk{}Ai%FZ`-!t^-~g9`0c-bAWY*NPEJRiJ0l^$O2u zbAuH8?AzILkCQE=^x9bwVY9;7XKe_zlNO1v+igO94R5weK{}q!TSNrRb%aZH*I&(P zuT6LQpvGg(kbZ;={Y&W=WpXA-$;L_jPYPR|?!EL0cK{S%R~~-WUYe-9nD^-!Vu27H zShRFhiq-b&IPUP=&zlHq`mEGSjHkIB{&P+on#`Y5Nv_GtZlRx z8o2Qw_3wt`Mapv<5n~S8uvQLhWS@GA`(d{<*zGtz+n1dYZ6cl)AB4w^MV`l`k5LPG%4krRm1pF*vYJY_!1C zC#LA|f8YwiDYzU~19B+B?p^vlEnFT|-F80|*>-3#0e1B9IbXVRV-PSx4pyBOCqZrL z9wbZ;)KGxnFRuXi5I6p&gj8R}5T-O0&&qC(1Sid!5T>cuB&OoY!nC8Jqyzr|3n1bS zug`_?$fvv0uT6cWed!g|T%ag6&r7afmM)?Ol6l)Ut7onBD$;|jN}+hrO;Nq8IO%4Q z;4Q5s)VplZ{|DwdP!6oJ^tO4ZL zI`@xvh&3q24&^g=_Djs!H9@EDFMNI?rf7p6*{J%1=kezh#Y#>(=oXkyVL(068d}P% zCuZSyy2mJI*2X7Ji#_`BQ?+m<3DJ*gyvb#$}4Nz{V3`c-h3neWB2w>|=r&NjsLX`1HiB1#}F z)ru(t(-u+;?j*6{3dZj$KunI1cc`LAR1;?%)90XbQ5CPej;|o5e!Q6b`XlBlVospyj?(;t)P} zrQ{hKSwnDe^Z;kh`(HUa7L=n0cy~Z8_r9$7!8Iw%R%SI8`6@Vr;43zrau!;|UJq4y zzB~C=Uz&AaV5o3#nY&eempem$FXKl=+h+cwJ&deP0=wJ2Rgo{$yF19-qy+lyTHD^5 z=go>D{_I{?7){Oh=O0crBaAP$EQ-$@@Y4!Hod1QlZ@$NB)ByY`tCEPt_5Egx54IEf za}v(dEt|?U4mO-_R8qW|{pqQ3GHvES0%xv|UY^xVqm7$;$nYyH3-o&w!t0bCf1WUC z)X7;WVb;^D?cxuvBo@*Cf(49Z1ZPkcBb&@no4El}3=X0rYkpG)1rfVA$?b;h3H!4W z{Camz!lsD^YVvBy!5Ml3Z}vVK3N`Y^A6!i=5=Y1$>0kT=O4Zti3!H8?<4gP8YuKuk5{ncemoRKoG-GCd&4PsRI5>V3xorABI*bSY*w|J z{Xm2ZzD&MrK4#XjA#jzT@%T*%GC)p&g)q)Bx==hMyTM!hUJz!(tK5D40_8ViRpcAN zS99Gnr9z1UPXH~i_xG^OE>jn?%ar4{G}M8zelvTc6$_+v7h#M)VZIaTU=Rvof){j|b5Emhb4EjT^1$!UyLgDbXn%mvPdWiAM#7+!dPp5a=luKU z0d}_#GRbak%n|QFlBfKs!DL6=`I$h2RII!L%`~~tDt!f^>G(?L4QfsMjw-{p-;~2| zF{dx7i@@x|&Gqg=-VX=ou5B%!?2p)SNk_PMiXZ%tve~5P_#~{~SPd!2pXkp|F~v2S zY!9w)cq~Z)cv#3K8~v55r)WkY>AM^su_4)@bu*wx&Ic`6GmR!QotzC};|dA+J;C=m zpzgk&<#%wi@m|hC80ZTI{t%j|-BpL5oqE&Pq}VGz#II4--dP(pfB3rG7}ar;d&U8YM!Th=InSOab9nJk{#9SaGKbxN*xR_^j#|s z$sc!U+q`r;Cidi?dyj28vs9&B+|pBESGe?DgxhJd1MdPJDM!lQ=NY|4jt%GOy>@BM zkfr^b}wPCE_^Ojbp1#disOneFsr;h`dKO#*KEQ{x{@Y`1U2rBaNw3tAhbA zeM?~>(vHU{Ke%YNuS;atd(_ukL7QLo0`R6GQ{|wS?n~=YeeLyrzYw%avULE7x(kydDK4r&1DSR2xxe<5V zGO&2dKp6@r#$oy~J33LxKDFuJ=rZ9Yr3}zPCoVi+vM!9@Fx)*$!ZZ;=lPwbi0=EYDucMx6PhYltC)}@nfPhZ?C>~TLdjf<5KU5a@ zpPc96ytx2rVGMd)M++tr@C{VaOPiKcU$9Rt0~N2x!p}|VO$pGBQjhMPXV?`!+g2#k z12C8e$0)H00N7HJaB41PgqX%4bN+|&a3$8|YL~^$I$Z@olI>vLz1~Rw=qeApEXL87 zlooF1=N}gERXN~!K}?K&!GMBbXV&(sO;!QWOQ8A)cBapV?A$`)5s#Wg4+<4;OI|6` zc^Dl>x1_A60A%_rHNx(?rB9lX&mi_NXpq|m?^LIG8h?$28bpBI&vVtat}o);eHIKLJ(=|nO61Mo zStb!1WiZ7H_+lXJ`lj;$aAU(#GzQRRVo3<@W&oD>XI~r?l1kWZK_F2McQJaO@YeEM znF{>fLwcrVliCSS(2Ln-_cB99B6!U6-WcSBea&@^?SxA$SOcsPrlCyRPSIxK)_!0S zDaO$P$1QTL(YZZPcaz_hTHmb3t&jHv!9v4jP>-->y$`zT`9Kcug>1QS9v4@RNBOpT z0p)$`gSX4mb^)^yu-0VdkD;e1&up~4HCI}&PxZzX;8**H6ya}84_eBMhJz7hV3?p= zz-4(=Zyw;DxmRO7OclS_bx%r8R*lSmFoO4gv^Z(OYUzBf|0jGYr0PA30#P?5p=nYfhLhbNg zs%SR)<3-Bbq8c_kPL zKf9>Gjj0oM*_hQ(gugrYoC-am{83Ud;o~8H808sJyZi{Vy;)YuaxX{QWdlg73yc~; zPZbu2-vb)jqlm!edKz%#fBArgeC0u>&1W~n{Mb7i%EN?{4dnqKh}_n_)mR^b@3H3K zSAPRn8wxX*N10GS_}LS=1O@1yRY}VB^M&n>XQHP0FdCI zL|WbZOzY>nzR4vCctt=mb$%u&sbmnl{4U#a2gcwH`B1`9xj6C#Pr{qEZOYyq{JCwY zCl+iPoukGH~b40~)+QNrBX>4voI@2s; z4|4PKe3{r&&0C>Ibq9ZOZ$5t{J>|cuA$8ksG2fXe%t2scRr|>rf%BFC@O!y!^NNkL z{8-*IFk>WSp~r-`&H(}b_8@U7O~e*{O-e|o!W`=64pxE zP{+qu0v$k;r$R@wF8&o|LX&+@m06X-FJY{S<%scCq4_m<(8YY`wQz+tUYKIvC+?k? zpXyUwGdn{%A-QuCF34im`?^dRv=L-rp-ST>C2QDI3DCI;3L`}C(nvELPp#%10T#Ft zvL(#W^vm~gk&vglsVD6!j*>drm6qz{o;0Kei(vBUzKc28oR){~-w2HZ5Fv;XZJ@LI zE_&HKFK9T|BRAlUDG>~OHTjJWup#uJQEp~LI42drz=ivY(+CnYyEfkAq&{9IZXCk` zaJ|3oF91YORPMwGxqYT|vSuKMEW`I(2kn4lc;8&F0HABcg9U2pe1fo)YrX&u<9u=e z8fx5_R8cHzkjo?V%w2FGq2Mxp#~_=}vEga3ET%xC7GY*8*t^l?rsK{EauuPcp zgNF%*57ny(%j^4E!aaTU-^=0S5`7T_rRM1fPrGO=CNJAG*Vhif(#@h8FwO1*Im62r z#=Cy?8=u-RHz?@v18D*ib}vrelh`PgbrzwLlq*aaIJ7 z{&SOEY6EjCoh92Tq`GuNvOO}<9~vF#*mzu(+)#PyHmH#;5<@`eLZ0Oc;og= ztl)mWpHNIRG%vnxLiXM8Wq+$8mNPd)@0a9p zqw=bKeaU|6nL#TTUZ~PA{6JqpFs*Mr^dzP3Xm43Z2Yl&f=$VpY*gT;1UijgsfGc_) z{<021daTAu=P=3)Dve>E2-g?5llKyj>p9~v-a6#PN0HBLat<7zP4u*IJ9JbLNGGaw zl5+J5nn&AtFOd36!`WSep)M!qlT_`?ufH5K)-ZsII44B1=d_9KQL(sWL1AGnU*oO~g65yP0~dBl#Tj1d^2X@YthMIW}f z;WHPexWAeccG1g09sv9AhQnP#SEO?VSpi6Eui;QNM1@a9z=Ho<^2+TgLlBGCK!=1? z)ZjNvwQO(h-^0En;Q+7HtKfwATRVh&)Hf6FcZPb&Gd|^1*78^5z|4>7(sJz1(u5_5 z33@Y2376I%t+E=t^o7Z6#v#&_ax614gq<-(gSg*?+28gEyJyWD;6@e8#)RCj^JI~> z*iyv-)Qjn_j_8leh8jB90du>r0;=MM(Xa8W?XiWV0lv{HbE-*j)ULf5rlCpOPCTDq zL|iLs3DtRdt!O8}%M zM;39x-@c)eCo!ATidQ9OSH;q2{P5yXl1FiB^&cz%W>PqZ)GpS>>o1!L>OAGd`yCe^oM_S9RUaYENa;F1l5Z&sXJEQ~*% zhkZpG-K5a$K?ITaQKoh|HKXTH36;mow7|gK1rn}MMqlXl9&^|(UzTN!IsWIg)YKk} z)6VxKEtY4;Tk^T=+lKj z1Ra_01psr13W4m5KkmBgi+=8j2&W7&Eq|VzM93%@7!(`Nmkz(&9_y)nF-cz6)lqQ@ zOe_wW%zF_9^u_WE1IS(Nu^8=qE$Gd`7uIE&laP@5m%A!p*b#1#Ws@RK z^K|FAe>2qU^mQ8+Dc377J zYGn;`GD~7fsa!0-_03L)SXhV_t{IPmnbri5r+Y=kc)lf|TVT?Y(qyi1t(*uK3otqR zQVRl(^C`Se4j8O`)j)-^PfgV?Ct)u(hIipC@`Y210qT;g z;gr~FggxaKQUO&_uEF?IFkAb|>19RVzxqObU;D|ucyW-Ij;b=}ZB@*rCgrD8$*Q!o zLNb!|^Yi{wV6x)mC;%o?&$G6mlTKUd{otiydtpX||lb`=*s@d?HCZbOHMcQ10K9OfK z3_sZm#+W%XK}Yvq!rzBephHuZ@ZVB~TK`>hnSjos=2%*7!DWBNZMe+Ln!I+{=!P_O8O+rXUXjjwsWPsSur@>AKoyh=VD&I)!n-?)oCF}^9wq#(V|cvS4=mu5gI-f^BLZdhCe1}pXZzWP5V zWY@gmy?-@y_tNc2a^81B?pWgt$0%x$BZxWI&bYw%gQ2c-**`GeBV@Z$SQ~GIcBkDL zhqzI#tHfTmZy0P+OS8gB;9{}69*$FIS0m_R^9dRoDC*WO9w_}YFlG0?RTD)$Tg1Lg z+&w$yY{MqCT!A9onuE@?erQ}^u^-h;5kqNzfO(x@e5x)D^l4g(N0@RDM7p*MI`Bb% zM}<*B={)F-c|+cBHVd2Nf52QnI(7M$&v%c&`wLe0ddJEW#d_*zeA&TU0Nic3#u_$D zbcHIwuG!x0HGaNLf^$+`_qUf1fsFs9QBPsthzIA=_IEn}xWeJne zvgpdu)QC6IJzm6{Y80B9qCb8}J>WPc4!jjY!%LHKElA1tO8R>V^oL4E6^EY`vVue} zJBp~OW^#}=G5Ldip70I$vy|OxWivn+WXb~wNhjBrC%g`WLE+b6cvXx6UZ>#7xf0pF z*DH>$E#?W1-n9?i+4fZDur+NdAkVeMMpTt5;B6!sv5DE`{)%2a!`Jpq?dhlkZE-Yz zIxiRr|FGjoWQ`OVu8DN{( z7foj~#8JiS{3`nEB|LnAp2l&B8|h^w4dY5 zK_^S(gOlD*p4 zkC%Gu5^hnYrnv3l#VNYuACGNGl>PGh?Dj~tcq{r-ByL{lwF-z{ZKvb11!{(yVfM$r zACfr)ySTr1*bC-$e&tNrW$*%uwuj@fENgfiBt;2VMu_rg`Z-6;yd{b<717i#%$boS z$GlnJ|7U}Liq|9YH@hUjnF zF#mbpX+U+tZa3R3bt~xdUYjH%vs>+XsB}VQqI7~&!iN7qI1T53J=RVSyuujmerlet zkdT@PPz=$8Abt2`WHa{lODEah3%UG8_!m3?tG zGiGcPG}sHW>fj;DXUwQC>BCKLAzBlfooML|QJaVMW zu-?tBrV;QMf`IB1_|DeOtjR9?adxFIfhAR(vzN`#PT#n_(44O>o!CXV^b($n_$H5D zb91vv#LBOzT464`G|pWh$!L3a|GJgp`K!^PK4zjn|6r3(bvOqKiQ^;haBtTN)^{IU z;6Gh_K*hza)>b&75m9_yE>1+dY)?$8=ZhyBcUakZv~Xu^oVFecUGDeU0;cnfdijCD ziguA&yejWmxu92dvf_lnRK&}K?Zl@E0^Q*5vfhTjnwtOlYTh)JCPwg~Dh~_`X7B`o z_tVgyT!|t$|>8ooo_m-5`>QsnX zlr=<}OOy)RUHt@=*rK%(io4Y4b_qiN>%Lf2%drpX#Na%irC68c%hs=r)NKVY&b#PZ z(CJK(w}m@>VTQ|=AxM{o;{)6lR|X(;Wm(R>J~Utf%ze6DLqTNiU+R?tI71On9;1%oa=^lpiI`v~)G*dfTcy9?hPjKLxi`r-*qvQeK=G zUi|E8jmBqfJ$t;FKmiBe686VKYzpC->DdPkE2Zky+7I`$xcYxr% z<~jMHNqeFmg*(Vj)kZtqjo9n?C~x%>vKL1gMwk@IuJgbDdh{GeW63aH&6SzhA;JQn;r*nFlejW9R`^zC}9qiTPfA}|@(3-=x;li7W zea#joN@KJL{ZhyHQ_Qhc)8x)~R9TB0E6a3W+;?B9%11QRDMb|1*oXmjC4-vAUmE6j z`;V!WyQdHKyg0=Q^x{cN<)kr=Rm(A8UAELbHuEf#S~%m~95(*>J64~5Z7lHEFgOMt zVt+F53mqEQrJ+aT>63$-VRsUO7L$UfZgrD!hFQn^A{rF>>g^)~?-qSc!t~WH%cq=gHfVgxP9P0# z;fHi;;9T!N4$>*g?cGZ9Lxm{o`tk zJ7Lzz`#%R7!X2T+8_+#H*@rilJ91Zv^6(#yfDBJq|A181U6}et5XA7ssDs@wZ1~_jJtyb)sB>;bP9q*nXm-q2lBpI zO+{M{YmZBA3_L8J8jCUEXa1cUD$RX6_&!vgAw~FHcM;W@vML_tT7v3`FC?AxJqeAYb5Efgt_vT{l%?PI6VkbI{p1o{=)3-8zR{ z{Sd!>XasUzja{&seGgzicKk;4;^dn7PjUM&B-Bntk6Bk7+%_&YXhF;9mHF*HLO z-S6z2sg>@MTx$GNQY_PDT0+gk9THyu@%h2)nID!y*bTU?u!K4>w(9h`h}Y832;czG zlwPRWoX1tzY|vHMQ=gw*d)f+oxeZST=a_>1r%F?MXToJ-cFA79C{=BakEI&(j9i0Z ziWM@3%gkId$}Uh>9=$O0da-TzLx`Pok3lN(TD{vERh?2t+s|8BlF~dZunE7}iq-WV z1sDr(()OrjDf$>RaNT^Vv8h?xrtHf~Q|)r~vrDN!FwVpqaAK+C@Ud;1Hh%Y#{jC;P z;aK^`5kIcQgtn6#8tC_f0S5W^hAa%+W1n;}D`(#t#xBv}@i;Ya((VT9oL)R^PmEoMksT#|GKIom}{4Lfgl zoJCBOT7M+O@LQ_T{%^I$^cRE#lx*jlrmTZVc8Y1b?^bzmeDOG9ZkAQ)bfuNX=yte3O`4hIog7yOQT5p41;F$v9_UMm^w!9*INmE1ABB_&|?Cwn- z4_=Po+X#)`zdV>>!``#SK=6~S*6j6((EHP&(f7RP)<2X^`wP=R{jF=LU-vIc%5Stxp^*KqJKnJ(v(d@-b68 z1$U`w6K1%V7BinL0fZ%a>jGWZa!EY6S*@RBRx|0=U# zyC)E2lP2jU1kt-SeY1Y_~!Xr)o+3$ypi;~Se0aj3pNSV^D9>bR0Yne(HtaA zo2loxziI-zFBwHHP@j!9T&;bdJN`gDzzHg z?9^i*7EwE4(-ULijq?9;GWctUd^`7|4^?#2cx>9T@*whRClE`SS6o=YZERQ{mwdR1 zk&&3wc@4^ASfe$6`(7{k&b1FlSA=r5dq?HWJVp;m!XAx&_TE8&4F2=yje~74b5=3N zUqU$+rMVkn-Nig8aMKQEP)988xiLRiwX4JsbnGk>VJGnl+O5*x-t0UzELSYyu!a}f zge|$oi6O6Nkt^!mFm+D|aEbYSc{=>AQ@na@rdzB=!l0kK^cp}=Y`PC@<{ubcGoB?! z{F5E)0l4H$YCZjy%cHsFZk1FC<2hyUc=8uAI-ls}E)oERcHV$?{=SC+C0`rG0O`sD z0j!VKEo`O1>i#c1UNewFMQoLQ{_PDwz>U>8`z9|;=zD}L?Gu+6J&&z~+Qd`~9uKqa-bEyi_}*GQlhUg0_u-vy*LiwmO&|M#3G`5qDlOKpg6ek zP6<4Iny$6z?%H_#+UrnSonm|yx;B6dG`4K&tRUKAjNcu6_Il}cSeVbAs30m0rf?vrSIy+HJcu$bQ+CsRKBN+!}{ZDH#ZN#Gts^6O6cgi z{u~27T|yol2KDaw_nUHy?&j^ld%OIEa!sF8%+L=~8}ABpDV+vRwG&$x$O@)&xr;z+)eH{og?EPy$^(p_-wzQ)$W%=zRtLEY>b(ZUf-$1e{e;~&~(vaYK zA#Oq+Z(=;UoYX9tDaMwoj#Q41w3h8lCi$?u*!Sil+f<7GDfiR@wRWE~OI>VEQ9Y;k zeT7>uSL11YiT4TGIH&PW}aG~iL zn?@ejv2_tw^X$m14D!GmL^$Z%n}M|Fr%kFO9y8 z?Hz3)r9NvW+uDnx{v45q-v%7qXNLJKOy&okqRf`VU5~bj4EZUrl36s zlCNI}FUm~5CkndHmk&9R_W_VIj%%#!!)?Wt@sh64I3#O>MS)~{ezZTllW{+Y^B2$}V^M1CYK$jz&?1e-$)O;&D=VxfAApTh(|A`H{{HFJd-zAqo zvA7?}5St4pZ{uIfx5tQks0_-re`p_x88^ZQ>bqNQ7N4)hy%ti_G1+XcW@ba>{JfKmfUbm(pDP!#CgH;#(YYIo^<|d#JcnA2}zCa?)B9;L4|g zA=&z-X}m1JBP3y$B-w5!cf{zFl7Rch*XReL{Fk;r*d&Ws)Cf9@qIAjWLT)%f>~9AJ z%n7qD+PQ9X5|*Q%%en|E?bR=>3yzS|;^v1<0=Qu*6b$}emwI#JXZezkCnNFC51D7#?oXJo*UPb=#8G7@bG5YqxcN@ zuxApm(?>ZX&WFH=CFHn*!qS5DVqH!_MqYa2lZmru%}$z8RFDIRC_yaADJX`?PgUR> zg#coD<{#*l!APu+Jjq86@{v_O@+cp9w2wT-M-G9^)?F?z%{uHkaMjmA{#Q=}< zkw*%Z6{pWouvjBwJ_JTyok9h9WXy-a2tzT)!wD%5^&$C6B8{3bkw~Hbj}Ye{ob+@^ zG;xkONgbOf5&AYq>5gbZcYwb+fzUi`@-pU^!E?tE=?MYC9w^>dP|1-vmPr0LA$>V6 znn>fng4FIGMd)uK^v8@RQfxoWUyUTv*kO)J(of~Y8F%!??%+{a@!iXRijfm(fds@H z5kY7bY_~z9>XlP^Vicht$f=}%L{8{iKsJUGDagQ4p+A_ApnpK)7f7hTf(qAg;lcfg zGN242bTyDd_`Hcd49fzEBpZh2_s|r9KMRWw5yXVYoZ^JyWvufX1QC!8qfIzP*E}*vY;OloPwM_!gzNO8jwzz^^#!G+|$< zaGD-MXu5)mc-xhjt^q#gf*+MGhalQo04vbA0HndL62ZTMx(Y!}h(!1MQ+fa%0cId? zKye4W0+xHg&tZNX{3Q6Jfs{@LKZW@R;OD^i!0r%uJ?0m|cYx=>P6N(f`XcyI@I$a; zd_U%&gKq{egWb#ETQL6&{2+Ke>=yL z4=e3O0o({foFgn=1PYhc(`r;vNag+lKU9&j^w^eg7$EDVfiaPUY9UzWp!DWgV44$! zwLgW*FbxG7KM1K=hN6u%0jxtA*Fj^BLt#ISGIUq^-?1BM8A?AFiq~OM0siYr#JTp% z``i7{D*WHJe61S?8&|3j6=QaN9rRzM_bX7t{+^q-EuCrS`n6v=s~0JRc9OEjcZ( zeEY8bbq(BUOGDlMUE9m^(vo>Tj+psbi?kKH8=9`Pw%zXH^yaSHZLL?D8g^G`7tP`) zlOcXqYT5R>rmLO3{eweh3uhg&n1=@YdpoZ-)om|JoyAW=jFjx^qZiw|ADAq5Ay706 zc8lpjcl*Vo)!8Zhcw*tznyqy$oqa~DAQwlub7ds{8->A>-KzfquXe6 zib|KCq;y$Cr_I=X<(F=%ztm;4Iz`SEB5^LO=(HNUF4b>Z%7+tM{Hn?m zH~P#DfDr&~qQl&G<3!~ubX8mYOVuYj1}vf|cSTEbmrQh620BhuzZ4%#?ELaACp!!_ zK!K7!4Fqf#ZHA7MTbA>qfiA5)(P6LyRZ1aL=|=5_juVwj`5^2_{kj`<0gV7EgD*p0 zSM!0yre1gW`aP@Y!darJA3%p_y?6caI&}cC@o6>9Jr*}M-iuj!nrqT{B`|gQsXHd8 z2)~KIY%bAhx^t>r&HE8sOy<7p5A5($NIX@zW7;2F-C$|0gbwCF$Cq~;0X*X(P3Ah39G z?s!JwLF>LX0Mw<8U2Gvvn&NgD5-nYgrE0=2*>Rx{yW^y(&>LN%z3;+~B|M*gu-%9_ z9<|J65{&H!)A`2=8@tTzD9l>sGK=P}#=^(co6dJ5I44bm*5nc`-RC!{)tj3=fxSw5 zU-M@5l6O8Ebcbb1nQK@W{Op}2&%Afd;ACrgl}^L8_nt{Vc+-THs_Bt(l{>9Z7@7mlhp3HEFw!0Ur7xVaYIj8J+C%ZgP zKFM5yXzDy(Hitt#J-_o(pT{f4{Vee6yR`FpUP0{YqGKIKB(^6(k%dlU$FU+c@|s^# zecoLU5A)j6bG~{BtB1JUL)Qj8IX`hE=LfDG%8m0!1um#;yoLIFSY)gMZ#7mf-~)gz z-**0MR!JzOD0i9JQA=N+-?p4pZ(K%Av);^5r4;6^UZ+{#T$2$unphLE7>bf{1xTZ4 z0J6p|3JlFk7{jWj=>2B>5NizM(G+Z=eMsN@evz6FW?ey7(|iXl1z?oB-Pmo0JIysZ zH6O~*teUfJ_l5;DsxWsK6Nc}#ovq1Iql;OB5?{XsF$&Sscs9;Mc^eexanQw*L5;b1mG};g)k}j@Q># xmAru8n#XWINpn**`DGPV+zwk+MOnTkb#4;>BO>QrN$R=WBG+7XlAMnw{{)#G%5MMw literal 0 HcmV?d00001 diff --git a/images/sourceImages/noun_signal_1771815.png b/images/sourceImages/noun_signal_1771815.png new file mode 100644 index 0000000000000000000000000000000000000000..c007970aeb4d5ee19a954d0d4690dc1dfc11021d GIT binary patch literal 16934 zcmdVCXHb+)*9JI&AW1|-5Rf>4WRM&L6Jdxtl9Tg@fMk^5kY@}CN*r=l7*H}ONQUtN zL5YF{$s-aZM`6g@` z?oAW~0&AdrpQ52WsdKvleo%Q|N1B`h|AJ244+o#=ymT$SArJ;u$~P2}mcapm@IjC_ zubE&nmM0!(*g0hGZuAmZ&z_>Ay8c_2R_m(jlS_u**jR4feyA0+{JV~xyj*%oynIN@ z%Qn&UGg#q0ONlH=2+ z#5c~LLJB#6z75QvJJ3m*1~v=9j~Ul3_3GAkm59B_UAR6TAxbL@c>z1ow;C<#^+7y_ zofudpUdJPl7nYzqkdUiZrQ&re65dI$VHhueiHZagPmmg=BGa$SY($~iKAfsKg;87J zaYo`1zQhM4G>LPXOXGt?46OZUa6jlp(mKkAc_Ft|>9Heu8#WJ7`*rgH=pxJw8pMow zPIl~(Pp-%2>ax-aA@L?}X)z27xKn+G>CT$Q&}{xIrIvU^LH|D21Xs9czrp;L`hv!k zX)FR+n8HKqCjMzva>zeOWy*jUXaTvI0Bj>}Dctj4gPl}{jL4`m1X8TP7B|H*<&#S8 zqB*AHh(RDxbj8evk!0b{8EIzti=U4iLBrR=(38^=d`%UX_jDi^3KYQpTLya0vw zG2um>dZ77&Nc=gpElFUSOW@9N7c5#}f>#QOAMnk^#>DdooS8V4>N2Z})(2hHJ@6*c zOmp$xnb?M9w-hx3ty;99*+L3jeM~X3!9)dO08q=9XhocDRZ?za?9wM5tRvll7!&O@ zJ)`i+o{9_`u&Zz}RfcwupuIt^Km89{B*<@R?#iwSo@h+=x~Q5hVK1+odB`NKF0S154R-=i0Ugg zpnR91hvzM%>&}S&$Rautg^1H6D})0P>55Wl;8Nz|V7CoV5LAM%@k#z8T+o_PDV%7{ z>?PqWM3Zjo>|YP5hFNQ|3l!8h{41U~%t7)Yo!n#>m>stv-6FmGeI4QZVs~9{R{jE+ z%?snhkAg?ap zHeg3IAwB9JZN%#l!_38E+;K&?3w_UZsjFGbWxZs}Al#A#j5TsheVP zuXOeICBvm|q5|D8dDCOBe1u$PNved)`xn>!`t?rZ={rDAAK|)1vIam(tfC%!glneE zL_q^{U99v5_OAU0ao;gN;vZri7|JCIXk@TP8*(}_O6q1|csJIyVj+?KE9fLE2IBN{ ztt_D`3hRnZrN~BuIe*@aA;#pqiWOVE)_=e6?bD^$0X{Cf0_90~#;fLPf5qehs&3dE z^Oq;1?Lq|W+igs7i8ux90x=B}>X$Xa+(3sBt~-Mi+-enNYxzW^CYPr?2!$Ur9#Uh% z2gY~iFOX(WAKP9bB2ZguncH7}8)6NSyYU|M#gm_!jl1h(eDZc{Nven_V6TqmY8d-8 zRh2{|{Wx^o(8X)5M98xfWhkW3WDQY^%{sHAiZ-}^o6TE#L2qpf=+BwORF>fBNh1Yq z(_u$F3Wa;XJc?(HUMp4R3>iY~n3r{3u#agJ8B5+Bu0nT&Lr?g?iQM(0wS(>(v~STn zGyRH*Esae&;yvMl@(MQIkx3?RNZW_R#Ox#?3PjcJ+4nD~!!oo3?wKWuJdL8>fgET0 zjA?I&h~6oMo4Ev;>*7spt>Yc{bg~;dV-K*Q(2s;e^M}?|{9Y0Z8?}80MU1s)*4`u` zoSzRtvk=Dw+ZlD*Ly__;inZON=g8*$R|@(LD!!<|W3C|aVe-^u)(w#*0Y?qzsOh7P zMXR@NOhH^JhKI+J_nJlzWA*VS9>0(=5>XmRcF7o6wuoeJ-l_$i^4XeH@{w&N_xFD> zKtwMWn|(I)krb+wW)jCd{bh6sdjgwd31vJdwC3(i5JHNjma&GSq^_TbOwoRHc260+ z7LV(N8ncAj%v^beJ+k6*MB+Pses9f=-OAb{D&eZExb1y=F{Bpa(^g&){~n2kY8$-C z?KN1^+0e^9u5<7y$dH>aHZir2_)ksch}YiK&VS4l&@FEe)G5*wKPMOBpKGOuA ze!UAC-|p>kPxidl!<(3h|9oT&|8Po~%_2AIp{YhBY?A3Pk^D(vnXDSZj-ykc)K;Bm@|NmJj*&p1Q@xkJ%^g^gs7d9L4W zNv-^p{HvOEq^ZST5Q+Mk=)5_4u_nKCbjyt7JRLJ@=B>F<;0=Z{x*%L8No~{?=sS4f z4$eDVv()-4T_*2outIOX#1Cgdb{5Iux?VD$15xQ~Ub=Ys$dU-n+tv7Nc#-cv>Vyzi zs5wh$u@YXbUY-VzNNkI0%`WFMlwNRlo-BGN5h*O`S^bP`AF8!yQ%!RfPk4W+W2Q<3 zdl5+Yzg(tPzj3%myk$vW{Jlyt8Bu_EDdWH5f(`(pE<_&^{d6ch&D^Mz#|H#5Z}Z}` z@p#MZ5;0}fy>6iVHj(}7u;gjpg~jt=OA7dBH0)gVvQSd8a_O_iTDG14Oy9qX&P%$YS%lp;Se&v+1tUCt4zSiZh5CjPlt?kXjluX@M__$(1ek|D zk8s%$eaep&rbN{T@?P{KC#4NAgAVio2=AF+zPe5!6qr#g7oie}`l;k>#}6P5CmOfT zR0$D9-v3Z8==4L>>zr+t%%ud&g!e!>daqlG=|T?o>`B+^4g%#EIFbix0Qk_>!5e;1 zW5~|Pf0TB5TzfaRQ6l1M{g0xi+v?uU72A^4mB51l{};(pVh~|tw)lik*HeVfX&ftY}Mrn zn5d?PmQzTK-92ZGzQ%tgG3)k(BI#1(LI4f!NRTCkK`OH!FY%CKM|tmye+-Nx@%H z{M;~!oQWrxT_)bagph?#9EHoSJP~m$u%xY+9^)!~C2oi(EGkFErB8_9m~!b8=vOBgn*&;O z;7h>Bm$lOx6uEv}E>7+ILH|JC&^)v!RK zC9`v;9TJ~Vas1VF9+LaS6<`c}-LyEd=C=U&-~Z-IwN#`If)uMU#YJI#v8h!tOAtBi zSK>|d>}Y!g(E`n~?qVB&Qz3f8HyPK_l8u(`v4}(vaS;lRjOB*5A4?rF8IM=L6 zgVsjB?)mF7wB;!qXwdl|4Igd*%t#Pe4;{HgQKIYEMRhRK+3y|1IHCdpiSA>1aDSD5qOQWqT2KshWMjcjrg0uA`>ieK znaX^~TyEQ$D-NE^s6jM^O8mJdEY9;Q!F6Jj?tkm#2^St%G)PNhk)MjVRHfBmh@E`AV*EP4Fct^g^z}>jqf70FUYb{*s+e*MQH%mx?iuf${~n(_zg*O<(0Yop{zS zptslSjX-bWmtSt*EZ_T^HjG2vG`lns5XgQRMYxARQ>t8)gY0@Xk?_VZA9R^?fhmYb zB#%vj8D7skS_B%y(6&K(Ru|q=DN|<;2_x~98P!nfvs1;XJ+mHYanMA}gL~b5I$iF2 zrUaD+!!{q-o7cFY)vQwqsjKtD#B&sp`=RL58&N4=rI?{*IzUa>ajJZUXdP5;#0lyp z4@MDE;89K|=XbWw8~n`%^zPGmF>FL7eKlT%3~7TFR3E9c5KW&h?{&sw?oyJGnnql5 zxM#6J$kt^q8T^1yZZ1QtiOU*I2%SNs)XiGO)5?s$sXb1WQj(c}+9CQ8%>fTyCh|h< zLB`Wj0S-a9XNJK;ZoIlzXj8Qkk(5j@~rXFJ+Af)h) za+;oRvXBn?v~*W< z+$bM0-N<;+T}t4OjF7sDlA6%78lQTMvyx-SlqMx(?NEY4)}0T~*Cl`9A_(nsrF6wO zc0ns&Z7|s8*Z5Sz&%@}-f%veUQww%|2RK?6US45^T82?_ZLV%S)iAYUk;5IdH(I~H zagcM_16>b%(rmmrMu=(N#tn1OEiBpX$F;Dy>rQ-SyI-G#=Se?M_nOC+MZrH(`xQE{ zqjRMOZ_C?FB+5&at?P02nDpmb7h$v>vUtgP33*X_(H5|D=Q81ru)(-SoT+^$T?JO& zawQLBHb~UQVVaM$C)*~y7u1VGrk3?7JDPD_m=s75Z|$>BGwoSuC4t3v2~gVTHc;9U zTh~X$*FSpECLygIpQY|c<9yb3+v2fYpx=Wo&>3~fO_lV#16*&s+0A!Pki`BN)r%Gc_5 zc2PCabqR*>R;6hXtulEyz`47Ui*)VOR#NJAJnsc_Hne8)R3P0&b{1=`eQ?}bl8OXQ zqwwALa8D1;ZgtYl!di86wS0(3Txqz@T8sA9$I{4WK!A`L;pw5>0V7$PI0;x)cR`NR zed@1nYuMh-5@UXf3jnU#{zCNB%s2WEWc0wOx zV>DMkwqQjfriTAm(l_L%b4eoch0fm7T$vzy*NFCXc5*nouCVEy-l8xuVFR>xc+5Jr zduS_^df5#FDHxj<`MQ~aE0aLJKU&ezsI>7;}8@ z0Ng@)yI*T*}K38tV)b}-V4BmcGY<~IC_(=jcOY4{Dm&C`!J37N;#MIbcXW#`sq4Ekbv-2tk0clKiNl-URM|VQl$-4c z0TN2+ZGRBl8!jW6WAU65o`KwI?|cre(!(Q;CXD2V8#0(9nF~z(AJTVUl=FNgdIyhi zPSq9>3g+ctGs)eY)tC3{Qh(0O2X>swI-9nKjS*duk-x9HAg_|hb5jvuQ73P+@k(xX z^R!2+*&N3;mh^7i=4l|JY_8t_9X6XAr&Vx95V#0&G-Kr#3kI*(tXr9)MjtGi5S&D|?jeQbRY8bdeBz5M!bbExKi3sN6eBwP-C)ibdmL9pvz65h_rUf$aHK@# zPPBtsEVdh-?QQ}6X8V_LTCGZv4qs*sqZ`3u)VaxfiEKpOOMQg76L=MNNqT~Omw2qMQx9tBW8^$6!<_s6P6Yy`^h(9 z1WueI&{*KS+YrAMi8*(!kpuMk^Y<=fbB^TstPRaekPjS z&n7x5i5J0jbSQ%BKrkl_qE2VagFw%>|*QjM`ley1%*Bs&XnWqd~af7TBO{eQG%j z?`q(H5Yy5{BS=wwS|&EkUKTzq_<>6ReQ0fIqiuXxrYHKS|3_%GkNOn$B5r}w zOz>~BQt(8#T49;p_8CIq-!OAj+%{xPi~UXWm^fG?muG;J7qH|_;kJEt>=6T4*a8;C z2K>wWMZaN{8HTzsTuB5QkR_NPxU|_{U;79!2wwXhKt#HV;Zj$hHYkEVI3xb{Ay)@s zdjAmERQ@bQxAy@)P|o|gphsuK-yZcSpI83-xmAaLKE`}PXco|A1c8d5-Pz7y8vVKy zrN82T!{on^|0_E)Ncn7`o~+xUav~yBpnLu%`)mF`TI_sC@xRJAY60c!K7-lyjI8U4 zo#mQ5T!{Lws#!qmr(Mzi{mk=+EtbY#RRUAleDL3?Y|6`s z)SZ%SA2UGwN81JXYP8h#&RXgJX_8Z`QU{To@BQx#x()lNf3MBKnJK$PumTuRei-^H z_thCl^{=K_69~m3jy!g=-LI@EPEGp<&^qT;4#OFUxel0B)NhKRt;+c))i_JoEto9M z0EZpbDh^uxX6Zl5ed#T!SdXWtEa&YbeP|$4|;V`MwE@* z7pwxhR=H?#4(uHyYS8BDVPdb?eIA*TPr(AQ(~RsNRo|-KfcC~d_E(|gzuPP zL5r2&@!=QpMtB*6J&r)?2}5>SLrp6sYRhYXCIc1DE7F`lGbM5UiZsMkL1hPsbbPr6 zyoFXTzhO)r@D`Wk9(hM3A{c>~2+x_wThqkDYG^?hg4$`=9UiITBPmF1}urHq2LuiLFY>{+-uJ#Q)Pn z_QNv_gW^+@B_KQQt~V({`t7V!Zrbx&=wv@y6zF8nmv1mwU<^)lTw#6#6x;L=Bxy`@ zeUtDOmC0j$3W7c1UJ4}W6p(O!{Y<>$}H{G_fa>3j`em+n^pX z;;Qt9p^M0-nAh0_v5%m)?Z;dYO~Iyr+(nu{ML2_ooS$5Ach62WD8{EFtDQg~M>36_ zBTKE*Z!oQR)pGwx-}YhxofQZbL3n9A_?kYgz>jHySUqjQ(xcdm;oNW=I>1Z`?^=>{ zh#|z3QqMdDTZU&K66It-CCPhq!(EX!$K}47Y@H#nt2TF7A)uO#;A98o1&L zv5qwh`Tf4K(YTnd=+p6VEg*C-knpA zM)HK!1i^XBj_+l7~G2D397lc7Z=9Mx1rgm zi}gQ$3*~Adj-R&#L;|X@`m#?;e|BiJxS`4rJfrJnOs3(#g%pOxn?>BiY9A{dX8Km0 zlPM@BE+$=Ci|rCfAw1z|Q9&iCU?N>jz*98$`W;R+$JKUwkm*yaMYM4y9|tjUrHG|d z+30z1Th!-3pf9u)PDiq18buhD6){l)jJRiAqj(RTNb2{-6xE(0hZJbYZyE zZ9A5Sr{dX8O#I2$8b7cDn)IDGCpFMg>mS`Z&67(QGxX=9U-M22#MuIB;eigt81$BP8^@fI_c2JF!Zs7vMbm(Jz_rr}hAVt9= zOn(@E$*)-GwXCu8wEF|mi93FXAKdUV6}AS%jF>A}Q#YE|%{Tt{Tqt&Y06_C0R8aU2K;w!wQ@r^e;eo)bhu3|HFF z2|qXq2RZC#o0++~L;V~OU`x(6ydbybCw4)b7>g(O_(c;_3A2-PEX}=X-5&^pH^4R@ zeROdn%1|wpIYZA1rQhksltI^^cc|ea;<>P11J>{5NEkUR|Bj_WfW&60$>aCmIY@jY z)by^-+s$9m(Uee>v37Rqn~z{9l?Gk$XziPhsz10gi1As?oy;B^bY2frA|+gXJ-5}= z<9L}@`GwD*w~bSnq*6XqCoU^8Xw%=IzUNe_#sMjxXRFk&x)g6ZaxY}f1iOwzyBs$k znW828HLVcNDyE0oIfS-ln(;HXH>1^|ar$wj%gYY~@M8F~G9Rm4sbHCgLBM)A zLK2i!^m~QCxLJU#B){#S#?Y%-d{+p$)t|G+;0DkkDn**V6wIs9d4cW_9iq4_5ejB z=-2=AeXvxt%8kcX_c=g$_ATPh5Z?An&X;!MOXXJqh8TFp3!93Rx{f-==szX=jHC&H zHVA@yaj1Fh5X$ZxV51HJR;d&ahnnL(9>#7i@}M7+D0IziJV0r2211g?AGLh=`_E)i z`@wi(!2NJY)Js51t#-q9+9R%ZWbYBiINbb%N@9Vywz`o^fD^bB9+aRtEqDcT6L0nf z0G3>E>-p&b!B)l$ZV3^9Vf%~*c=Ezy=mAt(hdoE{fXd5`68#ItcOZ8Bw$5_Go!pd) z2<;t8?1!W-&bZdpfo;<9;~0d+s94d^5NvlT0!ja#+1dZy@31ckN}8=IA|SK)!9%o} z<`TTq6vUiv3t}{@8U*mGe5so=@(oeIBg}RU5p){0mGtt-dx`*0Aw=IQLU}+~z5Ka* z7@#DO3MsLYuWUrci`qrEI|5pftrg@ktxC7@IV>52&H&&2#)7{}qw}*yMQiYQObVUsM{US|yMYajrQ+ zl3Az9Q-{1o%iD+-DO9^d05%t^(yHW{ztKglVKWvAT)zu(l=!?=>5=2iA&tf3-xDCb z5ZD%=r>V09n+oKf%F&Yf>;{3AT*l-{FNmn=boKa6ee~Q*KR{XF({=+$s?+I>V)J$+ zNx>D+7Ex77tIUXrB+r%U`O}dZW&CDrfgr)if{x6N5?$|XcaByxqsTz?3ef87s#uU$tAPhUu3o;zw~iHgKkqDNJX z+zoNPB*7+iVTV^`~gX)txGzufv)=z&82yD!NP zXT4B=VK+)C5_+7xjhUCE+*TFfPlMD+3=sPUs1IYbL28wpV4FQKMe}<-0z@`4INqV# zgLhp(2H+a-Vvg6+PvH2Tmn3S0yM43UBXp9Tf@Ms-g*vmtH7{C6n{e_v{&uWpu8}GS zRYqbCl4x1*CK+G4ujexHiSc-XlS2f|G682Sw;h3@^RhtOqT()|+I~OnIQ-QW(0ViH z!!$26({rCQPczZ=6f%TeEEw_K1bgnnkMKC-FAP|C4!G)@gK&EKWi|*SQ8dTWQJJxU zxX@FDx5Zu_IXC@B+SZRbDm#H4`l2mCbapbNa%frAAKVv81xR*-$8epS=fr1!G!d*r zt=#|9PjZHVtH_*C&HdDYHOQ!^-+@S3R4)b2=#|i7o(&oI=;LA>?Wz)_*Mv&$_j`O8 zYw7Xl}##~*7y0?SJFFo z!2?jqc?z7E=$FTBm6mIa07A(_+-FYiF7I^4i*hFHbX@1BVLYN8krPRNrbs`ylo zth7*UM;7XO%IwWg6>Jfc9W8Bx!=BS88!;+8b^>;@lnV~t?xu!#zwg1zO zNYk2K`Jz*u6l^>8$oYXBe z=HG_@WZXQAs7oBDck6q;ajOd41@RqYR&P?K-!H=>e0+1lzv8d6d`_bMyn@spW4eAe3MZ&V=4ubqs+P z4_EY|Zb#8J8Egythx5q?3DUUbF4J%m#Vy@SW?TS9YU8MWn|QB6z$xZP*jf? zGlbUn{)`Eg8dT|Q=(yJ0s2?pTcgYD9*$|ltw_#qM#BJQsC}BQ4XQ6jkk@SLM2@_`r z%LmPaHn4`$mD?tE22dmZk^z=rw^>oju%<)z-5B?E&7r+;MGsG9wg+PCYbHxO8IFI) zbqL0iSs!L?QaID^el(`3R!@Dj9s`RQXKm4!|MSXT0*QKKt4%5Av4kDIl>dp`s}x>m zA~{UQBsVz8dZArgfJ8;qr|M9yuf7D+ivggF$oGdLkr}(!%FCto^@TMjs0Y=%cej6x z+$S<4FvElTB~y?WXQrkNe-WEB{8Hoe%Oq(fO#qD(djlUXdUwoRI zS>5O`MEYLOgOD$vHl)lxTvLTz_A!(fZsr{FMIo|+IEKrHGek_aYjq0|#wa9dW_-9N zl3k$;;sp0>$;-B?5?{YdnEjBm>(uAXRsP7k1yo-6W?Vb`TK5~oN4j<9ABWNRt^2R! zhYE!>CkllMNe+T+ButjGCYBuUXL*@jnH|GkZpC`=rr1oN1hX-Px$B{{DY2dwwi`w; zsA-K!FlNPyW3S{eh^FQd0z=BEN?{&U0XM`u+VY9%y48WphAshv{?f3(I5XC`1VK>F z7*axicp761NSKe18lD=?n%t!1>*6j`%IH9Jbsc#pV7Kw+etWAe;Bq;;Kyh64AhP?1 zA|RV?@~)rhOq){EW$J49l4FLxT+UYRD_KVChiJUC%J^Uo;7F!!N=2Ol)>Stu z>YloOTKG14#Xfk>Z*MJ&rWoE;96X-lz_c1*dt9&Zo|*_HJhn&7CV<*O#_veuno)t8 z(@Y;@eb(j}ZrtZD=dh4(ZQme>v@vyc5JG1FD7Gub-&G07%Ul4=n(9h};@M9!czVDcik156#ycniAY6#0CaUQ&KZ`Sw z0cNZyf!ffxf!ke^dt16O_Dxa%=9X;m*cJRZ)=v8}$N3udxQk{ovb)se0g&G4rrx2I z0(Gxqsq?r->=>omF~1LRiHqT%iPQw7TNzWSmKflS*0RRA07l;d9OrCBf_z{Q;LAXu zjk}_2b-eZlnAbOqQWVI{78ScTF8s8{F>KA6pth{8rnn|auuWnh5x8JUmueK zORsV{d;1+ac{^UOC-8vag2|F!I&`8u-{5w>_ZG|wYGf5(XfL}i-mj(>NHZ*;Rmbpy zmaGrB!1ut)roC0fyEr(uq9RW>RV}^pY|AUk-2h>AENOtO-5aeW{HlRI=qxgE1{9@^ z=0lvIp1{J3IZn76I4+RxoNKY>B)C|g%3$7x1^&Egme|`(5P5r~0*s75=jqws_2+kW zR@8z?6Kx~~R`(c?w5Pe)K^8v&e^`Qdy4Q*{HxIO9xUQn5hbhl=& zNh8y=r)ZEING*UH^0i=ISLxqx1Xa-m9#XW5;NGI()kjFFVAF=|Up zh~A)-y-gb=LItj7uxMbbCUA-z=)Cw9>wp<E@v3)~(a5OeW`- zy{u(d=CSa677X$jf}$Zb8Wy6ThG3gGm88-Oe>?q@QbP4_7eoU-t{voUk<>-oLwg$5 zCf_8E5Djfu(?g9k&?ui%hWwyTuX0qUvw_(d)IDnA`^b%4WCWKK4j;wJnae{Z-*gRZ%kC!a!%68ysraZs}2u+LkMsB$&!Pn^1llf-qqq@J@hf$wA6k>;Lj zCBY4}WO(@{fPXuvff}46254{8R=rBO^NP1CtSG+wM1LM`JiK{sooa#3>Bw-k^TGfh z^7(y$`&TZ&i?6waEXiHqxZ7F7f6Ta$^mMP~zSKHnL2WQ+O8nO$u(tn8#Q%RQHru{W zj3BxbxzVyR3^i(ZR6gmL)8R zorC?3qyDPBexKtA?)?1+TT^m~cv9@;=#MA6v3GD& z!*pE^`pw^&gC<>9>Lomf>XcZ?47|O3mduA-Z zezXKRL3KX#%`Wql4^CQGZj}vY4;!`A zvzNpafFeI6=gj#e1v=NN4e$NXHu0($CFmN=k48~gK|6V<>Ek(NIN1MeWQldR6;}q_ z@ZiySx2`#PzwB~I^-f(Tt_7#Ic)Th{J5YH;GG-{a_q6ZAu#5X09Q$I(PB*dMwd?@0 zE|TOqWaW%G@D^I&S+Ye|1D5%_gKycG)72~RmD{_2wm9GA0-ao1##U2C*Fo9HwYgZ% zXY*r+Dt`>W{9&~F{PUvk-IMJzWm9KT%n$Yze6vs7CYFqNrhQ$9f{$Wa1{C|p=1Ycl z%6!Y}(>}3sic|zW9dc<2WLfwa7VsS>ve@KQHaf2ZjTsIOX@QohHLkiIDBaR2(kvSc ztDcap`UyygrxN@Mr$XJg<_DA>)H7}TVQ{^hu4M&tzvLuSQ(?t~F_erC!*uslPTEvi zkCpglk3CEoZY;v>hoLmHteLm2@D?0oeP7ZDy*Dx6Y_Yr7CsA*8B;Vq>bhYHL!E6^< zZ|?$5AD(7mn{Bd|v5AwelQUP_n~WXajpYe=cxqwt=USXu!n?Z)IY6bsQO7}d?_L?r zzySCkro0mc=~7bQP{Bdd(vn6q%fe8kM*rZqkig;Kli{>?d5PJ>iyF?44csw8$bP== z$riN>%6Um;L%~faPU^~COWFaiKNGo418tc*r*y2dYl)x#PQv=6@pFs29HDw<%o)RV zG!n|WUgf&f^wDs|d#dbU+{|#GzoXaHH@i|TA9yo_@6{0=^@r5ucUF2|#0ozz`4Ua6 zsM6GAIGlpsy?eQ9X)aC}apgup_-9?7(%?h&b>1q?ljqw*M=Jzt8OLD_O&h_ncV@2X z@M62b6|XSsj-uP&&DN{-n;%bN%Z@mHeAt=ajcr@atU768AbZFA`s~j$Jyz2?Zx8OU z#2l|AN$&ljx-lhUy%v;YtEE=Gx+Q39d0@M;^y)%W#2D7cc(sQ}9-ZeYYFu>)3*pli zG{2=&_wSgC`=8DfceSo1je)DdHwv0_)W>@@idO?eWJs>!@uk?^IL_^(rKZR4BK!rN zcjo`_-Z4JCQPoE{QW>CFwDDiCWML+nq6>QcX_Yq$vJq9MFzyq>+Ai`Mw!2rRE=(`Q zBcn`s7ec0w#TSmEzJFj0HeFw*IbLlZ2<{~g3XT@6#DhKQ7QUa4=dtPYXbBuV@m#aB zU%u1{i9V@jFSn*Pk&@+zZxiN}7d|VMR}>L}pptiK7`!y<5}w0RDjV0v>>kA_eI_S@ z=1tI@*Rp!&=sv%@-GvhUICQDxiF=gjt=H`O&Yd1(e_G^3=C6E^^X^IgP(Sasd!jG= zR&8yzVa+E^Lv73M!2~*|FJB~HeNFnEVCI9}>Sgap%o8ger9XK`J!Q+~Plk^30vhva zz(N;%3nIwf`vb=B`-DGs_XJ-!5C@4L5i6sF@uZnU z?P-PmJfdl9Yt;rKShK>;b#e~TYjw{;JFU!P?EH7mpd*~cHMELs&#tponWgm7x`)7% zgV|PXZs+p))ai&Pg4BQ1ziDv=6nF;Xoy28L?mtWM!lcY0YJ!MkO9~1ka5(ekpi2)t zGJ?T%ug9{&3-JfE*k1yt|m7QVz!XubP!`x?0tbfYUR34qe;V0pGVk8hN%@xvL zu3F&A`}u}yU7ukvJ7kxo<#t;HDM(>M`^`knV%_in7T_f3=slVeV{O_J4G?S#K2j5# zooU{!z3b8V^@kFDt88=(yvDA|DIcvs2Em;$;txXe>yk|OL?6wYzb!-pBO=_TS-Dhf zbZ9hS@%!an@rq?$v@n3SPUY&C=a_T>TdJr}bm= zk;@d{a@LN+z4lKENA6(&(*0U8IPEjE{o&g8ODn%vNZoAyOc?7JRw7d{5T$DH{uvUB zh@d|5Aj;PY>}@4uC5cP z1oLFRSpxv!?Yot(Oc8S}V=42*nM?-bZPnl>0fzY9o&}EqY#PHZs0VzTQ8+xjr>=(O zC%q!Zw=S(HAe;7xdiTagjx6_&m*&ME;PKHVI>gYS^`#hbSY{4g|Nu~HlFT(k0drb(rEQ=%GMuv9;i zSYJAG5Z!-d^Z<<9f}AW7nti)}{LW=6wYA67;m<|br1`C>y;f;@Q};au*&x9w8aVhx z0mnD5-rFm6pEPjIb8^n#?a|PQsL1LJr@PDWfXVSA#2hX-GM|KSXD#{lnrq(pjPXbl zT>}G|Z-Y=B7lP-wb4ZO=S!c0=xA6M*&C6rcnAPJl>IIUkCCC{sHHMimF)YK2W5oK1 z*HZ4CPiJkV2b_uZ0$&flKiT~LFf^gc^Ask0uJNIgm(WAc@tf);?lwg}4iV2QkHLJ& z7DGdcM9uihvG_-(Cr0BgmUx0?&LYxrAF*_sRB5x{fe20*60tv{pA{wSZp@ieG?RF?MNdz_xOcXjn2fMTap28vzzYi zqH06t5BLV6WerT*)Ox%K2EUE38CTo4J}a$r+&soaoU+TN*7$bqI1^br>$+z!Pp8IR+gpwr>tWGWs(}XWM;p7#TQ>J{ zezH=xziO3!#}H2U0CMt0qnBpV3bo4{@sH%Mgp(ssbc2zlSd8z@aAoIlC@Jr6{uZ@k~8dvRC7~ zuNm*#dIjeLmi6n)$EIxSJN@&2kfUbPiHE1wP488`j6WAT@5Y1)tTl7^!~A`8DlprKdm<*N*;WFsb1s+Y5}0-l)np5$?F;c@%KkI1AvEJ z5F$OtFlE!gwb~tpn@f2Iu#ZJ6TrDk$41W$95{4AqzyAtRow8E)yal&=7g|!dcvr93 z;uI~92-$|W_rOSHrlv=)!dKqtL@)`q;jXkwpVo4PDKWyU z&x~E*f}|BkZx&Ez0ylS#bu_AD+Nfx% zx{fEPI7*eZh<6iMNYgX(Z}$fQ6@zeAoe>GM9hsV=q@;J2F3Hllt9cn_j{$Pw|51wi h|5|hTAIHo=eGIJc_hp;GA9{cwwTy0-UPnLqKLFKnO%wnC literal 0 HcmV?d00001 diff --git a/images/sourceImages/noun_signal_1771815.xcf b/images/sourceImages/noun_signal_1771815.xcf new file mode 100644 index 0000000000000000000000000000000000000000..828dff59c51720bf382502edb145635d8a3ce505 GIT binary patch literal 4248 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 Ds)^p* literal 0 HcmV?d00001 diff --git a/ui/dbcmaineditor.ui b/ui/dbcmaineditor.ui index 126452b..4fa6719 100644 --- a/ui/dbcmaineditor.ui +++ b/ui/dbcmaineditor.ui @@ -13,33 +13,95 @@ DBC Editing Window - + - + - + - Nodes + New Node + + + + :/icons/images/node_new.png:/icons/images/node_new.png + + + Qt::ToolButtonTextUnderIcon - + + + New Message + + + + :/icons/images/message_new.png:/icons/images/message_new.png + + + Qt::ToolButtonTextUnderIcon + + + + + + + New Signal + + + + :/icons/images/signal_new.png:/icons/images/signal_new.png + + + Qt::ToolButtonTextUnderIcon + + + + + + + Delete + + + + :/icons/images/skull.png:/icons/images/skull.png + + + Qt::ToolButtonTextUnderIcon + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 1 + + + - - - - Messages - - - - - - @@ -82,6 +144,8 @@ - + + + diff --git a/ui/dbcsignaleditor.ui b/ui/dbcsignaleditor.ui index d2ea6aa..4a1d45c 100644 --- a/ui/dbcsignaleditor.ui +++ b/ui/dbcsignaleditor.ui @@ -6,7 +6,7 @@ 0 0 - 1239 + 967 644 @@ -15,10 +15,7 @@ - - - - + @@ -210,7 +207,7 @@ - Value Table For Selected Signal: + Value Table: @@ -228,7 +225,6 @@ - signalsList txtName rbNotMulti rbMultiplexed From 38ab58d92a2e6690aca13d102027050a8b9e5d73 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 19 Jul 2020 20:30:04 -0400 Subject: [PATCH 43/58] Removed windows build from TravisCI. Travis sucks for windows builds, switching to Appveyor for Windows build. --- .travis.yml | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1419c2a..f165ff1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,6 @@ language: cpp # sure it really builds. The linux version will be saved to the continuous tag and thus available # for testing. All three platforms will get a proper release upon a tagged commit. -env: - global: - - QT_INSTALL_DIR=~/Qt - - QT_VERSION=5.14.2 - matrix: include: - os: linux @@ -69,37 +64,6 @@ matrix: on: tags: true draft: false - - - os: windows - env: - QMAKE_PATH=${QT_INSTALL_DIR}/${QT_VERSION}/msvc2017/bin/qmake.exe - QDEPLOY_PATH=${QT_INSTALL_DIR}/${QT_VERSION}/msvc2017/bin/windeployqt.exe - before_install: - - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" - - ./install-qt.sh -d ${QT_INSTALL_DIR} --version ${QT_VERSION} --toolchain win64_msvc2017_64 qtbase qttools qtconnectivity qtimageformats qtserialport qtserialbus - script: - - cmd.exe //C cd '&&' "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat" '&&' cd "c:/Users/travis/build/collin80/savvycan" '&&' cd '&&' "c:/Users/travis/Qt/5.14.2/msvc2017/bin/qmake" CONFIG+=RELEASE SavvyCAN.pro '&&' nmake - - ls - - cd release - - cmd.exe //C 'C:/Users/travis/Qt/5.14.2/msvc2017/bin/windeployqt.exe' './SavvyCAN.exe' - - cd release - - ls - - rm *.cpp - - rm *.h - - rm *.obj - - mkdir help - - cp -R ../help/*.* ./help - - cp -r ../examples . - - 7z a -tzip SavvyCAN.zip -r * - - deploy: - provider: releases - skip_cleanup: true - api_key: $GITHUB_TOKEN - file: SavvyCAN.zip - on: - tags: true - draft: false branches: except: From c89bc43795b573023f08d1e223f3a8ef27fde267 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 22 Jul 2020 19:15:16 -0400 Subject: [PATCH 44/58] Added more functionality for new DBC editing system. Tweaked graphics of CANDataGrid a bit. --- SavvyCAN.pro | 3 + candatagrid.cpp | 24 ++++- candatagrid.h | 8 +- dbc/dbcmaineditor.cpp | 206 ++++++++++++---------------------------- dbc/dbcmaineditor.h | 13 ++- dbc/dbcmessageeditor.h | 5 +- dbc/dbcnodeeditor.cpp | 130 +++++++++++++++++++++++++ dbc/dbcnodeeditor.h | 41 ++++++++ dbc/dbcsignaleditor.cpp | 13 ++- dbc/dbcsignaleditor.h | 4 + ui/dbcmaineditor.ui | 74 +++++++++++---- ui/dbcmessageeditor.ui | 123 ++++++++++++++++++++++++ ui/dbcnodeeditor.ui | 52 ++++++++++ 13 files changed, 525 insertions(+), 171 deletions(-) create mode 100644 dbc/dbcnodeeditor.cpp create mode 100644 dbc/dbcnodeeditor.h create mode 100644 ui/dbcmessageeditor.ui create mode 100644 ui/dbcnodeeditor.ui diff --git a/SavvyCAN.pro b/SavvyCAN.pro index 33c8cb6..0ad1aad 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -44,6 +44,7 @@ SOURCES += main.cpp\ dbc/dbchandler.cpp \ dbc/dbcloadsavewindow.cpp \ dbc/dbcmaineditor.cpp \ + dbc/dbcnodeeditor.cpp \ dbc/dbcsignaleditor.cpp \ re/discretestatewindow.cpp \ re/filecomparatorwindow.cpp \ @@ -105,6 +106,7 @@ HEADERS += mainwindow.h \ dbc/dbcmaineditor.h \ dbc/dbcsignaleditor.h \ dbc/dbcmessageeditor.h \ + dbc/dbcnodeeditor.h \ re/discretestatewindow.h \ re/filecomparatorwindow.h \ re/flowviewwindow.h \ @@ -139,6 +141,7 @@ FORMS += ui/candatagrid.ui \ ui/dbcloadsavewindow.ui \ ui/dbcmaineditor.ui \ ui/dbcsignaleditor.ui \ + ui/dbcnodeeditor.ui \ ui/discretestatewindow.ui \ ui/filecomparatorwindow.ui \ ui/firmwareuploaderwindow.ui \ diff --git a/candatagrid.cpp b/candatagrid.cpp index 25c4262..f46783f 100644 --- a/candatagrid.cpp +++ b/candatagrid.cpp @@ -14,6 +14,7 @@ CANDataGrid::CANDataGrid(QWidget *parent) : memset(data, 0, 8); memset(refData, 0, 8); memset(usedData, 0, 8); + memset(usedSignalNum, 0, 64); for (int x = 0; x < 8; x++) for (int y = 0; y < 8; y++) textStates[x][y] = GridTextState::NORMAL; @@ -53,7 +54,7 @@ void CANDataGrid::setCellTextState(int x, int y, GridTextState state) this->update(); } -GridTextState CANDataGrid::geCellTextState(int x, int y) +GridTextState CANDataGrid::getCellTextState(int x, int y) { if (x < 0) return GridTextState::NORMAL; if (x > 7) return GridTextState::NORMAL; @@ -62,6 +63,21 @@ GridTextState CANDataGrid::geCellTextState(int x, int y) return textStates[x][y]; } +void CANDataGrid::setUsedSignalNum(int bit, unsigned char signal) +{ + if (bit < 0) return; + if (bit > 63) return; + usedSignalNum[bit] = signal; +} + +unsigned char CANDataGrid::getUsedSignalNum(int bit) +{ + if (bit < 0) return 0; + if (bit > 63) return 0; + return usedSignalNum[bit]; +} + + void CANDataGrid::paintEvent(QPaintEvent *event) { Q_UNUSED(event); @@ -161,7 +177,11 @@ void CANDataGrid::paintEvent(QPaintEvent *event) } else { - if ((usedData[y] & (1 << (7-x))) == (1 << (7-x))) painter.setBrush(grayBrush); + if ((usedData[y] & (1 << (7-x))) == (1 << (7-x))) + { + grayBrush = QBrush(QColor(0xB6, 0xB6, 0xB6), Qt::BDiagPattern); + painter.setBrush(grayBrush); + } else painter.setBrush(whiteBrush); } } diff --git a/candatagrid.h b/candatagrid.h index e3162c8..572e87f 100644 --- a/candatagrid.h +++ b/candatagrid.h @@ -19,6 +19,9 @@ class CANDataGrid; * Then, support was added for modifying the text color of each cell. This can be used for whatever the calling code wants. An example is * FlowView that uses all of the above functionality plus this functionality in order to show which bits are set as triggers for stopping * the flowview playback. + * + * Now the control also tracks which signal is using which bit. Currently this is not used for graphical output. I tried but couldn't make it look + * like I wanted. So, it's un-used as of yet. */ enum GridTextState @@ -41,7 +44,9 @@ public: void setUsed(unsigned char *, bool); void saveImage(QString filename, int width, int height); void setCellTextState(int x, int y, GridTextState state); - GridTextState geCellTextState(int x, int y); + GridTextState getCellTextState(int x, int y); + void setUsedSignalNum(int bit, unsigned char signal); + unsigned char getUsedSignalNum(int bit); protected: void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE; @@ -54,6 +59,7 @@ private: unsigned char refData[8]; unsigned char data[8]; unsigned char usedData[8]; + unsigned char usedSignalNum[64]; //allows a full char per 64 bits we track so we can specify which signal claims this bit GridTextState textStates[8][8]; QPoint upperLeft, gridSize; }; diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index f4a851d..5d1dd96 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -22,10 +22,18 @@ DBCMainEditor::DBCMainEditor( const QVector *frames, QWidget *parent) connect(ui->btnSearch, &QAbstractButton::clicked, this, &DBCMainEditor::handleSearch); connect(ui->lineSearch, &QLineEdit::returnPressed, this, &DBCMainEditor::handleSearch); + connect(ui->btnSearchNext, &QAbstractButton::clicked, this, &DBCMainEditor::handleSearchForward); + connect(ui->btnSearchPrev, &QAbstractButton::clicked, this, &DBCMainEditor::handleSearchBackward); connect(ui->treeDBC, &QTreeWidget::doubleClicked, this, &DBCMainEditor::onTreeDoubleClicked); sigEditor = new DBCSignalEditor(this); msgEditor = new DBCMessageEditor(this); + nodeEditor = new DBCNodeEditor(this); + + //all three might potentially change the data stored and force the tree to be updated + connect(sigEditor, &DBCSignalEditor::updatedTreeInfo, this, &DBCMainEditor::updatedTreeInfo); + connect(msgEditor, &DBCMessageEditor::updatedTreeInfo, this, &DBCMainEditor::updatedTreeInfo); + connect(nodeEditor, &DBCNodeEditor::updatedTreeInfo, this, &DBCMainEditor::updatedTreeInfo); nodeIcon = QIcon(":/icons/images/node.png"); messageIcon = QIcon(":/icons/images/message.png"); @@ -60,6 +68,8 @@ DBCMainEditor::~DBCMainEditor() removeEventFilter(this); delete ui; delete sigEditor; + delete msgEditor; + delete nodeEditor; } bool DBCMainEditor::eventFilter(QObject *obj, QEvent *event) @@ -71,6 +81,12 @@ bool DBCMainEditor::eventFilter(QObject *obj, QEvent *event) case Qt::Key_F1: HelpWindow::getRef()->showHelp("dbc_editor.html"); break; + case Qt::Key_F3: + handleSearchForward(); + break; + case Qt::Key_F4: + handleSearchBackward(); + break; } return true; } else { @@ -160,78 +176,36 @@ void DBCMainEditor::deleteCurrentTreeItem() void DBCMainEditor::handleSearch() { - /* - uint32_t msgId = Utility::ParseStringToNum(ui->lineSearch->text()); - if (ui->radioID->isChecked()) + searchItems = ui->treeDBC->findItems(ui->lineSearch->text(),Qt::MatchContains | Qt::MatchRecursive); + qDebug() << "Search returned " << searchItems.count() << "items."; + if (searchItems.count() > 0) { - DBC_MESSAGE *msg = dbcFile->messageHandler->findMsgByID(msgId); - if (msg) - { - DBC_NODE *node = msg->sender; - if (node) - { - QString nodeName = node->name; - for (int i = 0; i < ui->NodesTable->rowCount(); i++) - { - QTableWidgetItem *item = ui->NodesTable->item(i, 0); - if (item->text() == nodeName) - { - item->setSelected(true); - ui->NodesTable->scrollToItem(item); - onCellClickedNode(i, 0); - for (int j = 0; j < ui->MessagesTable->rowCount(); j++) - { - QTableWidgetItem *msgItem = ui->MessagesTable->item(j, 0); - if (!msgItem) break; - if (Utility::ParseStringToNum(msgItem->text()) == msgId) - { - msgItem->setSelected(true); - ui->MessagesTable->scrollToItem(msgItem); - onCellClickedMessage(j, 0); - break; - } - } - break; - } - } - } - } + ui->treeDBC->setCurrentItem(searchItems[0]); + searchItemPos = 0; + ui->lblSearchPos->setText("Search Results: " + QString::number(searchItemPos + 1) + " of " + QString::number(searchItems.count())); } - if (ui->radioName->isChecked()) + else { - DBC_MESSAGE *msg = dbcFile->messageHandler->findMsgByPartialName(ui->lineSearch->text()); - if (msg) - { - DBC_NODE *node = msg->sender; - if (node) - { - QString nodeName = node->name; - for (int i = 0; i < ui->NodesTable->rowCount(); i++) - { - QTableWidgetItem *item = ui->NodesTable->item(i, 0); - if (item->text() == nodeName) - { - item->setSelected(true); - ui->NodesTable->scrollToItem(item); - onCellClickedNode(i, 0); - for (int j = 0; j < ui->MessagesTable->rowCount(); j++) - { - QTableWidgetItem *msgItem = ui->MessagesTable->item(j, 1); - if (!msgItem) break; - if (msgItem->text().contains(ui->lineSearch->text(), Qt::CaseInsensitive)) - { - msgItem->setSelected(true); - ui->MessagesTable->scrollToItem(msgItem); - onCellClickedMessage(j, 0); - break; - } - } - break; - } - } - } - } - } */ + ui->lblSearchPos->setText("Search Results: 0 of 0"); + } +} + +void DBCMainEditor::handleSearchForward() +{ + if (searchItems.count() == 0) return; + if (searchItemPos < searchItems.count() - 1) searchItemPos++; + else searchItemPos = 0; + ui->treeDBC->setCurrentItem(searchItems[searchItemPos]); + ui->lblSearchPos->setText("Search Results: " + QString::number(searchItemPos + 1) + " of " + QString::number(searchItems.count())); +} + +void DBCMainEditor::handleSearchBackward() +{ + if (searchItems.count() == 0) return; + if (searchItemPos > 0) searchItemPos--; + else searchItemPos = searchItems.count() - 1; + ui->treeDBC->setCurrentItem(searchItems[searchItemPos]); + ui->lblSearchPos->setText("Search Results: " + QString::number(searchItemPos + 1) + " of " + QString::number(searchItems.count())); } //Double clicking is interpreted as a desire to edit the given item. @@ -243,6 +217,7 @@ void DBCMainEditor::onTreeDoubleClicked(const QModelIndex &index) bool ret = false; DBC_MESSAGE *msg; DBC_SIGNAL *sig; + DBC_NODE *node; uint32_t msgID; QString idString; @@ -251,6 +226,12 @@ void DBCMainEditor::onTreeDoubleClicked(const QModelIndex &index) switch (firstCol->data(0, Qt::UserRole).toInt()) { case 1: //a node + idString = firstCol->text(0).split(" ")[0]; + node = dbcFile->findNodeByName(idString); + nodeEditor->setFileIdx(fileIdx); + nodeEditor->setNodeRef(node); + nodeEditor->refreshView(); + nodeEditor->show(); break; case 2: //a message idString = firstCol->text(0).split(" ")[0]; @@ -258,8 +239,9 @@ void DBCMainEditor::onTreeDoubleClicked(const QModelIndex &index) msg = dbcFile->messageHandler->findMsgByID(msgID); msgEditor->setMessageRef(msg); msgEditor->setFileIdx(fileIdx); - msgEditor->setWindowModality(Qt::WindowModal); - msgEditor->exec(); //blocks this window from being active until we're done + //msgEditor->setWindowModality(Qt::WindowModal); + msgEditor->refreshView(); + msgEditor->show(); //show allows the rest of the forms to keep going break; case 3: //a signal idString = firstCol->parent()->text(0).split(" ")[0]; @@ -269,82 +251,11 @@ void DBCMainEditor::onTreeDoubleClicked(const QModelIndex &index) sigEditor->setSignalRef(sig); sigEditor->setMessageRef(msg); sigEditor->setFileIdx(fileIdx); - sigEditor->setWindowModality(Qt::WindowModal); - sigEditor->exec(); //blocks this window from being active until we're done + //sigEditor->setWindowModality(Qt::WindowModal); + sigEditor->refreshView(); + sigEditor->show(); break; } - -/* if (col == 3) //3 is the signals field. If clicked we go to the signals dialog - { - QTableWidgetItem* msg = ui->MessagesTable->item(row, 0); - if(msg) { - QString idString = msg->text(); - DBC_MESSAGE *message = dbcFile->messageHandler->findMsgByID(static_cast(Utility::ParseStringToNum(idString))); - sigEditor->setMessageRef(message); - sigEditor->setFileIdx(fileIdx); - sigEditor->setWindowModality(Qt::WindowModal); - sigEditor->exec(); //blocks this window from being active until we're done - //now update the displayed # of signals - inhibitCellChanged = true; - QTableWidgetItem *replacement = new QTableWidgetItem(QString::number(message->sigHandler->getCount())); - ui->MessagesTable->setItem(row, col, replacement); - inhibitCellChanged = false; - } - } - if (col == 4) - { - QColor newColor = QColorDialog::getColor(thisItem->background().color()); - thisItem->setBackground(newColor); - - if(!firstCol) return; - - msgID = Utility::ParseStringToNum2(firstCol->text(), &ret); - msg = dbcFile->messageHandler->findMsgByID(msgID); - - if (msg) - { - msg->fgColor = newColor; - DBC_ATTRIBUTE_VALUE *val = msg->findAttrValByName("GenMsgForegroundColor"); - if (val) - { - val->value = newColor.name(); - } - else - { - DBC_ATTRIBUTE_VALUE newVal; - newVal.attrName = "GenMsgForegroundColor"; - newVal.value = newColor.name(); - msg->attributes.append(newVal); - } - } - } - if (col == 5) - { - QColor newColor = QColorDialog::getColor(thisItem->background().color()); - thisItem->setBackground(newColor); - - if(!firstCol) return; - - msgID = Utility::ParseStringToNum2(firstCol->text(), &ret); - msg = dbcFile->messageHandler->findMsgByID(msgID); - - if (msg) - { - msg->bgColor = newColor; - DBC_ATTRIBUTE_VALUE *val = msg->findAttrValByName("GenMsgBackgroundColor"); - if (val) - { - val->value = newColor.name(); - } - else - { - DBC_ATTRIBUTE_VALUE newVal; - newVal.attrName = "GenMsgBackgroundColor"; - newVal.value = newColor.name(); - msg->attributes.append(newVal); - } - } - } */ } @@ -401,3 +312,8 @@ void DBCMainEditor::refreshTree() } ui->treeDBC->sortItems(0, Qt::SortOrder::AscendingOrder); //sort the display list for ease in viewing by mere mortals, helps me a lot. } + +void DBCMainEditor::updatedTreeInfo(QString oldData, QString newData, int type) +{ + +} diff --git a/dbc/dbcmaineditor.h b/dbc/dbcmaineditor.h index 3d2447e..6b83a4d 100644 --- a/dbc/dbcmaineditor.h +++ b/dbc/dbcmaineditor.h @@ -4,9 +4,11 @@ #include #include #include +#include #include "dbchandler.h" #include "dbcsignaleditor.h" #include "dbcmessageeditor.h" +#include "dbcnodeeditor.h" #include "utility.h" namespace Ui { @@ -22,11 +24,16 @@ public: ~DBCMainEditor(); void setFileIdx(int idx); +public slots: + void updatedTreeInfo(QString oldData, QString newData, int type); + private slots: void onTreeDoubleClicked(const QModelIndex &index); void onCustomMenuTree(QPoint); void deleteCurrentTreeItem(); void handleSearch(); + void handleSearchForward(); + void handleSearchBackward(); private: Ui::DBCMainEditor *ui; @@ -34,6 +41,7 @@ private: const QVector *referenceFrames; DBCSignalEditor *sigEditor; DBCMessageEditor *msgEditor; + DBCNodeEditor *nodeEditor; DBCFile *dbcFile; int fileIdx; QIcon nodeIcon; @@ -41,14 +49,15 @@ private: QIcon signalIcon; QIcon multiplexorSignalIcon; QIcon multiplexedSignalIcon; + QList searchItems; + int searchItemPos; - void refreshTree(); void showEvent(QShowEvent* event); void closeEvent(QCloseEvent *event); bool eventFilter(QObject *obj, QEvent *event); void readSettings(); void writeSettings(); - + void refreshTree(); }; #endif // DBCMAINEDITOR_H diff --git a/dbc/dbcmessageeditor.h b/dbc/dbcmessageeditor.h index 2685fcc..79cfe0f 100644 --- a/dbc/dbcmessageeditor.h +++ b/dbc/dbcmessageeditor.h @@ -19,6 +19,10 @@ public: void showEvent(QShowEvent*); void setMessageRef(DBC_MESSAGE *msg); void setFileIdx(int idx); + void refreshView(); + +signals: + void updatedTreeInfo(QString oldData, QString newData, int type); private: Ui::DBCMessageEditor *ui; @@ -31,7 +35,6 @@ private: bool eventFilter(QObject *obj, QEvent *event); void readSettings(); void writeSettings(); - void refreshView(); void generateSampleText(); }; diff --git a/dbc/dbcnodeeditor.cpp b/dbc/dbcnodeeditor.cpp new file mode 100644 index 0000000..62c269f --- /dev/null +++ b/dbc/dbcnodeeditor.cpp @@ -0,0 +1,130 @@ +#include "dbcnodeeditor.h" +#include "ui_dbcnodeeditor.h" + +#include +#include +#include +#include "helpwindow.h" +#include "utility.h" + +DBCNodeEditor::DBCNodeEditor(QWidget *parent) : + QDialog(parent), + ui(new Ui::DBCNodeEditor) +{ + ui->setupUi(this); + + readSettings(); + + dbcHandler = DBCHandler::getReference(); + dbcNode = nullptr; + + installEventFilter(this); +} + +DBCNodeEditor::~DBCNodeEditor() +{ + removeEventFilter(this); + delete ui; +} + +void DBCNodeEditor::closeEvent(QCloseEvent *event) +{ + Q_UNUSED(event); + writeSettings(); +} + +bool DBCNodeEditor::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::KeyRelease) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) + { + case Qt::Key_F1: + HelpWindow::getRef()->showHelp("nodeeditor.html"); + break; + } + return true; + } else { + // standard event processing + return QObject::eventFilter(obj, event); + } + return false; +} + +void DBCNodeEditor::setFileIdx(int idx) +{ + if (idx < 0 || idx > dbcHandler->getFileCount() - 1) return; + dbcFile = dbcHandler->getFileByIdx(idx); +} + +void DBCNodeEditor::readSettings() +{ + QSettings settings; + if (settings.value("Main/SaveRestorePositions", false).toBool()) + { + resize(settings.value("DBCNodeEditor/WindowSize", QSize(312, 128)).toSize()); + move(settings.value("DBCNodeEditor/WindowPos", QPoint(100, 100)).toPoint()); + } +} + +void DBCNodeEditor::writeSettings() +{ + QSettings settings; + + if (settings.value("Main/SaveRestorePositions", false).toBool()) + { + settings.setValue("DBCNodeEditor/WindowSize", size()); + settings.setValue("DBCNodeEditor/WindowPos", pos()); + } +} + + +void DBCNodeEditor::setNodeRef(DBC_NODE *node) +{ + dbcNode = node; +} + +void DBCNodeEditor::showEvent(QShowEvent* event) +{ + QDialog::showEvent(event); + + refreshView(); +} + +void DBCNodeEditor::refreshView() +{ + ui->lineComment->setText(dbcNode->comment); + ui->lineMsgName->setText(dbcNode->name); + + //generateSampleText(); +} + +void DBCNodeEditor::generateSampleText() +{ + /* + QBrush fg, bg; + + if (dbcMessage->fgColor.isValid()) fg = QBrush(dbcMessage->fgColor); + else fg = QBrush(QColor(dbcFile->findAttributeByName("GenMsgForegroundColor")->defaultValue.toString())); + if (dbcMessage->bgColor.isValid()) bg = QBrush(dbcMessage->bgColor); + else bg = QBrush(QColor(dbcFile->findAttributeByName("GenMsgBackgroundColor")->defaultValue.toString())); + + ui->listSample->clear(); + QListWidgetItem *item = new QListWidgetItem("Test String"); + item->setForeground(fg); + item->setBackground(bg); + ui->listSample->addItem(item); + item = new QListWidgetItem("0x20F TestMsg"); + item->setForeground(fg); + item->setBackground(bg); + ui->listSample->addItem(item); + item = new QListWidgetItem("20 FF 10 A1 BB CC 4D"); + item->setForeground(fg); + item->setBackground(bg); + ui->listSample->addItem(item); + item = new QListWidgetItem("1024.3434"); + item->setForeground(fg); + item->setBackground(bg); + ui->listSample->addItem(item); + */ +} diff --git a/dbc/dbcnodeeditor.h b/dbc/dbcnodeeditor.h new file mode 100644 index 0000000..6e9b44d --- /dev/null +++ b/dbc/dbcnodeeditor.h @@ -0,0 +1,41 @@ +#ifndef DBCNODEEDITOR_H +#define DBCNODEEDITOR_H + +#include +#include "dbc_classes.h" +#include "dbchandler.h" + +namespace Ui { +class DBCNodeEditor; +} + +class DBCNodeEditor : public QDialog +{ + Q_OBJECT + +public: + explicit DBCNodeEditor(QWidget *parent = nullptr); + ~DBCNodeEditor(); + void showEvent(QShowEvent*); + void setNodeRef(DBC_NODE *node); + void setFileIdx(int idx); + void refreshView(); + +signals: + void updatedTreeInfo(QString oldData, QString newData, int type); + +private: + Ui::DBCNodeEditor *ui; + + DBCHandler *dbcHandler; + DBC_NODE *dbcNode; + DBCFile *dbcFile; + + void closeEvent(QCloseEvent *event); + bool eventFilter(QObject *obj, QEvent *event); + void readSettings(); + void writeSettings(); + void generateSampleText(); +}; + +#endif // DBCNODEEDITOR_H diff --git a/dbc/dbcsignaleditor.cpp b/dbc/dbcsignaleditor.cpp index f8cbe6d..12a2fc7 100644 --- a/dbc/dbcsignaleditor.cpp +++ b/dbc/dbcsignaleditor.cpp @@ -125,6 +125,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : { if (currentSignal == nullptr) return; currentSignal->comment = ui->txtComment->text().simplified().replace(' ', '_'); + //updatedTreeInfo(); }); connect(ui->txtUnitName, &QLineEdit::editingFinished, @@ -151,8 +152,8 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : if (currentSignal == nullptr) return; QString tempNameStr = ui->txtName->text().simplified().replace(' ', '_'); if (tempNameStr.length() > 0) currentSignal->name = tempNameStr; - //need to update the list too. - //ui->signalsList->currentItem()->setText(currentSignal->name); + //need to update the tree too. + //updatedTreeInfo(); }); connect(ui->txtMultiplexValue, &QLineEdit::editingFinished, @@ -286,6 +287,12 @@ void DBCSignalEditor::showEvent(QShowEvent* event) fillValueTable(currentSignal); } +void DBCSignalEditor::refreshView() +{ + fillSignalForm(currentSignal); + fillValueTable(currentSignal); +} + void DBCSignalEditor::onValuesCellChanged(int row,int col) { if (inhibitCellChanged) return; @@ -519,6 +526,7 @@ void DBCSignalEditor::generateUsedBits() { int byt = y / 8; usedBits[byt] |= 1 << (y % 8); + ui->bitfield->setUsedSignalNum(y, x); } } else //big endian / motorola format @@ -529,6 +537,7 @@ void DBCSignalEditor::generateUsedBits() { int byt = startBit / 8; usedBits[byt] |= 1 << (startBit % 8); + ui->bitfield->setUsedSignalNum(startBit, x); size--; if ((startBit % 8) == 0) startBit += 15; else startBit--; diff --git a/dbc/dbcsignaleditor.h b/dbc/dbcsignaleditor.h index 2b6e94f..4de69a5 100644 --- a/dbc/dbcsignaleditor.h +++ b/dbc/dbcsignaleditor.h @@ -20,6 +20,10 @@ public: void setSignalRef(DBC_SIGNAL *sig); void showEvent(QShowEvent*); ~DBCSignalEditor(); + void refreshView(); + +signals: + void updatedTreeInfo(QString oldData, QString newData, int type); private slots: void bitfieldClicked(int x, int y); diff --git a/ui/dbcmaineditor.ui b/ui/dbcmaineditor.ui index 4fa6719..dfc98ce 100644 --- a/ui/dbcmaineditor.ui +++ b/ui/dbcmaineditor.ui @@ -32,6 +32,9 @@ + + false + New Message @@ -46,6 +49,9 @@ + + false + New Signal @@ -60,6 +66,9 @@ + + false + Delete @@ -91,6 +100,27 @@ + + false + + + QAbstractItemView::NoDragDrop + + + Qt::IgnoreAction + + + QAbstractItemView::SingleSelection + + + 28 + + + true + + + false + 1 @@ -107,30 +137,13 @@ - Find DBC Message: + Find: - - - - Msg ID - - - true - - - - - - - Msg Name - - - @@ -140,6 +153,31 @@ + + + + + + Search Results: 0 of 0 + + + + + + + Next + + + + + + + Previous + + + + + diff --git a/ui/dbcmessageeditor.ui b/ui/dbcmessageeditor.ui new file mode 100644 index 0000000..e5ff6db --- /dev/null +++ b/ui/dbcmessageeditor.ui @@ -0,0 +1,123 @@ + + + DBCMessageEditor + + + + 0 + 0 + 311 + 371 + + + + Message Editor + + + + + + + + Frame ID: + + + + + + + + + + + + + Message Name: + + + + + + + + + + Frame Length: + + + + + + + Text Color: + + + + + + + Background Color: + + + + + + + Comments: + + + + + + + + + + Sending Node: + + + + + + + + + + Set + + + + + + + Set + + + + + + + + + Sample Colored Output: + + + Qt::AlignCenter + + + + + + + + + + Close + + + + + + + + diff --git a/ui/dbcnodeeditor.ui b/ui/dbcnodeeditor.ui new file mode 100644 index 0000000..80c521b --- /dev/null +++ b/ui/dbcnodeeditor.ui @@ -0,0 +1,52 @@ + + + DBCNodeEditor + + + + 0 + 0 + 311 + 128 + + + + Node Editor + + + + + + + + Node Name: + + + + + + + + + + Comments: + + + + + + + + + + + + Close + + + + + + + + From 1a5010fca667a57200b217fb7d4b0daac13705bc Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 22 Jul 2020 20:57:22 -0400 Subject: [PATCH 45/58] Editing of nodes, messages, signals all works now. Dynamically updates DBC tree if info changes. --- dbc/dbcmaineditor.cpp | 54 ++++++++++++++++++++++++++++++++++------ dbc/dbcmaineditor.h | 7 +++++- dbc/dbcmessageeditor.cpp | 31 +++++++++++++++++++++++ dbc/dbcmessageeditor.h | 2 +- dbc/dbcnodeeditor.cpp | 16 ++++++++++++ dbc/dbcnodeeditor.h | 2 +- dbc/dbcsignaleditor.cpp | 4 +-- dbc/dbcsignaleditor.h | 2 +- ui/dbcmessageeditor.ui | 7 ------ ui/dbcnodeeditor.ui | 9 +------ 10 files changed, 105 insertions(+), 29 deletions(-) diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index 5d1dd96..7dc2e10 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -31,9 +31,9 @@ DBCMainEditor::DBCMainEditor( const QVector *frames, QWidget *parent) nodeEditor = new DBCNodeEditor(this); //all three might potentially change the data stored and force the tree to be updated - connect(sigEditor, &DBCSignalEditor::updatedTreeInfo, this, &DBCMainEditor::updatedTreeInfo); - connect(msgEditor, &DBCMessageEditor::updatedTreeInfo, this, &DBCMainEditor::updatedTreeInfo); - connect(nodeEditor, &DBCNodeEditor::updatedTreeInfo, this, &DBCMainEditor::updatedTreeInfo); + connect(sigEditor, &DBCSignalEditor::updatedTreeInfo, this, &DBCMainEditor::updatedSignal); + connect(msgEditor, &DBCMessageEditor::updatedTreeInfo, this, &DBCMainEditor::updatedMessage); + connect(nodeEditor, &DBCNodeEditor::updatedTreeInfo, this, &DBCMainEditor::updatedNode); nodeIcon = QIcon(":/icons/images/node.png"); messageIcon = QIcon(":/icons/images/message.png"); @@ -266,6 +266,9 @@ void DBCMainEditor::onTreeDoubleClicked(const QModelIndex &index) void DBCMainEditor::refreshTree() { ui->treeDBC->clear(); + nodeToItem.clear(); + messageToItem.clear(); + signalToItem.clear(); if (dbcFile->findNodeByName("Vector__XXX") == nullptr) { @@ -275,18 +278,20 @@ void DBCMainEditor::refreshTree() dbcFile->dbc_nodes.append(newNode); } - foreach (DBC_NODE node, dbcFile->dbc_nodes) + for (int n = 0; n < dbcFile->dbc_nodes.count(); n++) { + DBC_NODE *node = &dbcFile->dbc_nodes[n]; QTreeWidgetItem *nodeItem = new QTreeWidgetItem(); - QString nodeInfo = node.name; - if (node.comment.count() > 0) nodeInfo.append(" - ").append(node.comment); + QString nodeInfo = node->name; + if (node->comment.count() > 0) nodeInfo.append(" - ").append(node->comment); nodeItem->setText(0, nodeInfo); nodeItem->setIcon(0, nodeIcon); nodeItem->setData(0, Qt::UserRole, 1); + nodeToItem.insert(node, nodeItem); for (int x = 0; x < dbcFile->messageHandler->getCount(); x++) { DBC_MESSAGE *msg = dbcFile->messageHandler->findMsgByIdx(x); - if (msg->sender->name == node.name) + if (msg->sender->name == node->name) { QTreeWidgetItem *msgItem = new QTreeWidgetItem(nodeItem); QString msgInfo = Utility::formatCANID(msg->ID) + " " + msg->name; @@ -294,6 +299,7 @@ void DBCMainEditor::refreshTree() msgItem->setText(0, msgInfo); msgItem->setIcon(0, messageIcon); msgItem->setData(0, Qt::UserRole, 2); + messageToItem.insert(msg, msgItem); for (int i = 0; i < msg->sigHandler->getCount(); i++) { DBC_SIGNAL *sig = msg->sigHandler->findSignalByIdx(i); @@ -305,6 +311,7 @@ void DBCMainEditor::refreshTree() else if (sig->isMultiplexor) sigItem->setIcon(0, multiplexorSignalIcon); else sigItem->setIcon(0, signalIcon); sigItem->setData(0, Qt::UserRole, 3); + signalToItem.insert(sig, sigItem); } } } @@ -313,7 +320,38 @@ void DBCMainEditor::refreshTree() ui->treeDBC->sortItems(0, Qt::SortOrder::AscendingOrder); //sort the display list for ease in viewing by mere mortals, helps me a lot. } -void DBCMainEditor::updatedTreeInfo(QString oldData, QString newData, int type) +void DBCMainEditor::updatedNode(DBC_NODE *node) { + if (nodeToItem.contains(node)) + { + QTreeWidgetItem *item = nodeToItem[node]; + QString nodeInfo = node->name; + if (node->comment.count() > 0) nodeInfo.append(" - ").append(node->comment); + item->setText(0, nodeInfo); + } + else qDebug() << "That node doesn't exist. That's a bug dude."; +} +void DBCMainEditor::updatedMessage(DBC_MESSAGE *msg) +{ + if (messageToItem.contains(msg)) + { + QTreeWidgetItem *item = messageToItem.value(msg); + QString msgInfo = Utility::formatCANID(msg->ID) + " " + msg->name; + if (msg->comment.count() > 0) msgInfo.append(" - ").append(msg->comment); + item->setText(0, msgInfo); + } + else qDebug() << "Shit?! That mesage doesn't exist. That's a bug dude."; +} + +void DBCMainEditor::updatedSignal(DBC_SIGNAL *sig) +{ + if (signalToItem.contains(sig)) + { + QTreeWidgetItem *item = signalToItem.value(sig); + QString sigInfo = sig->name; + if (sig->comment.count() > 0) sigInfo.append(" - ").append(sig->comment); + item->setText(0, sigInfo); + } + else qDebug() << "Shit?! That signal doesn't exist. That's a bug dude."; } diff --git a/dbc/dbcmaineditor.h b/dbc/dbcmaineditor.h index 6b83a4d..b16e940 100644 --- a/dbc/dbcmaineditor.h +++ b/dbc/dbcmaineditor.h @@ -25,7 +25,9 @@ public: void setFileIdx(int idx); public slots: - void updatedTreeInfo(QString oldData, QString newData, int type); + void updatedNode(DBC_NODE *node); + void updatedMessage(DBC_MESSAGE *msg); + void updatedSignal(DBC_SIGNAL *sig); private slots: void onTreeDoubleClicked(const QModelIndex &index); @@ -51,6 +53,9 @@ private: QIcon multiplexedSignalIcon; QList searchItems; int searchItemPos; + QMap nodeToItem; + QMap messageToItem; + QMap signalToItem; void showEvent(QShowEvent* event); void closeEvent(QCloseEvent *event); diff --git a/dbc/dbcmessageeditor.cpp b/dbc/dbcmessageeditor.cpp index 1ccdcb5..c388c17 100644 --- a/dbc/dbcmessageeditor.cpp +++ b/dbc/dbcmessageeditor.cpp @@ -18,6 +18,37 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : dbcHandler = DBCHandler::getReference(); dbcMessage = nullptr; + connect(ui->lineComment, &QLineEdit::editingFinished, + [=]() + { + if (dbcMessage == nullptr) return; + dbcMessage->comment = ui->lineComment->text(); + emit updatedTreeInfo(dbcMessage); + }); + + connect(ui->lineFrameID, &QLineEdit::editingFinished, + [=]() + { + if (dbcMessage == nullptr) return; + dbcMessage->ID = Utility::ParseStringToNum(ui->lineFrameID->text()); + emit updatedTreeInfo(dbcMessage); + }); + + connect(ui->lineMsgName, &QLineEdit::editingFinished, + [=]() + { + if (dbcMessage == nullptr) return; + dbcMessage->name = ui->lineMsgName->text().simplified().replace(' ', '_'); + emit updatedTreeInfo(dbcMessage); + }); + + connect(ui->lineFrameLen, &QLineEdit::editingFinished, + [=]() + { + if (dbcMessage == nullptr) return; + dbcMessage->len = Utility::ParseStringToNum(ui->lineFrameLen->text()); + }); + connect(ui->btnTextColor, &QAbstractButton::clicked, [=]() { diff --git a/dbc/dbcmessageeditor.h b/dbc/dbcmessageeditor.h index 79cfe0f..5388466 100644 --- a/dbc/dbcmessageeditor.h +++ b/dbc/dbcmessageeditor.h @@ -22,7 +22,7 @@ public: void refreshView(); signals: - void updatedTreeInfo(QString oldData, QString newData, int type); + void updatedTreeInfo(DBC_MESSAGE *msg); private: Ui::DBCMessageEditor *ui; diff --git a/dbc/dbcnodeeditor.cpp b/dbc/dbcnodeeditor.cpp index 62c269f..e321fe3 100644 --- a/dbc/dbcnodeeditor.cpp +++ b/dbc/dbcnodeeditor.cpp @@ -18,6 +18,22 @@ DBCNodeEditor::DBCNodeEditor(QWidget *parent) : dbcHandler = DBCHandler::getReference(); dbcNode = nullptr; + connect(ui->lineComment, &QLineEdit::editingFinished, + [=]() + { + if (dbcNode == nullptr) return; + dbcNode->comment = ui->lineComment->text(); + emit updatedTreeInfo(dbcNode); + }); + + connect(ui->lineMsgName, &QLineEdit::editingFinished, + [=]() + { + if (dbcNode == nullptr) return; + dbcNode->name = ui->lineMsgName->text(); + emit updatedTreeInfo(dbcNode); + }); + installEventFilter(this); } diff --git a/dbc/dbcnodeeditor.h b/dbc/dbcnodeeditor.h index 6e9b44d..3bd65e0 100644 --- a/dbc/dbcnodeeditor.h +++ b/dbc/dbcnodeeditor.h @@ -22,7 +22,7 @@ public: void refreshView(); signals: - void updatedTreeInfo(QString oldData, QString newData, int type); + void updatedTreeInfo(DBC_NODE *node); private: Ui::DBCNodeEditor *ui; diff --git a/dbc/dbcsignaleditor.cpp b/dbc/dbcsignaleditor.cpp index 12a2fc7..7fd4016 100644 --- a/dbc/dbcsignaleditor.cpp +++ b/dbc/dbcsignaleditor.cpp @@ -125,7 +125,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : { if (currentSignal == nullptr) return; currentSignal->comment = ui->txtComment->text().simplified().replace(' ', '_'); - //updatedTreeInfo(); + emit updatedTreeInfo(currentSignal); }); connect(ui->txtUnitName, &QLineEdit::editingFinished, @@ -153,7 +153,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : QString tempNameStr = ui->txtName->text().simplified().replace(' ', '_'); if (tempNameStr.length() > 0) currentSignal->name = tempNameStr; //need to update the tree too. - //updatedTreeInfo(); + emit updatedTreeInfo(currentSignal); }); connect(ui->txtMultiplexValue, &QLineEdit::editingFinished, diff --git a/dbc/dbcsignaleditor.h b/dbc/dbcsignaleditor.h index 4de69a5..0c69ea3 100644 --- a/dbc/dbcsignaleditor.h +++ b/dbc/dbcsignaleditor.h @@ -23,7 +23,7 @@ public: void refreshView(); signals: - void updatedTreeInfo(QString oldData, QString newData, int type); + void updatedTreeInfo(DBC_SIGNAL *sig); private slots: void bitfieldClicked(int x, int y); diff --git a/ui/dbcmessageeditor.ui b/ui/dbcmessageeditor.ui index e5ff6db..c4b3a2a 100644 --- a/ui/dbcmessageeditor.ui +++ b/ui/dbcmessageeditor.ui @@ -109,13 +109,6 @@ - - - - Close - - - diff --git a/ui/dbcnodeeditor.ui b/ui/dbcnodeeditor.ui index 80c521b..f56bd35 100644 --- a/ui/dbcnodeeditor.ui +++ b/ui/dbcnodeeditor.ui @@ -7,7 +7,7 @@ 0 0 311 - 128 + 88 @@ -38,13 +38,6 @@
- - - - Close - - -
From 1a1ccb5fec6e78c4623c052874dd3f8ef13dda5e Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Thu, 23 Jul 2020 21:52:43 -0400 Subject: [PATCH 46/58] Initial implementation of adding nodes, messages, signals with new tree style. --- dbc/dbcmaineditor.cpp | 238 ++++++++++++++++++++++++++++++++++++------ dbc/dbcmaineditor.h | 6 ++ 2 files changed, 212 insertions(+), 32 deletions(-) diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index 7dc2e10..47569b2 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "helpwindow.h" @@ -25,6 +26,11 @@ DBCMainEditor::DBCMainEditor( const QVector *frames, QWidget *parent) connect(ui->btnSearchNext, &QAbstractButton::clicked, this, &DBCMainEditor::handleSearchForward); connect(ui->btnSearchPrev, &QAbstractButton::clicked, this, &DBCMainEditor::handleSearchBackward); connect(ui->treeDBC, &QTreeWidget::doubleClicked, this, &DBCMainEditor::onTreeDoubleClicked); + connect(ui->treeDBC, &QTreeWidget::currentItemChanged, this, &DBCMainEditor::currentItemChanged); + connect(ui->btnDelete, &QAbstractButton::clicked, this, &DBCMainEditor::deleteCurrentTreeItem); + connect(ui->btnNewNode, &QAbstractButton::clicked, this, &DBCMainEditor::newNode); + connect(ui->btnNewMessage, &QAbstractButton::clicked, this, &DBCMainEditor::newMessage); + connect(ui->btnNewSignal, &QAbstractButton::clicked, this, &DBCMainEditor::newSignal); sigEditor = new DBCSignalEditor(this); msgEditor = new DBCMessageEditor(this); @@ -142,38 +148,6 @@ void DBCMainEditor::onCustomMenuTree(QPoint point) } -void DBCMainEditor::deleteCurrentTreeItem() -{ - /* - int thisRow = ui->NodesTable->currentRow(); - QTableWidgetItem* thisItem = ui->NodesTable->item(thisRow, 0); - if (!thisItem) return; - QString nodeName = thisItem->text(); - if (nodeName.length() > 0 && nodeName.compare("Vector__XXX", Qt::CaseInsensitive) != 0) - { - ui->NodesTable->removeRow(thisRow); - dbcFile->dbc_nodes.removeAt(thisRow); - inhibitCellChanged = true; - refreshMessagesTable(&dbcFile->dbc_nodes[0]); - ui->NodesTable->selectRow(0); - inhibitCellChanged = false; - } */ -} - -//void DBCMainEditor::deleteCurrentMessage() -//{ - /* - int thisRow = ui->MessagesTable->currentRow(); - QTableWidgetItem* thisItem = ui->MessagesTable->item(thisRow, 0); - if (!thisItem) return; - if (thisItem->text().length() > 0) - { - ui->MessagesTable->removeRow(thisRow); - dbcFile->messageHandler->removeMessageByIndex(thisRow); - } - */ -//} - void DBCMainEditor::handleSearch() { searchItems = ui->treeDBC->findItems(ui->lineSearch->text(),Qt::MatchContains | Qt::MatchRecursive); @@ -208,6 +182,43 @@ void DBCMainEditor::handleSearchBackward() ui->lblSearchPos->setText("Search Results: " + QString::number(searchItemPos + 1) + " of " + QString::number(searchItems.count())); } +void DBCMainEditor::currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev) +{ + Q_UNUSED(prev) + if (!current) + { + ui->btnNewMessage->setEnabled(false); + ui->btnNewSignal->setEnabled(false); + ui->btnDelete->setEnabled(false); + return; + } + switch (current->data(0, Qt::UserRole).toInt()) + { + case 1: //node + ui->btnNewNode->setEnabled(true); + ui->btnNewMessage->setEnabled(true); + ui->btnNewSignal->setEnabled(false); + ui->btnDelete->setEnabled(true); + break; + case 2: //message + ui->btnNewNode->setEnabled(true); + ui->btnNewMessage->setEnabled(true); + ui->btnNewSignal->setEnabled(true); + ui->btnDelete->setEnabled(true); + break; + case 3: //signal + ui->btnNewNode->setEnabled(true); + ui->btnNewMessage->setEnabled(true); + ui->btnNewSignal->setEnabled(true); + ui->btnDelete->setEnabled(true); + break; + default: + ui->btnNewMessage->setEnabled(false); + ui->btnNewSignal->setEnabled(false); + ui->btnDelete->setEnabled(false); + } +} + //Double clicking is interpreted as a desire to edit the given item. void DBCMainEditor::onTreeDoubleClicked(const QModelIndex &index) { @@ -355,3 +366,166 @@ void DBCMainEditor::updatedSignal(DBC_SIGNAL *sig) } else qDebug() << "Shit?! That signal doesn't exist. That's a bug dude."; } + +void DBCMainEditor::newNode() +{ + DBC_NODE node; + DBC_NODE *nodePtr; + node.name = "Unnamed" + QString::number(randGen.bounded(50000)); + dbcFile->dbc_nodes.append(node); + nodePtr = dbcFile->findNodeByName(node.name); + QTreeWidgetItem *nodeItem = new QTreeWidgetItem(); + nodeItem->setText(0, node.name); + nodeItem->setIcon(0, nodeIcon); + nodeItem->setData(0, Qt::UserRole, 1); + nodeToItem.insert(nodePtr, nodeItem); + ui->treeDBC->addTopLevelItem(nodeItem); + ui->treeDBC->setCurrentItem(nodeItem); +} + + +//create a new message with it's parent being the node we're currently within +void DBCMainEditor::newMessage() +{ + QTreeWidgetItem *nodeItem = nullptr; + QTreeWidgetItem *msgItem = nullptr; + nodeItem = ui->treeDBC->currentItem(); + int typ = nodeItem->data(0, Qt::UserRole).toInt(); + if (typ == 2) + { + msgItem = nodeItem; + nodeItem = nodeItem->parent(); + } + if (typ == 3) + { + msgItem = nodeItem->parent(); + nodeItem = msgItem->parent(); + } + + DBC_NODE *node = dbcFile->findNodeByName(nodeItem->data(0, Qt::DisplayRole).toString()); + DBC_MESSAGE msg; + DBC_MESSAGE *msgPtr; + if (msgItem) + { + QString idString = msgItem->text(0).split(" ")[0]; + int msgID = static_cast(Utility::ParseStringToNum(idString)); + DBC_MESSAGE *oldMsg = dbcFile->messageHandler->findMsgByID(msgID); + if (oldMsg) + { + msg.name = oldMsg->name + QString::number(randGen.bounded(1000)); + msg.ID = oldMsg->ID + 1; + msg.len = oldMsg->len; + msg.bgColor = oldMsg->bgColor; + msg.fgColor = oldMsg->fgColor; + msg.comment = oldMsg->comment; + } + else + { + msg.name = nodeItem->text(0) + "Msg" + QString::number(randGen.bounded(500)); + msg.ID = 0; + msg.len = 0; + } + } + else + { + msg.name = nodeItem->text(0) + "Msg" + QString::number(randGen.bounded(500)); + msg.ID = 0; + msg.len = 0; + } + msg.sender = node; + + dbcFile->messageHandler->addMessage(msg); + msgPtr = dbcFile->messageHandler->findMsgByIdx(dbcFile->messageHandler->getCount() - 1); + QTreeWidgetItem *newMsgItem = new QTreeWidgetItem(); + QString msgInfo = Utility::formatCANID(msg.ID) + " " + msg.name; + if (msg.comment.count() > 0) msgInfo.append(" - ").append(msg.comment); + newMsgItem->setText(0, msgInfo); + newMsgItem->setIcon(0, messageIcon); + newMsgItem->setData(0, Qt::UserRole, 2); + messageToItem.insert(msgPtr, newMsgItem); + nodeItem->addChild(newMsgItem); + ui->treeDBC->setCurrentItem(newMsgItem); +} + +void DBCMainEditor::newSignal() +{ + QTreeWidgetItem *msgItem = nullptr; + QTreeWidgetItem *sigItem = nullptr; + msgItem = ui->treeDBC->currentItem(); + int typ = msgItem->data(0, Qt::UserRole).toInt(); + if (typ == 3) + { + sigItem = msgItem; + msgItem = msgItem->parent(); + } + + QString idString = msgItem->text(0).split(" ")[0]; + int msgID = static_cast(Utility::ParseStringToNum(idString)); + DBC_MESSAGE *msg = dbcFile->messageHandler->findMsgByID(msgID); + DBC_SIGNAL sig; + DBC_SIGNAL *sigPtr; + if (sigItem) + { + DBC_SIGNAL *oldSig = msg->sigHandler->findSignalByName(sigItem->text(0).split(" ")[0]); + if (oldSig) + { + sig = *oldSig; //copy it all over + sig.name = sig.name + QString::number(randGen.bounded(100)); + } + else + { + sig.name = msgItem->text(0).split(" ")[1] + "Sig" + QString::number(randGen.bounded(500)); + } + } + else + { + sig.name = msgItem->text(0).split(" ")[1] + "Sig" + QString::number(randGen.bounded(500)); + } + + msg->sigHandler->addSignal(sig); + sigPtr = msg->sigHandler->findSignalByIdx(msg->sigHandler->getCount() - 1); + QTreeWidgetItem *newSigItem = new QTreeWidgetItem(); + QString sigInfo = sig.name; + if (sig.comment.count() > 0) sigInfo.append(" - ").append(sig.comment); + newSigItem->setText(0, sigInfo); + if (sig.isMultiplexed) newSigItem->setIcon(0, multiplexedSignalIcon); + else if (sig.isMultiplexor) newSigItem->setIcon(0, multiplexorSignalIcon); + else newSigItem->setIcon(0, signalIcon); + newSigItem->setData(0, Qt::UserRole, 3); + signalToItem.insert(sigPtr, newSigItem); + msgItem->addChild(newSigItem); + ui->treeDBC->setCurrentItem(newSigItem); + +} + +void DBCMainEditor::deleteCurrentTreeItem() +{ + /* + int thisRow = ui->NodesTable->currentRow(); + QTableWidgetItem* thisItem = ui->NodesTable->item(thisRow, 0); + if (!thisItem) return; + QString nodeName = thisItem->text(); + if (nodeName.length() > 0 && nodeName.compare("Vector__XXX", Qt::CaseInsensitive) != 0) + { + ui->NodesTable->removeRow(thisRow); + dbcFile->dbc_nodes.removeAt(thisRow); + inhibitCellChanged = true; + refreshMessagesTable(&dbcFile->dbc_nodes[0]); + ui->NodesTable->selectRow(0); + inhibitCellChanged = false; + } */ +} + +//void DBCMainEditor::deleteCurrentMessage() +//{ + /* + int thisRow = ui->MessagesTable->currentRow(); + QTableWidgetItem* thisItem = ui->MessagesTable->item(thisRow, 0); + if (!thisItem) return; + if (thisItem->text().length() > 0) + { + ui->MessagesTable->removeRow(thisRow); + dbcFile->messageHandler->removeMessageByIndex(thisRow); + } + */ +//} diff --git a/dbc/dbcmaineditor.h b/dbc/dbcmaineditor.h index b16e940..82cff20 100644 --- a/dbc/dbcmaineditor.h +++ b/dbc/dbcmaineditor.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "dbchandler.h" #include "dbcsignaleditor.h" #include "dbcmessageeditor.h" @@ -31,11 +32,15 @@ public slots: private slots: void onTreeDoubleClicked(const QModelIndex &index); + void currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev); void onCustomMenuTree(QPoint); void deleteCurrentTreeItem(); void handleSearch(); void handleSearchForward(); void handleSearchBackward(); + void newNode(); + void newMessage(); + void newSignal(); private: Ui::DBCMainEditor *ui; @@ -56,6 +61,7 @@ private: QMap nodeToItem; QMap messageToItem; QMap signalToItem; + QRandomGenerator randGen; void showEvent(QShowEvent* event); void closeEvent(QCloseEvent *event); From 4cfb4b903a347ec22114949074c51c60da2c4cb0 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 26 Jul 2020 14:49:08 -0400 Subject: [PATCH 47/58] Added ability to delete nodes, messages, signals within tree. --- dbc/dbchandler.cpp | 26 ++++-- dbc/dbcmaineditor.cpp | 186 ++++++++++++++++++++++++++++++++++++------ dbc/dbcmaineditor.h | 7 ++ 3 files changed, 187 insertions(+), 32 deletions(-) diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index 10cacbd..c1db554 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -46,9 +46,16 @@ bool DBCSignalHandler::addSignal(DBC_SIGNAL &sig) bool DBCSignalHandler::removeSignal(DBC_SIGNAL *sig) { - Q_UNUSED(sig) - //if (sigs.removeAll(*sig) > 0) return true; - return false; + qDebug() << "Total # of signals: " << getCount(); + for (int i = 0; i < getCount(); i++) + { + if (sigs[i].name == sig->name) + { + sigs.removeAt(i); + qDebug() << "Removed signal at idx " << i; + } + } + return true; } bool DBCSignalHandler::removeSignal(int idx) @@ -173,9 +180,16 @@ bool DBCMessageHandler::addMessage(DBC_MESSAGE &msg) bool DBCMessageHandler::removeMessage(DBC_MESSAGE *msg) { - Q_UNUSED(msg); - //if (messages.removeAll(*msg) > 0) return true; - return false; + qDebug() << "Total # of messages: " << getCount(); + for (int i = 0; i < getCount(); i++) + { + if (messages[i].name == msg->name) + { + messages.removeAt(i); + qDebug() << "Removed message at idx " << i; + } + } + return true; } bool DBCMessageHandler::removeMessageByIndex(int idx) diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index 47569b2..721b576 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -93,6 +93,17 @@ bool DBCMainEditor::eventFilter(QObject *obj, QEvent *event) case Qt::Key_F4: handleSearchBackward(); break; + case Qt::Key_F5: + newNode(); + break; + case Qt::Key_F6: + newMessage(); + break; + case Qt::Key_F7: + newSignal(); + break; + case Qt::Key_Delete: + deleteCurrentTreeItem(); } return true; } else { @@ -280,6 +291,9 @@ void DBCMainEditor::refreshTree() nodeToItem.clear(); messageToItem.clear(); signalToItem.clear(); + itemToNode.clear(); + itemToMessage.clear(); + itemToSignal.clear(); if (dbcFile->findNodeByName("Vector__XXX") == nullptr) { @@ -299,6 +313,7 @@ void DBCMainEditor::refreshTree() nodeItem->setIcon(0, nodeIcon); nodeItem->setData(0, Qt::UserRole, 1); nodeToItem.insert(node, nodeItem); + itemToNode.insert(nodeItem, node); for (int x = 0; x < dbcFile->messageHandler->getCount(); x++) { DBC_MESSAGE *msg = dbcFile->messageHandler->findMsgByIdx(x); @@ -311,6 +326,7 @@ void DBCMainEditor::refreshTree() msgItem->setIcon(0, messageIcon); msgItem->setData(0, Qt::UserRole, 2); messageToItem.insert(msg, msgItem); + itemToMessage.insert(msgItem, msg); for (int i = 0; i < msg->sigHandler->getCount(); i++) { DBC_SIGNAL *sig = msg->sigHandler->findSignalByIdx(i); @@ -323,6 +339,7 @@ void DBCMainEditor::refreshTree() else sigItem->setIcon(0, signalIcon); sigItem->setData(0, Qt::UserRole, 3); signalToItem.insert(sig, sigItem); + itemToSignal.insert(sigItem, sig); } } } @@ -352,7 +369,7 @@ void DBCMainEditor::updatedMessage(DBC_MESSAGE *msg) if (msg->comment.count() > 0) msgInfo.append(" - ").append(msg->comment); item->setText(0, msgInfo); } - else qDebug() << "Shit?! That mesage doesn't exist. That's a bug dude."; + else qDebug() << "That mesage doesn't exist. That's a bug dude."; } void DBCMainEditor::updatedSignal(DBC_SIGNAL *sig) @@ -364,7 +381,7 @@ void DBCMainEditor::updatedSignal(DBC_SIGNAL *sig) if (sig->comment.count() > 0) sigInfo.append(" - ").append(sig->comment); item->setText(0, sigInfo); } - else qDebug() << "Shit?! That signal doesn't exist. That's a bug dude."; + else qDebug() << "That signal doesn't exist. That's a bug dude."; } void DBCMainEditor::newNode() @@ -379,6 +396,7 @@ void DBCMainEditor::newNode() nodeItem->setIcon(0, nodeIcon); nodeItem->setData(0, Qt::UserRole, 1); nodeToItem.insert(nodePtr, nodeItem); + itemToNode.insert(nodeItem, nodePtr); ui->treeDBC->addTopLevelItem(nodeItem); ui->treeDBC->setCurrentItem(nodeItem); } @@ -391,6 +409,7 @@ void DBCMainEditor::newMessage() QTreeWidgetItem *msgItem = nullptr; nodeItem = ui->treeDBC->currentItem(); int typ = nodeItem->data(0, Qt::UserRole).toInt(); + if (!nodeItem) return; //nothing selected! if (typ == 2) { msgItem = nodeItem; @@ -443,6 +462,7 @@ void DBCMainEditor::newMessage() newMsgItem->setIcon(0, messageIcon); newMsgItem->setData(0, Qt::UserRole, 2); messageToItem.insert(msgPtr, newMsgItem); + itemToMessage.insert(newMsgItem, msgPtr); nodeItem->addChild(newMsgItem); ui->treeDBC->setCurrentItem(newMsgItem); } @@ -452,7 +472,9 @@ void DBCMainEditor::newSignal() QTreeWidgetItem *msgItem = nullptr; QTreeWidgetItem *sigItem = nullptr; msgItem = ui->treeDBC->currentItem(); + if (!msgItem) return; //nothing selected! int typ = msgItem->data(0, Qt::UserRole).toInt(); + if (typ == 1) return; //can't add signals to a node! if (typ == 3) { sigItem = msgItem; @@ -482,6 +504,7 @@ void DBCMainEditor::newSignal() sig.name = msgItem->text(0).split(" ")[1] + "Sig" + QString::number(randGen.bounded(500)); } + sig.parentMessage = msg; msg->sigHandler->addSignal(sig); sigPtr = msg->sigHandler->findSignalByIdx(msg->sigHandler->getCount() - 1); QTreeWidgetItem *newSigItem = new QTreeWidgetItem(); @@ -493,39 +516,150 @@ void DBCMainEditor::newSignal() else newSigItem->setIcon(0, signalIcon); newSigItem->setData(0, Qt::UserRole, 3); signalToItem.insert(sigPtr, newSigItem); + itemToSignal.insert(newSigItem, sigPtr); msgItem->addChild(newSigItem); ui->treeDBC->setCurrentItem(newSigItem); } +//gets confirmation before calling the real routines that delete things void DBCMainEditor::deleteCurrentTreeItem() { - /* - int thisRow = ui->NodesTable->currentRow(); - QTableWidgetItem* thisItem = ui->NodesTable->item(thisRow, 0); - if (!thisItem) return; - QString nodeName = thisItem->text(); - if (nodeName.length() > 0 && nodeName.compare("Vector__XXX", Qt::CaseInsensitive) != 0) + QTreeWidgetItem *currItem = ui->treeDBC->currentItem(); + int typ = currItem->data(0, Qt::UserRole).toInt(); + QString idString; + int msgID; + DBC_MESSAGE *msg; + DBC_NODE *node; + int numMsg = 0, numSig = 0; + + QMessageBox::StandardButton confirmDialog; + + switch (typ) { - ui->NodesTable->removeRow(thisRow); - dbcFile->dbc_nodes.removeAt(thisRow); - inhibitCellChanged = true; - refreshMessagesTable(&dbcFile->dbc_nodes[0]); - ui->NodesTable->selectRow(0); - inhibitCellChanged = false; - } */ + case 1: //deleting a node cascades deletion down to messages and signals + node = dbcFile->findNodeByName(currItem->text(0)); + if (!node) return; + for (int x = 0; x < dbcFile->messageHandler->getCount(); x++) + { + if (dbcFile->messageHandler->findMsgByIdx(x)->sender == node) + { + numMsg++; + numSig += dbcFile->messageHandler->findMsgByIdx(x)->sigHandler->getCount(); + } + } + confirmDialog = QMessageBox::question(this, "Really?", "Are you sure you want to delete this node, its\n" + + QString::number(numMsg) + " messages, and their " + QString::number(numSig) + " combined signals?", QMessageBox::Yes|QMessageBox::No); + if (confirmDialog == QMessageBox::Yes) + { + if (itemToNode.contains(currItem)) + { + DBC_NODE *node = itemToNode.value(currItem); + deleteNode(node); + } + else + { + qDebug() << "WTF, could not find the node in the map. That should not happen."; + } + } + + break; + case 2: //cascades to removing all signals too. + idString = currItem->text(0).split(" ")[0]; + msgID = static_cast(Utility::ParseStringToNum(idString)); + msg = dbcFile->messageHandler->findMsgByID(msgID); + + confirmDialog = QMessageBox::question(this, "Really?", "Are you sure you want to delete\nthis message and its " + + QString::number(msg->sigHandler->getCount()) + " signals?", QMessageBox::Yes|QMessageBox::No); + if (confirmDialog == QMessageBox::Yes) + { + QTreeWidgetItem *currItem = ui->treeDBC->currentItem(); + if (itemToMessage.contains(currItem)) + { + DBC_MESSAGE *msg = itemToMessage.value(currItem); + deleteMessage(msg); + } + else + { + qDebug() << "WTF, could not find the message in the map. That should not happen."; + } + } + break; + case 3: //no cascade, just this one signal. + confirmDialog = QMessageBox::question(this, "Really?", "Are you sure you want to delete this signal?", + QMessageBox::Yes|QMessageBox::No); + if (confirmDialog == QMessageBox::Yes) + { + QTreeWidgetItem *currItem = ui->treeDBC->currentItem(); + if (itemToSignal.contains(currItem)) + { + DBC_SIGNAL *sig = itemToSignal.value(currItem); + deleteSignal(sig); + } + else + { + qDebug() << "WTF, could not find the signal in the map. That should not happen."; + } + } + break; + } } -//void DBCMainEditor::deleteCurrentMessage() -//{ - /* - int thisRow = ui->MessagesTable->currentRow(); - QTableWidgetItem* thisItem = ui->MessagesTable->item(thisRow, 0); - if (!thisItem) return; - if (thisItem->text().length() > 0) +//these don't ask for permission. You call it, it disappears forever. +void DBCMainEditor::deleteNode(DBC_NODE *node) +{ + qDebug() << "Going through with it you mass deleter!"; + if (!nodeToItem.contains(node)) return; + QTreeWidgetItem *currItem = nodeToItem[node]; + //don't actually store which messages are associated to which nodes so just iterate through the messages list and whack the ones + //that claim to be associated to this node. + int numItems = dbcFile->messageHandler->getCount(); + for (int i = numItems - 1; i > -1; i--) { - ui->MessagesTable->removeRow(thisRow); - dbcFile->messageHandler->removeMessageByIndex(thisRow); + if (dbcFile->messageHandler->findMsgByIdx(i)->sender == node) deleteMessage(dbcFile->messageHandler->findMsgByIdx(i)); } - */ -//} + + for (int j = 0; j < dbcFile->dbc_nodes.count(); j++) + { + if (dbcFile->dbc_nodes.at(j).name == node->name) dbcFile->dbc_nodes.removeAt(j); + } + + nodeToItem.remove(node); + itemToNode.remove(currItem); + ui->treeDBC->removeItemWidget(currItem, 0); + delete currItem; +} + +void DBCMainEditor::deleteMessage(DBC_MESSAGE *msg) +{ + qDebug() << "Deleting the message and all signals. Bye bye!"; + if (!messageToItem.contains(msg)) return; + QTreeWidgetItem *currItem = messageToItem[msg]; + + int numItems = msg->sigHandler->getCount(); + for (int i = numItems - 1; i > -1; i--) + { + deleteSignal(msg->sigHandler->findSignalByIdx(i)); + } + + dbcFile->messageHandler->removeMessage(msg); + + itemToMessage.remove(currItem); + messageToItem.remove(msg); + ui->treeDBC->removeItemWidget(currItem, 0); + delete currItem; + +} + +void DBCMainEditor::deleteSignal(DBC_SIGNAL *sig) +{ + qDebug() << "Signal about to vanish."; + if (!signalToItem.contains(sig)) return; + QTreeWidgetItem *currItem = signalToItem[sig]; + sig->parentMessage->sigHandler->removeSignal(sig); + + itemToSignal.remove(currItem); + signalToItem.remove(sig); + ui->treeDBC->removeItemWidget(currItem, 0); + delete currItem; +} diff --git a/dbc/dbcmaineditor.h b/dbc/dbcmaineditor.h index 82cff20..02e8e1f 100644 --- a/dbc/dbcmaineditor.h +++ b/dbc/dbcmaineditor.h @@ -35,6 +35,9 @@ private slots: void currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev); void onCustomMenuTree(QPoint); void deleteCurrentTreeItem(); + void deleteNode(DBC_NODE *node); + void deleteMessage(DBC_MESSAGE *msg); + void deleteSignal(DBC_SIGNAL *sig); void handleSearch(); void handleSearchForward(); void handleSearchBackward(); @@ -58,9 +61,13 @@ private: QIcon multiplexedSignalIcon; QList searchItems; int searchItemPos; + //bidirectional mapping of QTreeWidget items back and forth to DBC objects QMap nodeToItem; QMap messageToItem; QMap signalToItem; + QMap itemToNode; + QMap itemToMessage; + QMap itemToSignal; QRandomGenerator randGen; void showEvent(QShowEvent* event); From a5b741062964a2317eaf17694a8b5230842cc3c3 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 29 Jul 2020 20:45:36 -0400 Subject: [PATCH 48/58] Add tracking of DBC editing in order to warn when exiting without saving, add sorting to DBC routines, add a new screen that allows one to compare two DBC files for changes/differences. --- SavvyCAN.pro | 7 +- dbc/dbc_classes.h | 15 ++ dbc/dbchandler.cpp | 49 ++++- dbc/dbchandler.h | 8 + dbc/dbcmaineditor.cpp | 8 +- dbc/dbcmessageeditor.cpp | 8 +- dbc/dbcnodeeditor.cpp | 2 + dbc/dbcsignaleditor.cpp | 38 +++- mainwindow.cpp | 32 ++- mainwindow.h | 3 + re/dbccomparatorwindow.cpp | 399 +++++++++++++++++++++++++++++++++++++ re/dbccomparatorwindow.h | 45 +++++ ui/dbccomparatorwindow.ui | 131 ++++++++++++ ui/mainwindow.ui | 6 + 14 files changed, 738 insertions(+), 13 deletions(-) create mode 100644 re/dbccomparatorwindow.cpp create mode 100644 re/dbccomparatorwindow.h create mode 100644 ui/dbccomparatorwindow.ui diff --git a/SavvyCAN.pro b/SavvyCAN.pro index 0ad1aad..a42ad47 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -15,7 +15,8 @@ DEFINES += QCUSTOMPLOT_USE_OPENGL TARGET = SavvyCAN TEMPLATE = app -SOURCES += main.cpp\ +SOURCES += main.cpp\ + re/dbccomparatorwindow.cpp \ mainwindow.cpp \ canframemodel.cpp \ utility.cpp \ @@ -75,7 +76,8 @@ SOURCES += main.cpp\ HEADERS += mainwindow.h \ can_structs.h \ - canframemodel.h \ + canframemodel.h \ \ + re/dbccomparatorwindow.h \ utility.h \ qcustomplot.h \ frameplaybackwindow.h \ @@ -136,6 +138,7 @@ HEADERS += mainwindow.h \ filterutility.h FORMS += ui/candatagrid.ui \ + ui/dbccomparatorwindow.ui \ ui/dbcmessageeditor.ui \ ui/connectionwindow.ui \ ui/dbcloadsavewindow.ui \ diff --git a/dbc/dbc_classes.h b/dbc/dbc_classes.h index 5d32b67..ac9279e 100644 --- a/dbc/dbc_classes.h +++ b/dbc/dbc_classes.h @@ -74,6 +74,11 @@ public: DBC_ATTRIBUTE_VALUE *findAttrValByName(QString name); DBC_ATTRIBUTE_VALUE *findAttrValByIdx(int idx); + + friend bool operator<(const DBC_NODE& l, const DBC_NODE& r) + { + return (l.name.toLower() < r.name.toLower()); + } }; class DBC_MESSAGE; //forward reference so that DBC_SIGNAL can compile before we get to real definition of DBC_MESSAGE @@ -107,6 +112,11 @@ public: //TODO: this is sloppy. It shouldn't all be public! QString makePrettyOutput(double floatVal, int64_t intVal, bool outputName = true); DBC_ATTRIBUTE_VALUE *findAttrValByName(QString name); DBC_ATTRIBUTE_VALUE *findAttrValByIdx(int idx); + + friend bool operator<(const DBC_SIGNAL& l, const DBC_SIGNAL& r) + { + return (l.name.toLower() < r.name.toLower()); + } }; class DBCSignalHandler; //forward declaration to keep from having to include dbchandler.h in this file and thus create a loop @@ -129,6 +139,11 @@ public: DBC_ATTRIBUTE_VALUE *findAttrValByName(QString name); DBC_ATTRIBUTE_VALUE *findAttrValByIdx(int idx); + + friend bool operator<(const DBC_MESSAGE& l, const DBC_MESSAGE& r) + { + return (l.name.toLower() < r.name.toLower()); + } }; diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index c1db554..e88a658 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -92,6 +92,11 @@ int DBCSignalHandler::getCount() return sigs.count(); } +void DBCSignalHandler::sort() +{ + std::sort(sigs.begin(), sigs.end()); +} + DBC_MESSAGE* DBCMessageHandler::findMsgByID(uint32_t id) { if (messages.count() == 0) return nullptr; @@ -241,6 +246,15 @@ int DBCMessageHandler::getCount() return messages.count(); } +void DBCMessageHandler::sort() +{ + std::sort(messages.begin(), messages.end()); + for (int i = 0; i < messages.count(); i++) + { + messages[i].sigHandler->sort(); + } +} + bool DBCMessageHandler::filterLabeling() { return filterLabelingEnabled; @@ -266,6 +280,7 @@ DBCFile::DBCFile() messageHandler = new DBCMessageHandler; messageHandler->setMatchingCriteria(EXACT); messageHandler->setFilterLabeling(false); + isDirty = false; } DBCFile::DBCFile(const DBCFile& cpy) : QObject() @@ -283,6 +298,7 @@ DBCFile::DBCFile(const DBCFile& cpy) : QObject() dbc_nodes.append(cpy.dbc_nodes); dbc_attributes.clear(); dbc_attributes.append(cpy.dbc_attributes); + isDirty = cpy.isDirty; } DBCFile& DBCFile::operator=(const DBCFile& cpy) @@ -301,6 +317,12 @@ DBCFile& DBCFile::operator=(const DBCFile& cpy) return *this; } +void DBCFile::sort() +{ + std::sort(dbc_nodes.begin(), dbc_nodes.end()); //sort node names + messageHandler->sort(); //sort messages, each of which sorts its signals too +} + DBC_NODE* DBCFile::findNodeByIdx(int idx) { if (idx < 0) return nullptr; @@ -380,6 +402,17 @@ void DBCFile::findAttributesByType(DBC_ATTRIBUTE_TYPE typ, QList } } +//there's no external way to clear the flag. It is only cleared when the file is saved by this object. +void DBCFile::setDirtyFlag() +{ + isDirty = true; +} + +bool DBCFile::getDirtyFlag() +{ + return isDirty; +} + DBC_MESSAGE* DBCFile::parseMessageLine(QString line) { QRegularExpression regex; @@ -735,6 +768,7 @@ void DBCFile::loadFile(QString fileName) DBC_MESSAGE *currentMessage = nullptr; DBC_ATTRIBUTE attr; int numSigFaults = 0, numMsgFaults = 0; + int linesSinceYield = 0; bool inMultilineBU = false; @@ -761,6 +795,12 @@ void DBCFile::loadFile(QString fileName) rawLine = QString(inFile->readLine()); line = rawLine.simplified(); + linesSinceYield++; + if (linesSinceYield > 100) + { + qApp->processEvents(); + } + if (inMultilineBU) { if (rawLine.startsWith("\t") || rawLine.startsWith(" ")) @@ -998,6 +1038,7 @@ void DBCFile::loadFile(QString fileName) this->fileName = fileList[fileList.length() - 1]; //whoops... same name as parameter in this function. filePath = fileName.left(fileName.length() - this->fileName.length()); assocBuses = -1; + isDirty = false; } QVariant DBCFile::processAttributeVal(QString input, DBC_ATTRIBUTE_VAL_TYPE typ) @@ -1214,7 +1255,10 @@ void DBCFile::saveFile(QString fileName) msgNumber++; } - msgOutput.append("BO_ " + QString::number(msg->ID) + " " + msg->name + ": " + QString::number(msg->len) + + uint32_t ID = msg->ID; + if (msg->ID > 0x7FF) msg->ID += 0x80000000ul; //set bit 31 if this ID is extended. + + msgOutput.append("BO_ " + QString::number(ID) + " " + msg->name + ": " + QString::number(msg->len) + " " + msg->sender->name + "\n"); if (msg->comment.length() > 0) { @@ -1405,6 +1449,8 @@ void DBCFile::saveFile(QString fileName) outFile->close(); delete outFile; + isDirty = false; + QStringList fileList = fileName.split('/'); this->fileName = fileList[fileList.length() - 1]; //whoops... same name as parameter in this function. filePath = fileName.left(fileName.length() - this->fileName.length()); @@ -1681,6 +1727,7 @@ DBCFile* DBCHandler::loadJSONFile(QString filename) } } } + thisFile->setDirtyFlag(); return thisFile; } diff --git a/dbc/dbchandler.h b/dbc/dbchandler.h index 5e83525..f2341eb 100644 --- a/dbc/dbchandler.h +++ b/dbc/dbchandler.h @@ -29,6 +29,8 @@ public: bool removeSignal(QString name); void removeAllSignals(); int getCount(); + void sort(); + private: QList sigs; //signals is a reserved word or I'd have used that }; @@ -52,6 +54,8 @@ public: void setMatchingCriteria(MatchingCriteria_t mc); void setFilterLabeling( bool labelFiltering ); bool filterLabeling(); + void sort(); + private: QList messages; MatchingCriteria_t matchingCriteria; @@ -79,6 +83,9 @@ public: QString getPath(); int getAssocBus(); void setAssocBus(int bus); + void setDirtyFlag(); + bool getDirtyFlag(); + void sort(); DBCMessageHandler *messageHandler; QList dbc_nodes; @@ -87,6 +94,7 @@ private: QString fileName; QString filePath; int assocBuses; //-1 = all buses, 0 = first bus, 1 = second bus, etc. + bool isDirty; //has the file been modified? bool parseAttribute(QString inpString, DBC_ATTRIBUTE &attr); QVariant processAttributeVal(QString input, DBC_ATTRIBUTE_VAL_TYPE typ); diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index 721b576..e3ea24e 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -399,6 +399,7 @@ void DBCMainEditor::newNode() itemToNode.insert(nodeItem, nodePtr); ui->treeDBC->addTopLevelItem(nodeItem); ui->treeDBC->setCurrentItem(nodeItem); + dbcFile->setDirtyFlag(); } @@ -465,6 +466,7 @@ void DBCMainEditor::newMessage() itemToMessage.insert(newMsgItem, msgPtr); nodeItem->addChild(newMsgItem); ui->treeDBC->setCurrentItem(newMsgItem); + dbcFile->setDirtyFlag(); } void DBCMainEditor::newSignal() @@ -519,7 +521,7 @@ void DBCMainEditor::newSignal() itemToSignal.insert(newSigItem, sigPtr); msgItem->addChild(newSigItem); ui->treeDBC->setCurrentItem(newSigItem); - + dbcFile->setDirtyFlag(); } //gets confirmation before calling the real routines that delete things @@ -628,6 +630,7 @@ void DBCMainEditor::deleteNode(DBC_NODE *node) itemToNode.remove(currItem); ui->treeDBC->removeItemWidget(currItem, 0); delete currItem; + dbcFile->setDirtyFlag(); } void DBCMainEditor::deleteMessage(DBC_MESSAGE *msg) @@ -648,7 +651,7 @@ void DBCMainEditor::deleteMessage(DBC_MESSAGE *msg) messageToItem.remove(msg); ui->treeDBC->removeItemWidget(currItem, 0); delete currItem; - + dbcFile->setDirtyFlag(); } void DBCMainEditor::deleteSignal(DBC_SIGNAL *sig) @@ -662,4 +665,5 @@ void DBCMainEditor::deleteSignal(DBC_SIGNAL *sig) signalToItem.remove(sig); ui->treeDBC->removeItemWidget(currItem, 0); delete currItem; + dbcFile->setDirtyFlag(); } diff --git a/dbc/dbcmessageeditor.cpp b/dbc/dbcmessageeditor.cpp index c388c17..8b3a426 100644 --- a/dbc/dbcmessageeditor.cpp +++ b/dbc/dbcmessageeditor.cpp @@ -22,6 +22,7 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : [=]() { if (dbcMessage == nullptr) return; + if (dbcMessage->comment != ui->lineComment->text()) dbcFile->setDirtyFlag(); dbcMessage->comment = ui->lineComment->text(); emit updatedTreeInfo(dbcMessage); }); @@ -30,6 +31,7 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : [=]() { if (dbcMessage == nullptr) return; + if (dbcMessage->ID != Utility::ParseStringToNum(ui->lineFrameID->text())) dbcFile->setDirtyFlag(); dbcMessage->ID = Utility::ParseStringToNum(ui->lineFrameID->text()); emit updatedTreeInfo(dbcMessage); }); @@ -38,6 +40,7 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : [=]() { if (dbcMessage == nullptr) return; + if (dbcMessage->name != ui->lineMsgName->text().simplified().replace(' ', '_')) dbcFile->setDirtyFlag(); dbcMessage->name = ui->lineMsgName->text().simplified().replace(' ', '_'); emit updatedTreeInfo(dbcMessage); }); @@ -46,6 +49,7 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : [=]() { if (dbcMessage == nullptr) return; + if (dbcMessage->len != Utility::ParseStringToNum(ui->lineFrameLen->text())) dbcFile->setDirtyFlag(); dbcMessage->len = Utility::ParseStringToNum(ui->lineFrameLen->text()); }); @@ -53,7 +57,7 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : [=]() { QColor newColor = QColorDialog::getColor(dbcMessage->fgColor); - + if (dbcMessage->fgColor != newColor) dbcFile->setDirtyFlag(); dbcMessage->fgColor = newColor; DBC_ATTRIBUTE_VALUE *val = dbcMessage->findAttrValByName("GenMsgForegroundColor"); if (val) @@ -74,7 +78,7 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : [=]() { QColor newColor = QColorDialog::getColor(dbcMessage->bgColor); - + if (dbcMessage->bgColor != newColor) dbcFile->setDirtyFlag(); dbcMessage->bgColor = newColor; DBC_ATTRIBUTE_VALUE *val = dbcMessage->findAttrValByName("GenMsgBackgroundColor"); if (val) diff --git a/dbc/dbcnodeeditor.cpp b/dbc/dbcnodeeditor.cpp index e321fe3..0f25543 100644 --- a/dbc/dbcnodeeditor.cpp +++ b/dbc/dbcnodeeditor.cpp @@ -22,6 +22,7 @@ DBCNodeEditor::DBCNodeEditor(QWidget *parent) : [=]() { if (dbcNode == nullptr) return; + if (dbcNode->comment != ui->lineComment->text()) dbcFile->setDirtyFlag(); dbcNode->comment = ui->lineComment->text(); emit updatedTreeInfo(dbcNode); }); @@ -30,6 +31,7 @@ DBCNodeEditor::DBCNodeEditor(QWidget *parent) : [=]() { if (dbcNode == nullptr) return; + if (dbcNode->name != ui->lineMsgName->text()) dbcFile->setDirtyFlag(); dbcNode->name = ui->lineMsgName->text(); emit updatedTreeInfo(dbcNode); }); diff --git a/dbc/dbcsignaleditor.cpp b/dbc/dbcsignaleditor.cpp index 7fd4016..ce69871 100644 --- a/dbc/dbcsignaleditor.cpp +++ b/dbc/dbcsignaleditor.cpp @@ -44,6 +44,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : [=]() { if (currentSignal == nullptr) return; + if (currentSignal->intelByteOrder != ui->cbIntelFormat->isChecked()) dbcFile->setDirtyFlag(); currentSignal->intelByteOrder = ui->cbIntelFormat->isChecked(); fillSignalForm(currentSignal); }); @@ -52,7 +53,9 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : [=]() { if (currentSignal == nullptr) return; - currentSignal->receiver = dbcFile->findNodeByName(ui->comboReceiver->currentText()); + DBC_NODE *node = dbcFile->findNodeByName(ui->comboReceiver->currentText()); + if (currentSignal->receiver != node) dbcFile->setDirtyFlag(); + currentSignal->receiver = node; }); connect(ui->comboType, &QComboBox::currentTextChanged, [=]() @@ -80,6 +83,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : currentSignal->valType = STRING; break; } + dbcFile->setDirtyFlag(); fillSignalForm(currentSignal); }); connect(ui->txtBias, &QLineEdit::editingFinished, @@ -89,7 +93,11 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : double temp; bool result; temp = ui->txtBias->text().toDouble(&result); - if (result) currentSignal->bias = temp; + if (result) + { + if (currentSignal->bias != temp) dbcFile->setDirtyFlag(); + currentSignal->bias = temp; + } }); connect(ui->txtMaxVal, &QLineEdit::editingFinished, @@ -99,7 +107,11 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : double temp; bool result; temp = ui->txtMaxVal->text().toDouble(&result); - if (result) currentSignal->max = temp; + if (result) + { + if (currentSignal->max != temp) dbcFile->setDirtyFlag(); + currentSignal->max = temp; + } }); connect(ui->txtMinVal, &QLineEdit::editingFinished, @@ -109,7 +121,11 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : double temp; bool result; temp = ui->txtMinVal->text().toDouble(&result); - if (result) currentSignal->min = temp; + if (result) + { + if (currentSignal->min != temp) dbcFile->setDirtyFlag(); + currentSignal->min = temp; + } }); connect(ui->txtScale, &QLineEdit::editingFinished, [=]() @@ -118,12 +134,17 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : double temp; bool result; temp = ui->txtScale->text().toDouble(&result); - if (result) currentSignal->factor = temp; + if (result) + { + if (currentSignal->factor != temp) dbcFile->setDirtyFlag(); + currentSignal->factor = temp; + } }); connect(ui->txtComment, &QLineEdit::editingFinished, [=]() { if (currentSignal == nullptr) return; + if (currentSignal->comment != ui->txtComment->text().simplified().replace(' ','_')) dbcFile->setDirtyFlag(); currentSignal->comment = ui->txtComment->text().simplified().replace(' ', '_'); emit updatedTreeInfo(currentSignal); }); @@ -132,6 +153,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : [=]() { if (currentSignal == nullptr) return; + if (currentSignal->unitName != ui->txtUnitName->text().simplified().replace(' ','_')) dbcFile->setDirtyFlag(); currentSignal->unitName = ui->txtUnitName->text().simplified().replace(' ', '_'); }); connect(ui->txtBitLength, &QLineEdit::textChanged, @@ -142,6 +164,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : temp = Utility::ParseStringToNum(ui->txtBitLength->text()); if (temp < 1) return; if (temp > 64) return; + if (currentSignal->signalSize != temp) dbcFile->setDirtyFlag(); if (currentSignal->valType != SP_FLOAT && currentSignal->valType != DP_FLOAT) currentSignal->signalSize = temp; fillSignalForm(currentSignal); @@ -151,6 +174,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : { if (currentSignal == nullptr) return; QString tempNameStr = ui->txtName->text().simplified().replace(' ', '_'); + if (currentSignal->name != tempNameStr) dbcFile->setDirtyFlag(); if (tempNameStr.length() > 0) currentSignal->name = tempNameStr; //need to update the tree too. emit updatedTreeInfo(currentSignal); @@ -162,6 +186,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : if (currentSignal == nullptr) return; int temp; temp = Utility::ParseStringToNum(ui->txtMultiplexValue->text()); + if (currentSignal->multiplexValue != temp) dbcFile->setDirtyFlag(); //TODO: could look up the multiplexor and ensure that the value is within a range that the multiplexor could return currentSignal->multiplexValue = temp; }); @@ -175,6 +200,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : //if the set multiplexor for the message was this signal then clear it if (dbcMessage->multiplexorSignal == currentSignal) dbcMessage->multiplexorSignal = nullptr; } + dbcFile->setDirtyFlag(); }); connect(ui->rbMultiplexor, &QRadioButton::toggled, @@ -189,6 +215,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : //we just set that this is the multiplexor so update the message to show that as well. dbcMessage->multiplexorSignal = currentSignal; } + dbcFile->setDirtyFlag(); }); connect(ui->rbNotMulti, &QRadioButton::toggled, @@ -200,6 +227,7 @@ DBCSignalEditor::DBCSignalEditor(QWidget *parent) : currentSignal->isMultiplexor = false; if (dbcMessage->multiplexorSignal == currentSignal) dbcMessage->multiplexorSignal = nullptr; } + dbcFile->setDirtyFlag(); }); installEventFilter(this); diff --git a/mainwindow.cpp b/mainwindow.cpp index ab8e7c5..4b32a54 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -80,6 +80,7 @@ MainWindow::MainWindow(QWidget *parent) : bisectWindow = nullptr; signalViewerWindow = nullptr; temporalGraphWindow = nullptr; + dbcComparatorWindow = nullptr; dbcHandler = DBCHandler::getReference(); bDirty = false; inhibitFilterUpdate = false; @@ -111,6 +112,7 @@ MainWindow::MainWindow(QWidget *parent) : connect(ui->actionSave_Decoded_Frames, &QAction::triggered, this, &MainWindow::handleSaveDecoded); connect(ui->actionSingle_Multi_State_2, &QAction::triggered, this, &MainWindow::showSingleMultiWindow); connect(ui->actionFile_Comparison, &QAction::triggered, this, &MainWindow::showComparisonWindow); + connect(ui->actionDBC_Comparison, &QAction::triggered, this, &MainWindow::showDBCComparisonWindow); connect(ui->actionScripting_INterface, &QAction::triggered, this, &MainWindow::showScriptingWindow); connect(ui->btnNormalize, &QAbstractButton::clicked, this, &MainWindow::normalizeTiming); connect(ui->actionPreferences, &QAction::triggered, this, &MainWindow::showSettingsDialog); @@ -257,12 +259,31 @@ void MainWindow::exitApp() QApplication::quit(); //forces the whole application to terminate when the main window is closed } + +//the close event can be trapped and ignored so put unsaved warnings in here so the user can abort the program closing if they forgot to save things. void MainWindow::closeEvent(QCloseEvent *event) { - Q_UNUSED(event); + + QMessageBox::StandardButton confirmDialog; + + for (int i = 0; i < dbcHandler->getFileCount(); i++) + { + DBCFile *file = dbcHandler->getFileByIdx(i); + if (file->getDirtyFlag()) + { + confirmDialog = QMessageBox::question(this, "Unsaved DBC", "DBC File:\n" + file->getFilename() + "\nAppears to have unsaved changes\nReally close without saving?", QMessageBox::Yes|QMessageBox::No); + if (confirmDialog != QMessageBox::Yes) + { + event->ignore(); + return; + } + } + } + removeEventFilter(this); writeSettings(); exitApp(); + event->accept(); } bool MainWindow::eventFilter(QObject *obj, QEvent *event) @@ -1001,6 +1022,15 @@ void MainWindow::showComparisonWindow() comparatorWindow->show(); } +void MainWindow::showDBCComparisonWindow() +{ + if (!dbcComparatorWindow) + { + dbcComparatorWindow = new DBCComparatorWindow(); + } + dbcComparatorWindow->show(); +} + void MainWindow::showSingleMultiWindow() { if (!discreteStateWindow) diff --git a/mainwindow.h b/mainwindow.h index d3a45dd..68b8cec 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -32,6 +32,7 @@ #include "motorcontrollerconfigwindow.h" #include "signalviewerwindow.h" #include "re/temporalgraphwindow.h" +#include "re/dbccomparatorwindow.h" class CANConnection; class ConnectionWindow; @@ -85,6 +86,7 @@ private slots: void showBisectWindow(); void showSignalViewer(); void showTemporalGraphWindow(); + void showDBCComparisonWindow(); void exitApp(); void handleSaveDecoded(); void connectionStatusUpdated(int conns); @@ -165,6 +167,7 @@ private: BisectWindow* bisectWindow; SignalViewerWindow *signalViewerWindow; TemporalGraphWindow *temporalGraphWindow; + DBCComparatorWindow *dbcComparatorWindow; //various private storage QLabel lbStatusConnected; diff --git a/re/dbccomparatorwindow.cpp b/re/dbccomparatorwindow.cpp new file mode 100644 index 0000000..b4f2cc1 --- /dev/null +++ b/re/dbccomparatorwindow.cpp @@ -0,0 +1,399 @@ +#include "dbccomparatorwindow.h" +#include "ui_dbccomparatorwindow.h" +#include "helpwindow.h" +#include +#include +#include + +DBCComparatorWindow::DBCComparatorWindow(QWidget *parent) : + QDialog(parent), + ui(new Ui::DBCComparatorWindow) +{ + ui->setupUi(this); + setWindowFlags(Qt::Window); + + connect(ui->btnDBCFile1, SIGNAL(clicked(bool)), this, SLOT(loadFirstFile())); + connect(ui->btnDBCFile2, SIGNAL(clicked(bool)), this, SLOT(loadSecondFile())); + connect(ui->btnSaveDetails, SIGNAL(clicked(bool)), this, SLOT(saveDetails())); + + ui->lblFirstFile->setText(""); + ui->lblSecondFile->setText(""); + + firstDBC = nullptr; + secondDBC = nullptr; + + installEventFilter(this); +} + +DBCComparatorWindow::~DBCComparatorWindow() +{ + removeEventFilter(this); + delete ui; +} + +void DBCComparatorWindow::showEvent(QShowEvent *) +{ + readSettings(); +} + +void DBCComparatorWindow::closeEvent(QCloseEvent *event) +{ + Q_UNUSED(event) + writeSettings(); +} + +bool DBCComparatorWindow::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::KeyRelease) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) + { + case Qt::Key_F1: + HelpWindow::getRef()->showHelp("filecomparison.html"); + break; + } + return true; + } else { + // standard event processing + return QObject::eventFilter(obj, event); + } + return false; +} + +void DBCComparatorWindow::readSettings() +{ + QSettings settings; + if (settings.value("Main/SaveRestorePositions", false).toBool()) + { + resize(settings.value("DBCComparator/WindowSize", QSize(720, 631)).toSize()); + move(settings.value("DBCComparator/WindowPos", QPoint(50, 50)).toPoint()); + } +} + +void DBCComparatorWindow::writeSettings() +{ + QSettings settings; + + if (settings.value("Main/SaveRestorePositions", false).toBool()) + { + settings.setValue("DBCComparator/WindowSize", size()); + settings.setValue("DBCComparator/WindowPos", pos()); + } +} + +QString DBCComparatorWindow::loadDBC(DBCFile **file) +{ + QString filename; + QFileDialog dialog; + QSettings settings; + + QStringList filters; + filters.append(QString(tr("DBC Files (*.dbc *.DBC)"))); + + dialog.setDirectory(settings.value("FileIO/LoadSaveDirectory", dialog.directory().path()).toString()); + dialog.setFileMode(QFileDialog::ExistingFile); + dialog.setNameFilters(filters); + dialog.setViewMode(QFileDialog::Detail); + + if (dialog.exec() == QDialog::Accepted) + { + filename = dialog.selectedFiles()[0]; + + QProgressDialog progress(qApp->activeWindow()); + progress.setWindowModality(Qt::WindowModal); + progress.setLabelText("Loading file..."); + progress.setCancelButton(nullptr); + progress.setRange(0,0); + progress.setMinimumDuration(0); + progress.show(); + + qApp->processEvents(); + + if (dialog.selectedNameFilter() == filters[0]) + { + if (file[0]) delete file[0]; + file[0] = new DBCFile; + file[0]->loadFile(filename); + qDebug() << "Loaded the DBC file into first slot"; + } + + progress.cancel(); + return filename; + } + return QString(); +} + +void DBCComparatorWindow::loadFirstFile() +{ + QString filename; + filename = loadDBC(&firstDBC); + ui->lblFirstFile->setText(filename); + if (firstDBC && secondDBC) calculateDetails(); +} + +void DBCComparatorWindow::loadSecondFile() +{ + QString filename; + filename = loadDBC(&secondDBC); + ui->lblSecondFile->setText(filename); + if (firstDBC && secondDBC) calculateDetails(); +} + +void DBCComparatorWindow::calculateDetails() +{ + QProgressDialog progress(this); + progress.setWindowModality(Qt::WindowModal); + progress.setLabelText("Calculating differences"); + progress.setCancelButton(nullptr); + progress.setRange(0,0); + progress.setMinimumDuration(0); + progress.show(); + + qApp->processEvents(); + + firstDBC->sort(); + secondDBC->sort(); + + + ui->treeDetails->clear(); + + //Find nodes missing in one of the DBC files + + QTreeWidgetItem *nodeDiffRoot = new QTreeWidgetItem(); + nodeDiffRoot->setText(0, "Node Differences"); + QTreeWidgetItem *nodesMissingDBCFirst = new QTreeWidgetItem(); + nodesMissingDBCFirst->setText(0, "Nodes not found in first DBC"); + QTreeWidgetItem *nodesMissingDBCSecond = new QTreeWidgetItem(); + nodesMissingDBCSecond->setText(0, "Nodes not found in second DBC"); + nodeDiffRoot->addChild(nodesMissingDBCFirst); + nodeDiffRoot->addChild(nodesMissingDBCSecond); + ui->treeDetails->addTopLevelItem(nodeDiffRoot); + + for (int i = 0; i < firstDBC->dbc_nodes.count(); i++) + { + QString nodeName = firstDBC->dbc_nodes[i].name; + DBC_NODE *node = secondDBC->findNodeByName(nodeName); + if (!node) + { + QTreeWidgetItem *missingNodeItem = new QTreeWidgetItem(); + missingNodeItem->setText(0, nodeName); + nodesMissingDBCSecond->addChild(missingNodeItem); + } + } + + for (int i = 0; i < secondDBC->dbc_nodes.count(); i++) + { + QString nodeName = secondDBC->dbc_nodes[i].name; + DBC_NODE *node = firstDBC->findNodeByName(nodeName); + if (!node) + { + QTreeWidgetItem *missingNodeItem = new QTreeWidgetItem(); + missingNodeItem->setText(0, nodeName); + nodesMissingDBCFirst->addChild(missingNodeItem); + } + } + + + + //Find messages that are missing in one of the DBC files + //If a message exists in both then check for missing signals + + QTreeWidgetItem *msgDiffRoot = new QTreeWidgetItem(); + msgDiffRoot->setText(0, "Message Differences"); + QTreeWidgetItem *msgMissingDBCFirst = new QTreeWidgetItem(); + msgMissingDBCFirst->setText(0, "Messages not found in first DBC"); + QTreeWidgetItem *msgMissingDBCSecond = new QTreeWidgetItem(); + msgMissingDBCSecond->setText(0, "Messages not found in second DBC"); + QTreeWidgetItem *msgSignalsDiff = new QTreeWidgetItem(); + msgSignalsDiff->setText(0, "Messages with missing signals"); + QTreeWidgetItem *sigDiffOne = new QTreeWidgetItem(); + sigDiffOne->setText(0, "Missing from first DBC"); + QTreeWidgetItem *sigDiffTwo = new QTreeWidgetItem(); + sigDiffTwo->setText(0, "Missing from second DBC"); + QTreeWidgetItem *sigModifiedRoot = new QTreeWidgetItem(); + sigModifiedRoot->setText(0, "Modified Signals"); + msgSignalsDiff->addChild(sigDiffOne); + msgSignalsDiff->addChild(sigDiffTwo); + + msgDiffRoot->addChild(msgMissingDBCFirst); + msgDiffRoot->addChild(msgMissingDBCSecond); + msgDiffRoot->addChild(msgSignalsDiff); + msgDiffRoot->addChild(sigModifiedRoot); + + ui->treeDetails->addTopLevelItem(msgDiffRoot); + + QTreeWidgetItem *msgItem; + QTreeWidgetItem *sigTemp; + + for (int i = 0; i < firstDBC->messageHandler->getCount(); i++) + { + DBC_MESSAGE *thisMsg = firstDBC->messageHandler->findMsgByIdx(i); + QString msgName = thisMsg->name; + DBC_MESSAGE *otherMsg = secondDBC->messageHandler->findMsgByName(msgName); + if (!otherMsg) + { + QTreeWidgetItem *missingMsgItem = new QTreeWidgetItem(); + missingMsgItem->setText(0, msgName); + msgMissingDBCSecond->addChild(missingMsgItem); + } + else //both have Msg. Check sigs for missing + { + bool thisMsgHasMissing = false; + bool thisMsgHasMods = false; + + for (int i = 0; i < thisMsg->sigHandler->getCount(); i++) + { + DBC_SIGNAL *thisSig = thisMsg->sigHandler->findSignalByIdx(i); + QString sigName = thisSig->name; + DBC_SIGNAL *otherSig = otherMsg->sigHandler->findSignalByName(sigName); + if (!otherSig) + { + QTreeWidgetItem *missingSigItem = new QTreeWidgetItem(); + missingSigItem->setText(0, sigName); + if (!thisMsgHasMissing) + { + thisMsgHasMissing = true; + msgItem = new QTreeWidgetItem(); + msgItem->setText(0, msgName); + sigDiffTwo->addChild(msgItem); + } + msgItem->addChild(missingSigItem); + } + else //signal exists on both sides. See if it as changed position or length + { + bool didChange = false; + QTreeWidgetItem *sigItem = new QTreeWidgetItem(); + sigItem->setText(0, sigName); + if (thisSig->startBit != otherSig->startBit) + { + didChange = true; + QTreeWidgetItem *sigStartBit = new QTreeWidgetItem(); + sigStartBit->setText(0, "Start Bit First DBC: " + QString::number(thisSig->startBit) + " Second: " + QString::number(otherSig->startBit)); + sigItem->addChild(sigStartBit); + } + if (thisSig->signalSize != otherSig->signalSize) + { + didChange = true; + QTreeWidgetItem *sigSize = new QTreeWidgetItem(); + sigSize->setText(0, "Size First DBC: " + QString::number(thisSig->signalSize) + " Second: " + QString::number(otherSig->signalSize)); + sigItem->addChild(sigSize); + } + if (thisSig->bias != otherSig->bias) + { + didChange = true; + QTreeWidgetItem *sigBias = new QTreeWidgetItem(); + sigBias->setText(0, "Bias First DBC: " + QString::number(thisSig->bias) + " Second: " + QString::number(otherSig->bias)); + sigItem->addChild(sigBias); + } + if (thisSig->factor != otherSig->factor) + { + didChange = true; + QTreeWidgetItem *sigFactor = new QTreeWidgetItem(); + sigFactor->setText(0, "Factor First DBC: " + QString::number(thisSig->factor) + " Second: " + QString::number(otherSig->factor)); + sigItem->addChild(sigFactor); + } + if (didChange) + { + if (!thisMsgHasMods) + { + thisMsgHasMods = true; + sigTemp = new QTreeWidgetItem(); + sigTemp->setText(0, msgName); + sigModifiedRoot->addChild(sigTemp); + } + sigTemp->addChild(sigItem); + } + } + } + thisMsgHasMissing = false; + for (int i = 0; i < otherMsg->sigHandler->getCount(); i++) + { + DBC_SIGNAL *thisSig = otherMsg->sigHandler->findSignalByIdx(i); + QString sigName = thisSig->name; + DBC_SIGNAL *otherSig = thisMsg->sigHandler->findSignalByName(sigName); + if (!otherSig) + { + QTreeWidgetItem *missingSigItem = new QTreeWidgetItem(); + missingSigItem->setText(0, sigName); + if (!thisMsgHasMissing) + { + thisMsgHasMissing = true; + msgItem = new QTreeWidgetItem(); + msgItem->setText(0, msgName); + sigDiffOne->addChild(msgItem); + } + msgItem->addChild(missingSigItem); + } + } + } + } + + for (int i = 0; i < secondDBC->messageHandler->getCount(); i++) + { + QString msgName = secondDBC->messageHandler->findMsgByIdx(i)->name; + DBC_MESSAGE *msg = firstDBC->messageHandler->findMsgByName(msgName); + if (!msg) + { + QTreeWidgetItem *missingMsgItem = new QTreeWidgetItem(); + missingMsgItem->setText(0, msgName); + msgMissingDBCFirst->addChild(missingMsgItem); + } + } + + QSettings settings; + if (settings.value("InfoCompare/AutoExpand", false).toBool()) + { + ui->treeDetails->expandAll(); + } + + progress.cancel(); + + qApp->processEvents(); +} + +void DBCComparatorWindow::saveDetails() +{ + QString filename; + QFileDialog dialog(this); + QSettings settings; + + QStringList filters; + filters.append(QString(tr("Text File (*.txt)"))); + + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setNameFilters(filters); + dialog.setViewMode(QFileDialog::Detail); + dialog.setAcceptMode(QFileDialog::AcceptSave); + dialog.setDirectory(settings.value("FileComparator/LoadSaveDirectory", dialog.directory().path()).toString()); + + if (dialog.exec() == QDialog::Accepted) + { + filename = dialog.selectedFiles()[0]; + settings.setValue("FileComparator/LoadSaveDirectory", dialog.directory().path()); + if (!filename.contains('.')) filename += ".txt"; + QFile *outFile = new QFile(filename); + + if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) + return; + + QTreeWidget *tree = ui->treeDetails; + + + QTreeWidgetItemIterator it(tree); + while (*it) { + QTreeWidgetItem *item = *it; + QString itemText = item->text(0); + while (item->parent()) + { + outFile->write(" "); + item = item->parent(); + } + outFile->write(itemText.toUtf8() + "\n"); + ++it; + } + + outFile->close(); + + } +} + diff --git a/re/dbccomparatorwindow.h b/re/dbccomparatorwindow.h new file mode 100644 index 0000000..b05f155 --- /dev/null +++ b/re/dbccomparatorwindow.h @@ -0,0 +1,45 @@ +#ifndef DBCCOMPARATORWINDOW_H +#define DBCCOMPARATORWINDOW_H + +#include +#include +#include +#include "framefileio.h" +#include "dbc/dbc_classes.h" +#include "dbc/dbchandler.h" +#include "utility.h" + +namespace Ui { +class DBCComparatorWindow; +} + +class DBCComparatorWindow : public QDialog +{ + Q_OBJECT + +public: + explicit DBCComparatorWindow(QWidget *parent = 0); + ~DBCComparatorWindow(); + +private slots: + void loadFirstFile(); + void loadSecondFile(); + void saveDetails(); + +private: + Ui::DBCComparatorWindow *ui; + DBCFile *firstDBC; + DBCFile *secondDBC; + QString firstDBCFilename; + QString secondDBCFilename; + + void calculateDetails(); + void showEvent(QShowEvent *); + void closeEvent(QCloseEvent *event); + bool eventFilter(QObject *obj, QEvent *event); + QString loadDBC(DBCFile **file); + void readSettings(); + void writeSettings(); +}; + +#endif //DBCCOMPARATORWINDOW_H diff --git a/ui/dbccomparatorwindow.ui b/ui/dbccomparatorwindow.ui new file mode 100644 index 0000000..6f736d6 --- /dev/null +++ b/ui/dbccomparatorwindow.ui @@ -0,0 +1,131 @@ + + + DBCComparatorWindow + + + + 0 + 0 + 720 + 631 + + + + DBC Comparator + + + + + + + + + + + 16 + + + + Side 1 + + + Qt::AlignCenter + + + + + + + TextLabel + + + + + + + Load New File + + + + + + + + + Qt::Vertical + + + + + + + + + + 16 + + + + Side 2 + + + Qt::AlignCenter + + + + + + + TextLabel + + + + + + + + + Load A File + + + + + + + + + + + + + Qt::Horizontal + + + + + + + + 1 + + + + + + + + Save Details to File + + + + + + + btnDBCFile1 + btnDBCFile2 + treeDetails + btnSaveDetails + + + + diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index 46acf44..c50a4ae 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -304,6 +304,7 @@ + @@ -531,6 +532,11 @@ Temporal Graph + + + DBC Comparison + + From 6dddee0e6c2014eada8581df477bb4f36117c984 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 29 Jul 2020 21:52:12 -0400 Subject: [PATCH 49/58] Add IDs everywhere messages are reported in the DBC comparison window. --- re/dbccomparatorwindow.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/re/dbccomparatorwindow.cpp b/re/dbccomparatorwindow.cpp index b4f2cc1..57d4c1e 100644 --- a/re/dbccomparatorwindow.cpp +++ b/re/dbccomparatorwindow.cpp @@ -233,7 +233,7 @@ void DBCComparatorWindow::calculateDetails() if (!otherMsg) { QTreeWidgetItem *missingMsgItem = new QTreeWidgetItem(); - missingMsgItem->setText(0, msgName); + missingMsgItem->setText(0, msgName+ " (" + Utility::formatCANID(thisMsg->ID) + ")"); msgMissingDBCSecond->addChild(missingMsgItem); } else //both have Msg. Check sigs for missing @@ -254,7 +254,7 @@ void DBCComparatorWindow::calculateDetails() { thisMsgHasMissing = true; msgItem = new QTreeWidgetItem(); - msgItem->setText(0, msgName); + msgItem->setText(0, msgName + " (" + Utility::formatCANID(thisMsg->ID) + ")"); sigDiffTwo->addChild(msgItem); } msgItem->addChild(missingSigItem); @@ -298,7 +298,7 @@ void DBCComparatorWindow::calculateDetails() { thisMsgHasMods = true; sigTemp = new QTreeWidgetItem(); - sigTemp->setText(0, msgName); + sigTemp->setText(0, msgName + " (" + Utility::formatCANID(thisMsg->ID) + ")"); sigModifiedRoot->addChild(sigTemp); } sigTemp->addChild(sigItem); @@ -319,7 +319,7 @@ void DBCComparatorWindow::calculateDetails() { thisMsgHasMissing = true; msgItem = new QTreeWidgetItem(); - msgItem->setText(0, msgName); + msgItem->setText(0, msgName + " (" + Utility::formatCANID(thisMsg->ID) + ")"); sigDiffOne->addChild(msgItem); } msgItem->addChild(missingSigItem); @@ -330,12 +330,13 @@ void DBCComparatorWindow::calculateDetails() for (int i = 0; i < secondDBC->messageHandler->getCount(); i++) { - QString msgName = secondDBC->messageHandler->findMsgByIdx(i)->name; + DBC_MESSAGE *origMsg = secondDBC->messageHandler->findMsgByIdx(i); + QString msgName = origMsg->name; DBC_MESSAGE *msg = firstDBC->messageHandler->findMsgByName(msgName); if (!msg) { QTreeWidgetItem *missingMsgItem = new QTreeWidgetItem(); - missingMsgItem->setText(0, msgName); + missingMsgItem->setText(0, msgName + " (" + Utility::formatCANID(origMsg->ID) + ")"); msgMissingDBCFirst->addChild(missingMsgItem); } } From fbfc1f8c642146753e401425643cdc027529d7a5 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Thu, 30 Jul 2020 21:44:36 -0400 Subject: [PATCH 50/58] Now the DBC Message editor can change the parent node of a message and you can create new nodes simply by typing something new into the sender field. --- dbc/dbcmaineditor.cpp | 26 ++++++++++++++++++++++ dbc/dbcmessageeditor.cpp | 48 +++++++++++++++++++++++++++++++++++++++- dbc/dbcmessageeditor.h | 1 + ui/dbcmessageeditor.ui | 6 ++++- 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/dbc/dbcmaineditor.cpp b/dbc/dbcmaineditor.cpp index e3ea24e..2aa5825 100644 --- a/dbc/dbcmaineditor.cpp +++ b/dbc/dbcmaineditor.cpp @@ -368,6 +368,32 @@ void DBCMainEditor::updatedMessage(DBC_MESSAGE *msg) QString msgInfo = Utility::formatCANID(msg->ID) + " " + msg->name; if (msg->comment.count() > 0) msgInfo.append(" - ").append(msg->comment); item->setText(0, msgInfo); + //editor could have changed the parent Node too. Have to figure out which node + //is parent in the GUI and compare that to parent in the data. + DBC_NODE *oldParent = dbcFile->findNodeByName(item->parent()->text(0)); + if (oldParent != msg->sender) + { + qDebug() << "Changed parent of message. Trying to rehome it."; + QTreeWidgetItem *newParent = nullptr; + if (nodeToItem.contains(msg->sender)) newParent = nodeToItem.value(msg->sender); + else //new node created within message editor. Create the item and then use it + { + QTreeWidgetItem *newNodeItem = new QTreeWidgetItem(); + newNodeItem->setText(0, msg->sender->name); + newNodeItem->setIcon(0, nodeIcon); + newNodeItem->setData(0, Qt::UserRole, 1); + ui->treeDBC->addTopLevelItem(newNodeItem); + nodeToItem.insert(msg->sender, newNodeItem); + itemToNode.insert(newNodeItem, msg->sender); + newParent = newNodeItem; + } + + QTreeWidgetItem *prevParent = nodeToItem.value(oldParent); + prevParent->removeChild(item); + newParent->addChild(item); + ui->treeDBC->setCurrentItem(item); + ui->treeDBC->sortItems(0, Qt::AscendingOrder); //resort because we just moved an item + } } else qDebug() << "That mesage doesn't exist. That's a bug dude."; } diff --git a/dbc/dbcmessageeditor.cpp b/dbc/dbcmessageeditor.cpp index 8b3a426..2366696 100644 --- a/dbc/dbcmessageeditor.cpp +++ b/dbc/dbcmessageeditor.cpp @@ -17,11 +17,13 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : dbcHandler = DBCHandler::getReference(); dbcMessage = nullptr; + suppressEditCallbacks = false; connect(ui->lineComment, &QLineEdit::editingFinished, [=]() { if (dbcMessage == nullptr) return; + if (suppressEditCallbacks) return; if (dbcMessage->comment != ui->lineComment->text()) dbcFile->setDirtyFlag(); dbcMessage->comment = ui->lineComment->text(); emit updatedTreeInfo(dbcMessage); @@ -31,6 +33,7 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : [=]() { if (dbcMessage == nullptr) return; + if (suppressEditCallbacks) return; if (dbcMessage->ID != Utility::ParseStringToNum(ui->lineFrameID->text())) dbcFile->setDirtyFlag(); dbcMessage->ID = Utility::ParseStringToNum(ui->lineFrameID->text()); emit updatedTreeInfo(dbcMessage); @@ -40,6 +43,7 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : [=]() { if (dbcMessage == nullptr) return; + if (suppressEditCallbacks) return; if (dbcMessage->name != ui->lineMsgName->text().simplified().replace(' ', '_')) dbcFile->setDirtyFlag(); dbcMessage->name = ui->lineMsgName->text().simplified().replace(' ', '_'); emit updatedTreeInfo(dbcMessage); @@ -49,13 +53,47 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : [=]() { if (dbcMessage == nullptr) return; + if (suppressEditCallbacks) return; if (dbcMessage->len != Utility::ParseStringToNum(ui->lineFrameLen->text())) dbcFile->setDirtyFlag(); dbcMessage->len = Utility::ParseStringToNum(ui->lineFrameLen->text()); }); + connect(ui->comboSender, &QComboBox::currentTextChanged, + [=](const QString newText) + { + if (dbcMessage == nullptr) return; + if (suppressEditCallbacks) return; + DBC_NODE *node = dbcFile->findNodeByName(newText); + if (!node) return; + if (node != dbcMessage->sender) dbcFile->setDirtyFlag(); + dbcMessage->sender = node; + emit updatedTreeInfo(dbcMessage); + }); + + connect(ui->comboSender->lineEdit(), &QLineEdit::editingFinished, + [=]() + { + if (dbcMessage == nullptr) return; + if (suppressEditCallbacks) return; + QString newText = ui->comboSender->currentText(); + DBC_NODE *node = dbcFile->findNodeByName(newText); + if (!node) + { + DBC_NODE newNode; + newNode.name = newText; + dbcFile->dbc_nodes.append(newNode); + node = dbcFile->findNodeByName(newText); + ui->comboSender->addItem(newText); + } + if (node != dbcMessage->sender) dbcFile->setDirtyFlag(); + dbcMessage->sender = node; + emit updatedTreeInfo(dbcMessage); + }); + connect(ui->btnTextColor, &QAbstractButton::clicked, [=]() { + if (suppressEditCallbacks) return; QColor newColor = QColorDialog::getColor(dbcMessage->fgColor); if (dbcMessage->fgColor != newColor) dbcFile->setDirtyFlag(); dbcMessage->fgColor = newColor; @@ -77,6 +115,7 @@ DBCMessageEditor::DBCMessageEditor(QWidget *parent) : connect(ui->btnBackgroundColor, &QAbstractButton::clicked, [=]() { + if (suppressEditCallbacks) return; QColor newColor = QColorDialog::getColor(dbcMessage->bgColor); if (dbcMessage->bgColor != newColor) dbcFile->setDirtyFlag(); dbcMessage->bgColor = newColor; @@ -133,10 +172,13 @@ void DBCMessageEditor::setFileIdx(int idx) if (idx < 0 || idx > dbcHandler->getFileCount() - 1) return; dbcFile = dbcHandler->getFileByIdx(idx); + suppressEditCallbacks = true; + ui->comboSender->clear(); for (int x = 0; x < dbcFile->dbc_nodes.count(); x++) { ui->comboSender->addItem(dbcFile->dbc_nodes[x].name); } + suppressEditCallbacks = false; } void DBCMessageEditor::readSettings() @@ -175,10 +217,12 @@ void DBCMessageEditor::showEvent(QShowEvent* event) void DBCMessageEditor::refreshView() { + suppressEditCallbacks = true; + ui->lineComment->setText(dbcMessage->comment); ui->lineFrameID->setText(Utility::formatCANID(dbcMessage->ID)); ui->lineMsgName->setText(dbcMessage->name); - ui->lineFrameLen->setText(QString::number(dbcMessage->len)); + ui->lineFrameLen->setText(QString::number(dbcMessage->len)); for (int i = 0; i < ui->comboSender->count(); i++) { if (ui->comboSender->itemText(i) == dbcMessage->sender->name) @@ -188,6 +232,8 @@ void DBCMessageEditor::refreshView() } } + suppressEditCallbacks = false; + generateSampleText(); } diff --git a/dbc/dbcmessageeditor.h b/dbc/dbcmessageeditor.h index 5388466..473b54d 100644 --- a/dbc/dbcmessageeditor.h +++ b/dbc/dbcmessageeditor.h @@ -30,6 +30,7 @@ private: DBCHandler *dbcHandler; DBC_MESSAGE *dbcMessage; DBCFile *dbcFile; + bool suppressEditCallbacks; void closeEvent(QCloseEvent *event); bool eventFilter(QObject *obj, QEvent *event); diff --git a/ui/dbcmessageeditor.ui b/ui/dbcmessageeditor.ui index c4b3a2a..7ce6554 100644 --- a/ui/dbcmessageeditor.ui +++ b/ui/dbcmessageeditor.ui @@ -78,7 +78,11 @@
- + + + true + + From ee287eac4b98cbe285a929a34811120ce5f73485 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 5 Aug 2020 20:24:02 -0400 Subject: [PATCH 51/58] Add two new external libraries. SimpleCrypt is used to encrypt passwords, QMQTT is an MQTT library that I was able to integrate into the codebase so there aren't any external dependencies. Both external projects have their own open source licenses. Those licenses are included. Also in this commit is some preliminary work to start adding MQTT support to SavvyCAN. --- SavvyCAN.pro | 36 +- connections/canconconst.h | 1 + connections/newconnectiondialog.cpp | 13 + connections/newconnectiondialog.h | 1 + mainsettingsdialog.cpp | 23 +- mqtt/LICENSE | 2 + mqtt/README.md | 139 +++++ mqtt/edl-v10 | 30 + mqtt/epl-v10 | 220 ++++++++ mqtt/qmqtt.h | 38 ++ mqtt/qmqtt.pri | 48 ++ mqtt/qmqtt.pro | 17 + mqtt/qmqtt.qbs | 134 +++++ mqtt/qmqtt_client.cpp | 412 ++++++++++++++ mqtt/qmqtt_client.h | 276 ++++++++++ mqtt/qmqtt_client_p.cpp | 811 ++++++++++++++++++++++++++++ mqtt/qmqtt_client_p.h | 183 +++++++ mqtt/qmqtt_frame.cpp | 209 +++++++ mqtt/qmqtt_frame.h | 137 +++++ mqtt/qmqtt_global.h | 48 ++ mqtt/qmqtt_message.cpp | 136 +++++ mqtt/qmqtt_message.h | 96 ++++ mqtt/qmqtt_message_p.h | 83 +++ mqtt/qmqtt_network.cpp | 295 ++++++++++ mqtt/qmqtt_network_p.h | 139 +++++ mqtt/qmqtt_networkinterface.h | 92 ++++ mqtt/qmqtt_routedmessage.h | 71 +++ mqtt/qmqtt_router.cpp | 67 +++ mqtt/qmqtt_router.h | 60 ++ mqtt/qmqtt_routesubscription.cpp | 117 ++++ mqtt/qmqtt_routesubscription.h | 79 +++ mqtt/qmqtt_socket.cpp | 81 +++ mqtt/qmqtt_socket_p.h | 69 +++ mqtt/qmqtt_socketinterface.h | 84 +++ mqtt/qmqtt_ssl_socket.cpp | 112 ++++ mqtt/qmqtt_ssl_socket_p.h | 79 +++ mqtt/qmqtt_timer.cpp | 73 +++ mqtt/qmqtt_timer_p.h | 62 +++ mqtt/qmqtt_timerinterface.h | 62 +++ mqtt/qmqtt_websocket.cpp | 103 ++++ mqtt/qmqtt_websocket_p.h | 102 ++++ mqtt/qmqtt_websocketiodevice.cpp | 48 ++ mqtt/qmqtt_websocketiodevice_p.h | 86 +++ simplecrypt.cpp | 252 +++++++++ simplecrypt.h | 225 ++++++++ ui/mainsettingsdialog.ui | 36 +- ui/newconnectiondialog.ui | 15 +- 47 files changed, 5482 insertions(+), 20 deletions(-) create mode 100644 mqtt/LICENSE create mode 100644 mqtt/README.md create mode 100644 mqtt/edl-v10 create mode 100644 mqtt/epl-v10 create mode 100644 mqtt/qmqtt.h create mode 100644 mqtt/qmqtt.pri create mode 100644 mqtt/qmqtt.pro create mode 100644 mqtt/qmqtt.qbs create mode 100644 mqtt/qmqtt_client.cpp create mode 100644 mqtt/qmqtt_client.h create mode 100644 mqtt/qmqtt_client_p.cpp create mode 100644 mqtt/qmqtt_client_p.h create mode 100644 mqtt/qmqtt_frame.cpp create mode 100644 mqtt/qmqtt_frame.h create mode 100644 mqtt/qmqtt_global.h create mode 100644 mqtt/qmqtt_message.cpp create mode 100644 mqtt/qmqtt_message.h create mode 100644 mqtt/qmqtt_message_p.h create mode 100644 mqtt/qmqtt_network.cpp create mode 100644 mqtt/qmqtt_network_p.h create mode 100644 mqtt/qmqtt_networkinterface.h create mode 100644 mqtt/qmqtt_routedmessage.h create mode 100644 mqtt/qmqtt_router.cpp create mode 100644 mqtt/qmqtt_router.h create mode 100644 mqtt/qmqtt_routesubscription.cpp create mode 100644 mqtt/qmqtt_routesubscription.h create mode 100644 mqtt/qmqtt_socket.cpp create mode 100644 mqtt/qmqtt_socket_p.h create mode 100644 mqtt/qmqtt_socketinterface.h create mode 100644 mqtt/qmqtt_ssl_socket.cpp create mode 100644 mqtt/qmqtt_ssl_socket_p.h create mode 100644 mqtt/qmqtt_timer.cpp create mode 100644 mqtt/qmqtt_timer_p.h create mode 100644 mqtt/qmqtt_timerinterface.h create mode 100644 mqtt/qmqtt_websocket.cpp create mode 100644 mqtt/qmqtt_websocket_p.h create mode 100644 mqtt/qmqtt_websocketiodevice.cpp create mode 100644 mqtt/qmqtt_websocketiodevice_p.h create mode 100644 simplecrypt.cpp create mode 100644 simplecrypt.h diff --git a/SavvyCAN.pro b/SavvyCAN.pro index a42ad47..b946b25 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -4,11 +4,12 @@ # #------------------------------------------------- -QT = core gui printsupport qml serialbus serialport widgets help +QT = core gui printsupport qml serialbus serialport widgets help network CONFIG(release, debug|release):DEFINES += QT_NO_DEBUG_OUTPUT CONFIG += c++11 +CONFIG += NO_UNIT_TESTS DEFINES += QCUSTOMPLOT_USE_OPENGL @@ -16,9 +17,22 @@ TARGET = SavvyCAN TEMPLATE = app SOURCES += main.cpp\ + mqtt/qmqtt_client.cpp \ + mqtt/qmqtt_client_p.cpp \ + mqtt/qmqtt_frame.cpp \ + mqtt/qmqtt_message.cpp \ + mqtt/qmqtt_network.cpp \ + mqtt/qmqtt_router.cpp \ + mqtt/qmqtt_routesubscription.cpp \ + mqtt/qmqtt_socket.cpp \ + mqtt/qmqtt_ssl_socket.cpp \ + mqtt/qmqtt_timer.cpp \ + mqtt/qmqtt_websocket.cpp \ + mqtt/qmqtt_websocketiodevice.cpp \ re/dbccomparatorwindow.cpp \ mainwindow.cpp \ canframemodel.cpp \ + simplecrypt.cpp \ utility.cpp \ qcustomplot.cpp \ frameplaybackwindow.cpp \ @@ -77,7 +91,27 @@ SOURCES += main.cpp\ HEADERS += mainwindow.h \ can_structs.h \ canframemodel.h \ \ + mqtt/qmqtt.h \ + mqtt/qmqtt_client.h \ + mqtt/qmqtt_client_p.h \ + mqtt/qmqtt_frame.h \ + mqtt/qmqtt_global.h \ + mqtt/qmqtt_message.h \ + mqtt/qmqtt_message_p.h \ + mqtt/qmqtt_network_p.h \ + mqtt/qmqtt_networkinterface.h \ + mqtt/qmqtt_routedmessage.h \ + mqtt/qmqtt_router.h \ + mqtt/qmqtt_routesubscription.h \ + mqtt/qmqtt_socket_p.h \ + mqtt/qmqtt_socketinterface.h \ + mqtt/qmqtt_ssl_socket_p.h \ + mqtt/qmqtt_timer_p.h \ + mqtt/qmqtt_timerinterface.h \ + mqtt/qmqtt_websocket_p.h \ + mqtt/qmqtt_websocketiodevice_p.h \ re/dbccomparatorwindow.h \ + simplecrypt.h \ utility.h \ qcustomplot.h \ frameplaybackwindow.h \ diff --git a/connections/canconconst.h b/connections/canconconst.h index f70ebd9..71dc3fd 100644 --- a/connections/canconconst.h +++ b/connections/canconconst.h @@ -18,6 +18,7 @@ namespace CANCon { KVASER, SERIALBUS, REMOTE, + MQTT, NONE }; } diff --git a/connections/newconnectiondialog.cpp b/connections/newconnectiondialog.cpp index 1f03eba..d2bd9e7 100644 --- a/connections/newconnectiondialog.cpp +++ b/connections/newconnectiondialog.cpp @@ -25,6 +25,8 @@ NewConnectionDialog::NewConnectionDialog(QVector* ips, QWidget *parent) connect(ui->rbGVRET, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged); connect(ui->rbSocketCAN, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged); connect(ui->rbRemote, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged); + connect(ui->rbMQTT, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged); + connect(ui->cbDeviceType, QOverload::of(&QComboBox::currentIndexChanged), this, &NewConnectionDialog::handleDeviceTypeChanged); connect(ui->btnOK, &QPushButton::clicked, this, &NewConnectionDialog::handleCreateButton); @@ -50,6 +52,7 @@ void NewConnectionDialog::handleConnTypeChanged() if (ui->rbGVRET->isChecked()) selectSerial(); if (ui->rbSocketCAN->isChecked()) selectSocketCan(); if (ui->rbRemote->isChecked()) selectRemote(); + } void NewConnectionDialog::handleDeviceTypeChanged() @@ -102,6 +105,14 @@ void NewConnectionDialog::selectRemote() } } +void NewConnectionDialog::selectMQTT() +{ + ui->lPort->setText("Topic Name:"); + ui->lblDeviceType->setHidden(true); + ui->cbDeviceType->setHidden(true); + ui->cbPort->clear(); +} + void NewConnectionDialog::setPortName(CANCon::type pType, QString pPortName, QString pDriver) { @@ -156,6 +167,7 @@ QString NewConnectionDialog::getPortName() case CANCon::GVRET_SERIAL: case CANCon::SERIALBUS: case CANCon::REMOTE: + case CANCon::MQTT: return ui->cbPort->currentText(); default: qDebug() << "getPortName: can't get port"; @@ -178,6 +190,7 @@ CANCon::type NewConnectionDialog::getConnectionType() if (ui->rbGVRET->isChecked()) return CANCon::GVRET_SERIAL; if (ui->rbSocketCAN->isChecked()) return CANCon::SERIALBUS; if (ui->rbRemote->isChecked()) return CANCon::REMOTE; + if (ui->rbMQTT->isChecked()) return CANCon::MQTT; qDebug() << "getConnectionType: error"; return CANCon::NONE; diff --git a/connections/newconnectiondialog.h b/connections/newconnectiondialog.h index f826b70..52b43e5 100644 --- a/connections/newconnectiondialog.h +++ b/connections/newconnectiondialog.h @@ -40,6 +40,7 @@ private: void selectKvaser(); void selectSocketCan(); void selectRemote(); + void selectMQTT(); bool isSerialBusAvailable(); void setPortName(CANCon::type pType, QString pPortName, QString pDriver); }; diff --git a/mainsettingsdialog.cpp b/mainsettingsdialog.cpp index 149fef0..dd1764f 100644 --- a/mainsettingsdialog.cpp +++ b/mainsettingsdialog.cpp @@ -1,8 +1,13 @@ #include "mainsettingsdialog.h" #include "ui_mainsettingsdialog.h" #include "helpwindow.h" - #include +#include "simplecrypt.h" + +//using this simple encryption library to obfuscate stored password a bit. It's not super secure but better than +//storing a password in straight plaintext. You have the source to this application anyway, whatever algorithm used, +//whatever key, you'd see it. Just behave yourselves +SimpleCrypt crypto(Q_UINT64_C(0xdeadbeefface6285)); MainSettingsDialog::MainSettingsDialog(QWidget *parent) : QDialog(parent), @@ -11,6 +16,7 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) : QSettings settings; ui->setupUi(this); + //TODO: This is still hard coded to support only two buses. Sometimes there is none, sometimes 1, sometimes much more than 2. Fix this. ui->comboSendingBus->addItem(tr("None")); ui->comboSendingBus->addItem(tr("0")); ui->comboSendingBus->addItem(tr("1")); @@ -30,8 +36,12 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) : ui->spinPlaybackSpeed->setValue(settings.value("Playback/DefSpeed", 5).toInt()); ui->lineClockFormat->setText(settings.value("Main/TimeFormat", "MMM-dd HH:mm:ss.zzz").toString()); ui->lineRemoteHost->setText(settings.value("Remote/Host", "api.savvycan.com").toString()); - ui->lineRemotePort->setText(settings.value("Remote/Port", "21315").toString()); // = 0x5343 = SC. Yep, really creative port number - ui->cbAutoStartRemote->setChecked(settings.value("Remote/AutoStart", false).toBool()); + ui->lineRemotePort->setText(settings.value("Remote/Port", "8883").toString()); //default port for SSL enabled MQTT + ui->lineRemoteUser->setText(settings.value("Remote/User", "Anonymous").toString()); + QByteArray encPass = settings.value("Remote/Pass", "").toByteArray(); + QString decPass = crypto.decryptToString(encPass); + ui->lineRemotePassword->setText(decPass); + ui->cbLoadConnections->setChecked(settings.value("Main/SaveRestoreConnections", false).toBool()); ui->spinFontSize->setValue(settings.value("Main/FontSize", ui->cbDisplayHex->font().pointSize()).toUInt()); @@ -82,9 +92,10 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) : connect(ui->cbUseFiltered, SIGNAL(toggled(bool)), this, SLOT(updateSettings())); connect(ui->lineClockFormat, SIGNAL(editingFinished()), this, SLOT(updateSettings())); connect(ui->cbUseOpenGL, SIGNAL(toggled(bool)), this, SLOT(updateSettings())); - connect(ui->cbAutoStartRemote, SIGNAL(toggled(bool)), this, SLOT(updateSettings())); connect(ui->lineRemoteHost, SIGNAL(editingFinished()), this, SLOT(updateSettings())); connect(ui->lineRemotePort, SIGNAL(editingFinished()), this, SLOT(updateSettings())); + connect(ui->lineRemoteUser, SIGNAL(editingFinished()), this, SLOT(updateSettings())); + connect(ui->lineRemotePassword, SIGNAL(editingFinished()), this, SLOT(updateSettings())); connect(ui->cbLoadConnections, SIGNAL(toggled(bool)), this, SLOT(updateSettings())); connect(ui->cbFilterLabeling, SIGNAL(toggled(bool)), this, SLOT(updateSettings())); installEventFilter(this); @@ -144,7 +155,9 @@ void MainSettingsDialog::updateSettings() settings.setValue("Main/FontSize", ui->spinFontSize->value()); settings.setValue("Remote/Host", ui->lineRemoteHost->text()); settings.setValue("Remote/Port", ui->lineRemotePort->text()); - settings.setValue("Remote/AutoStart", ui->cbAutoStartRemote->isChecked()); + settings.setValue("Remote/User", ui->lineRemoteUser->text()); + QByteArray encPass = crypto.encryptToByteArray(ui->lineRemotePassword->text()); + settings.setValue("Remote/Pass", encPass); settings.setValue("Main/FilterLabeling", ui->cbFilterLabeling->isChecked()); settings.sync(); diff --git a/mqtt/LICENSE b/mqtt/LICENSE new file mode 100644 index 0000000..56bef4e --- /dev/null +++ b/mqtt/LICENSE @@ -0,0 +1,2 @@ +This project is dual licensed under the Eclipse Public License 1.0 and the +Eclipse Distribution License 1.0 as described in the epl-v10 and edl-v10 files. diff --git a/mqtt/README.md b/mqtt/README.md new file mode 100644 index 0000000..ba0068b --- /dev/null +++ b/mqtt/README.md @@ -0,0 +1,139 @@ +QMQTT +===== + +mqtt client for Qt + +**Please compile the library with Qt >= 5.3 version. On Windows you need to specify `CONFIG += NO_UNIT_TESTS`, since gtest is not supported.** + +SSL is enabled by default, if the version of OpenSSL < 1.0.2, SSL may not be supported. + +Disable the SSL in CMakeLists.txt (cmake): + + option( ${PROJECT_NAME}_SSL "Enable SSL support for MQTT" OFF ) + +Disable the SSL with src/mqtt/qmqtt.pro (qmake): + + CONFIG+=QMQTT_NO_SSL + +To add websocket support, compile the library with Qt >= 5.7, and specify 'CONFIG += QMQTT_WEBSOCKETS'. +This also works when compiling qmqtt for WebAssembly. + +Usage +===== + +In your QMake project, add: + + QT += qmqtt + +Connect using TCP: + + #include "qmqtt.h" + + QMQTT::Client *client = new QMQTT::Client(QHostAddress::LocalHost, 1883); + client->setClientId("clientId"); + client->setUsername("user"); + client->setPassword("password"); + client->connectToHost(); + +Connect using SSL: + + QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); + // Add custom SSL options here (for example extra certificates) + QMQTT::Client *client = new QMQTT::Client("example.com", 8883, sslConfig); + client->setClientId("clientId"); + client->setUsername("user"); + client->setPassword("password"); + // Optionally, set ssl errors you want to ignore. Be careful, because this may weaken security. + // See QSslSocket::ignoreSslErrors(const QList &) for more information. + client->ignoreSslErrors() + client->connectToHost(); + // Here's another option to suppress SSL errors (again, be careful) + QObject::connect(client, &QMQTT::Client::sslErrors, [&](const QList &errors) { + // Investigate the errors here, if you find no serious problems, call ignoreSslErrors() + // to continue connecting. + client->ignoreSslErrors(); + }); + +Connect using WebSockets: + + QMQTT::Client *client = new QMQTT::Client("ws://www.example.com/mqtt", "", QWebSocketProtocol::VersionLatest); + client->setClientId("clientId"); + client->setUsername("user"); + client->setPassword("password"); + client->connectToHost(); + +Slots +===== + + void setHost(const QHostAddress& host); + void setPort(const quint16 port); + void setClientId(const QString& clientId); + void setUsername(const QString& username); + void setPassword(const QString& password); + void setKeepAlive(const int keepAlive); + void setCleanSession(const bool cleansess); + void setAutoReconnect(const bool value); + void setAutoReconnectInterval(const int autoReconnectInterval); + void setWillTopic(const QString& willTopic); + void setWillQos(const quint8 willQos); + void setWillRetain(const bool willRetain); + void setWillMessage(const QString& willMessage); + + void connectToHost(); + void disconnectFromHost(); + + quint16 subscribe(const QString& topic, const quint8 qos); + void unsubscribe(const QString& topic); + + quint16 publish(const Message& message); + +Signals +======= + + void connected(); + void disconnected(); + void error(const QMQTT::ClientError error); + + void subscribed(const QString& topic, const quint8 qos); + void unsubscribed(const QString& topic); + void published(const quint16 msgid, const quint8 qos); + void pingresp(); + void received(const QMQTT::Message& message); + + +License +======= + +New BSD License + + +Contributors +============= + +[@Kampfgnom](https://github.com/Kampfgnom) + +[@rafaeldelucena](https://github.com/rafaeldelucena) + +[@Vortex375](https://github.com/Vortex375) + +[@mwallnoefer](https://github.com/mwallnoefer) + +[@KonstantinRitt](https://github.com/KonstantinRitt) + +[@cncap](https://github.com/cncap) + +[@Psy-Kai](https://github.com/Psy-Kai) + +[@ejvr](https://github.com/ejvr) + +[@keytee](https://github.com/keytee) + + +Authors +======= + +[@emqplus](https://github.com/emqplus) Feng Lee + +[@wguynes](https://github.com/wguynes) William Guynes + +[@wuming123057](https://github.com/wuming123057) Xuan diff --git a/mqtt/edl-v10 b/mqtt/edl-v10 new file mode 100644 index 0000000..d36ee54 --- /dev/null +++ b/mqtt/edl-v10 @@ -0,0 +1,30 @@ +Eclipse Distribution License - v 1.0 + +Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + Neither the name of the Eclipse Foundation, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/mqtt/epl-v10 b/mqtt/epl-v10 new file mode 100644 index 0000000..4ec0f61 --- /dev/null +++ b/mqtt/epl-v10 @@ -0,0 +1,220 @@ +Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + +1. DEFINITIONS + +"Contribution" means: + a) in the case of the initial Contributor, the initial code and + documentation distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + + i) changes to the Program, and + ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are +distributed by that particular Contributor. A Contribution 'originates' from a +Contributor if it was added to the Program by such Contributor itself or anyone +acting on such Contributor's behalf. Contributions do not include additions to +the Program which: (i) are separate modules of software distributed in +conjunction with the Program under their own license agreement, and (ii) are +not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which are +necessarily infringed by the use or sale of its Contribution alone or when +combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free copyright license to + reproduce, prepare derivative works of, publicly display, publicly + perform, distribute and sublicense the Contribution of such Contributor, + if any, and such derivative works, in source code and object code form. + + b) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free patent license under + Licensed Patents to make, use, sell, offer to sell, import and otherwise + transfer the Contribution of such Contributor, if any, in source code and + object code form. This patent license shall apply to the combination of the + Contribution and the Program if, at the time the Contribution is added by the + Contributor, such addition of the Contribution causes such combination to be + covered by the Licensed Patents. The patent license shall not apply to any + other combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the licenses + to its Contributions set forth herein, no assurances are provided by any + Contributor that the Program does not infringe the patent or other + intellectual property rights of any other entity. Each Contributor disclaims + any liability to Recipient for claims brought by any other entity based on + infringement of intellectual property rights or otherwise. As a condition to + exercising the rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual property rights + needed, if any. For example, if a third party patent license is required to + allow Recipient to distribute the Program, it is Recipient's responsibility + to acquire that license before distributing the Program. + + d) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright license + set forth in this Agreement. + + +3. REQUIREMENTS + + A Contributor may choose to distribute the Program in object code form under + its own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + + b) its license agreement: + + i) effectively disclaims on behalf of all Contributors all warranties and + conditions, express and implied, including warranties or conditions of + title and non-infringement, and implied warranties or conditions of + merchantability and fitness for a particular purpose; + + ii) effectively excludes on behalf of all Contributors all liability for + damages, including direct, indirect, special, incidental and consequential + damages, such as lost profits; + + iii) states that any provisions which differ from this Agreement are offered + by that Contributor alone and not by any other party; and + + iv) states that source code for the Program is available from such + Contributor, and informs licensees how to obtain it in a reasonable manner + on or through a medium customarily used for software exchange. + + When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + + b) a copy of this Agreement must be included with each copy of the Program. + + Contributors may not remove or alter any copyright notices contained within + the Program. + + Each Contributor must identify itself as the originator of its Contribution, + if any, in a manner that reasonably allows subsequent Recipients to identify + the originator of the Contribution. + + +4. COMMERCIAL DISTRIBUTION + + Commercial distributors of software may accept certain responsibilities with + respect to end users, business partners and the like. While this license is + intended to facilitate the commercial use of the Program, the Contributor who + includes the Program in a commercial product offering should do so in a + manner which does not create potential liability for other Contributors. + Therefore, if a Contributor includes the Program in a commercial product + offering, such Contributor ("Commercial Contributor") hereby agrees to defend + and indemnify every other Contributor ("Indemnified Contributor") against any + losses, damages and costs (collectively "Losses") arising from claims, + lawsuits and other legal actions brought by a third party against the + Indemnified Contributor to the extent caused by the acts or omissions of such + Commercial Contributor in connection with its distribution of the Program in + a commercial product offering. The obligations in this section do not apply + to any claims or Losses relating to any actual or alleged intellectual + property infringement. In order to qualify, an Indemnified Contributor must: + a) promptly notify the Commercial Contributor in writing of such claim, and + b) allow the Commercial Contributor to control, and cooperate with the + Commercial Contributor in, the defense and any related settlement + negotiations. The Indemnified Contributor may participate in any such claim + at its own expense. + + For example, a Contributor might include the Program in a commercial product + offering, Product X. That Contributor is then a Commercial Contributor. If + that Commercial Contributor then makes performance claims, or offers + warranties related to Product X, those performance claims and warranties are + such Commercial Contributor's responsibility alone. Under this section, the + Commercial Contributor would have to defend claims against the other + Contributors related to those performance claims and warranties, and if a + court requires any other Contributor to pay any damages as a result, the + Commercial Contributor must pay those damages. + + +5. NO WARRANTY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON + AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER + EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR + CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A + PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the + appropriateness of using and distributing the Program and assumes all risks + associated with its exercise of rights under this Agreement , including but + not limited to the risks and costs of program errors, compliance with + applicable laws, damage to or loss of data, programs or equipment, and + unavailability or interruption of operations. + + +6. DISCLAIMER OF LIABILITY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY + CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION + LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE + EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGES. + + +7. GENERAL + + If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of the + remainder of the terms of this Agreement, and without further action by the + parties hereto, such provision shall be reformed to the minimum extent + necessary to make such provision valid and enforceable. + + If Recipient institutes patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Program itself + (excluding combinations of the Program with other software or hardware) + infringes such Recipient's patent(s), then such Recipient's rights granted + under Section 2(b) shall terminate as of the date such litigation is filed. + + All Recipient's rights under this Agreement shall terminate if it fails to + comply with any of the material terms or conditions of this Agreement and + does not cure such failure in a reasonable period of time after becoming + aware of such noncompliance. If all Recipient's rights under this Agreement + terminate, Recipient agrees to cease use and distribution of the Program as + soon as reasonably practicable. However, Recipient's obligations under this + Agreement and any licenses granted by Recipient relating to the Program shall + continue and survive. + + Everyone is permitted to copy and distribute copies of this Agreement, but in + order to avoid inconsistency the Agreement is copyrighted and may only be + modified in the following manner. The Agreement Steward reserves the right to + publish new versions (including revisions) of this Agreement from time to + time. No one other than the Agreement Steward has the right to modify this + Agreement. The Eclipse Foundation is the initial Agreement Steward. The + Eclipse Foundation may assign the responsibility to serve as the Agreement + Steward to a suitable separate entity. Each new version of the Agreement will + be given a distinguishing version number. The Program (including + Contributions) may always be distributed subject to the version of the + Agreement under which it was received. In addition, after a new version of + the Agreement is published, Contributor may elect to distribute the Program + (including its Contributions) under the new version. Except as expressly + stated in Sections 2(a) and 2(b) above, Recipient receives no rights or + licenses to the intellectual property of any Contributor under this + Agreement, whether expressly, by implication, estoppel or otherwise. All + rights in the Program not expressly granted under this Agreement are + reserved. + + This Agreement is governed by the laws of the State of New York and the + intellectual property laws of the United States of America. No party to this + Agreement will bring a legal action under this Agreement more than one year + after the cause of action arose. Each party waives its rights to a jury trial + in any resulting litigation. \ No newline at end of file diff --git a/mqtt/qmqtt.h b/mqtt/qmqtt.h new file mode 100644 index 0000000..6e607dd --- /dev/null +++ b/mqtt/qmqtt.h @@ -0,0 +1,38 @@ +/* + * qmqtt.h - qmqtt library heaer + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_H +#define QMQTT_H + +#include +#include + +#endif // QMQTT_H diff --git a/mqtt/qmqtt.pri b/mqtt/qmqtt.pri new file mode 100644 index 0000000..c5790bf --- /dev/null +++ b/mqtt/qmqtt.pri @@ -0,0 +1,48 @@ +INCLUDEPATH += $$PWD + +PUBLIC_HEADERS += \ + $$PWD/qmqtt_client.h \ + $$PWD/qmqtt_frame.h \ + $$PWD/qmqtt_message.h \ + $$PWD/qmqtt_routesubscription.h \ + $$PWD/qmqtt_routedmessage.h \ + $$PWD/qmqtt_router.h \ + $$PWD/qmqtt_networkinterface.h \ + $$PWD/qmqtt_socketinterface.h \ + $$PWD/qmqtt_timerinterface.h + +PRIVATE_HEADERS += \ + $$PWD/qmqtt_client_p.h \ + $$PWD/qmqtt_message_p.h \ + $$PWD/qmqtt_network_p.h \ + $$PWD/qmqtt_socket_p.h \ + $$PWD/qmqtt_timer_p.h + +SOURCES += \ + $$PWD/qmqtt_client_p.cpp \ + $$PWD/qmqtt_client.cpp \ + $$PWD/qmqtt_frame.cpp \ + $$PWD/qmqtt_message.cpp \ + $$PWD/qmqtt_network.cpp \ + $$PWD/qmqtt_routesubscription.cpp \ + $$PWD/qmqtt_router.cpp \ + $$PWD/qmqtt_socket.cpp \ + $$PWD/qmqtt_timer.cpp + +QMQTT_WEBSOCKETS { + PRIVATE_HEADERS += \ + $$PWD/qmqtt_websocket_p.h \ + $$PWD/qmqtt_websocketiodevice_p.h + + SOURCES += \ + $$PWD/qmqtt_websocket.cpp \ + $$PWD/qmqtt_websocketiodevice.cpp +} + +!contains(CONFIG, QMQTT_NO_SSL) { + PRIVATE_HEADERS += \ + $$PWD/qmqtt_ssl_socket_p.h + + SOURCES += \ + $$PWD/qmqtt_ssl_socket.cpp +} diff --git a/mqtt/qmqtt.pro b/mqtt/qmqtt.pro new file mode 100644 index 0000000..4862266 --- /dev/null +++ b/mqtt/qmqtt.pro @@ -0,0 +1,17 @@ +TARGET = QtQmqtt +QT = core network +qtHaveModule(websockets): QMQTT_WEBSOCKETS: QT += websockets + +DEFINES += QT_NO_CAST_TO_ASCII QT_NO_CAST_FROM_ASCII + +# CONFIG += QMQTT_NO_SSL + +HEADERS += \ + $$PWD/qmqtt_global.h \ + $$PWD/qmqtt.h + +include(qmqtt.pri) + +HEADERS += $$PUBLIC_HEADERS $$PRIVATE_HEADERS + +load(qt_module) diff --git a/mqtt/qmqtt.qbs b/mqtt/qmqtt.qbs new file mode 100644 index 0000000..b3d0f62 --- /dev/null +++ b/mqtt/qmqtt.qbs @@ -0,0 +1,134 @@ +import qbs +import qbs.TextFile +import qbs.FileInfo + +Product { + name: "qmqtt" + type: [libraryType] + property bool webSocketSupport: false + property string libraryType: "dynamiclibrary" + targetName: "qmqtt" + version: "1.0.0" + + cpp.defines: [ + "QT_BUILD_QMQTT_LIB", + "QT_NO_CAST_TO_ASCII", + "QT_NO_CAST_FROM_ASCII", + ] + + cpp.includePaths: sourceDirectory + + files: [ + "*.cpp" + ] + + Group { + name: "Public Headers" + fileTags: [ + "hpp", + "public_headers" + ] + files: [ + "*.h" + ] + excludeFiles: privateHeaders.files + } + + Group { + id: privateHeaders + name: "Private Headers" + fileTags: [ + "hpp", + "private_headers", + ] + files: [ + "*_p.h" + ] + } + + Group { + fileTagsFilter: libraryType + qbs.install: true + qbs.installDir: "lib" + } + + Group { + fileTagsFilter: "public_headers" + qbs.install: true + qbs.installDir: FileInfo.joinPaths("include", product.name) + } + + Group { + fileTagsFilter: "private_headers" + qbs.install: true + qbs.installDir: FileInfo.joinPaths("include", product.name, version, product.name) + } + + Group { + fileTagsFilter: "Exporter.qbs.module" + qbs.installDir: FileInfo.joinPaths("qbs", "modules", product.name, "private") + qbs.install: true + } + + Group { + fileTagsFilter: "Exporter.pkgconfig.pc" + qbs.installDir: FileInfo.joinPaths("lib" ,"pkgconfig") + qbs.install: true + } + + Depends { + name: "cpp" + } + + Depends { + name: "Qt" + property var baseModules: ["core", "network"] + + Properties { + condition: webSocketSupport + submodules: baseModules.concat(["websockets"]) + } + Properties { + condition: !webSocketSupport + submodules: baseModules + } + } + + Depends { + name: "Exporter.qbs" + } + Depends { + name: "Exporter.pkgconfig" + } + + Export { + Depends { + name: "cpp" + } + + Depends { + name: "Qt" + property var baseModules: ["core", "network"] + Properties { + condition: product.webSocketSupport + submodules: baseModules.concat(["websockets"]) + } + Properties { + condition: !product.webSocketSupport + submodules: baseModules + } + } + + cpp.includePaths: [ + product.sourceDirectory, + FileInfo.joinPaths(qbs.installRoot, qbs.installPrefix, "include"), + ] + prefixMapping: [ + { + prefix: product.sourceDirectory, + replacement: FileInfo.joinPaths(qbs.installRoot, qbs.installPrefix, "include", + product.name) + } + ] + } +} diff --git a/mqtt/qmqtt_client.cpp b/mqtt/qmqtt_client.cpp new file mode 100644 index 0000000..78542e4 --- /dev/null +++ b/mqtt/qmqtt_client.cpp @@ -0,0 +1,412 @@ +/* + * qmqtt_client.cpp - qmqtt client + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "qmqtt_client.h" +#include "qmqtt_client_p.h" + +QMQTT::Client::Client(const QHostAddress& host, + const quint16 port, + QObject* parent) + : QObject(parent) + , d_ptr(new ClientPrivate(this)) +{ + Q_D(Client); + d->init(host, port); +} + +#ifndef QT_NO_SSL +QMQTT::Client::Client(const QString &hostName, + const quint16 port, + const QSslConfiguration &config, + const bool ignoreSelfSigned, QObject *parent) + : QObject(parent) + , d_ptr(new ClientPrivate(this)) +{ + Q_D(Client); + d->init(hostName, port, config, ignoreSelfSigned); +} +#endif // QT_NO_SSL + +QMQTT::Client::Client(const QString &hostName, + const quint16 port, + const bool ssl, + const bool ignoreSelfSigned, + QObject* parent) + : QObject(parent) + , d_ptr(new ClientPrivate(this)) +{ + Q_D(Client); + d->init(hostName, port, ssl, ignoreSelfSigned); +} + +#ifdef QT_WEBSOCKETS_LIB +QMQTT::Client::Client(const QString& url, + const QString& origin, + QWebSocketProtocol::Version version, + bool ignoreSelfSigned, + QObject* parent) + : QObject(parent) + , d_ptr(new ClientPrivate(this)) +{ + Q_D(Client); +#ifndef QT_NO_SSL + d->init(url, origin, version, NULL, ignoreSelfSigned); +#else + Q_UNUSED(ignoreSelfSigned) + d->init(url, origin, version); +#endif // QT_NO_SSL +} + +#ifndef QT_NO_SSL +QMQTT::Client::Client(const QString& url, + const QString& origin, + QWebSocketProtocol::Version version, + const QSslConfiguration& sslConfig, + const bool ignoreSelfSigned, + QObject* parent) + : QObject(parent) + , d_ptr(new ClientPrivate(this)) +{ + Q_D(Client); + d->init(url, origin, version, &sslConfig, ignoreSelfSigned); +} +#endif // QT_NO_SSL +#endif // QT_WEBSOCKETS_LIB + +QMQTT::Client::Client(NetworkInterface* network, + const QHostAddress& host, + const quint16 port, + QObject* parent) + : QObject(parent) + , d_ptr(new ClientPrivate(this)) +{ + Q_D(Client); + d->init(host, port, network); +} + +QMQTT::Client::~Client() +{ +} + +QHostAddress QMQTT::Client::host() const +{ + Q_D(const Client); + return d->host(); +} + +void QMQTT::Client::setHost(const QHostAddress& host) +{ + Q_D(Client); + d->setHost(host); +} + +QString QMQTT::Client::hostName() const +{ + Q_D(const Client); + return d->hostName(); +} + +void QMQTT::Client::setHostName(const QString &hostName) +{ + Q_D(Client); + d->setHostName(hostName); +} + +quint16 QMQTT::Client::port() const +{ + Q_D(const Client); + return d->port(); +} + +void QMQTT::Client::setPort(const quint16 port) +{ + Q_D(Client); + d->setPort(port); +} + +QString QMQTT::Client::clientId() const +{ + Q_D(const Client); + return d->clientId(); +} + +void QMQTT::Client::setClientId(const QString& clientId) +{ + Q_D(Client); + d->setClientId(clientId); +} + +QString QMQTT::Client::username() const +{ + Q_D(const Client); + return d->username(); +} + +void QMQTT::Client::setUsername(const QString& username) +{ + Q_D(Client); + d->setUsername(username); +} + +QMQTT::MQTTVersion QMQTT::Client::version() const +{ + Q_D(const Client); + return d->version(); +} + +void QMQTT::Client::setVersion(const QMQTT::MQTTVersion version) +{ + Q_D(Client); + d->setVersion(version); +} + +QByteArray QMQTT::Client::password() const +{ + Q_D(const Client); + return d->password(); +} + +void QMQTT::Client::setPassword(const QByteArray &password) +{ + Q_D(Client); + d->setPassword(password); +} + +quint16 QMQTT::Client::keepAlive() const +{ + Q_D(const Client); + return d->keepAlive(); +} + +void QMQTT::Client::setKeepAlive(const quint16 keepAlive) +{ + Q_D(Client); + d->setKeepAlive(keepAlive); +} + +bool QMQTT::Client::cleanSession() const +{ + Q_D(const Client); + return d->cleanSession(); +} + +void QMQTT::Client::setCleanSession(const bool cleanSession) +{ + Q_D(Client); + d->setCleanSession(cleanSession); +} + +bool QMQTT::Client::autoReconnect() const +{ + Q_D(const Client); + return d->autoReconnect(); +} + +void QMQTT::Client::setAutoReconnect(const bool value) +{ + Q_D(Client); + d->setAutoReconnect(value); +} + +int QMQTT::Client::autoReconnectInterval() const +{ + Q_D(const Client); + return d->autoReconnectInterval(); +} + +void QMQTT::Client::setAutoReconnectInterval(const int autoReconnectInterval) +{ + Q_D(Client); + d->setAutoReconnectInterval(autoReconnectInterval); +} + +QString QMQTT::Client::willTopic() const +{ + Q_D(const Client); + return d->willTopic(); +} + +void QMQTT::Client::setWillTopic(const QString& willTopic) +{ + Q_D(Client); + d->setWillTopic(willTopic); +} + +quint8 QMQTT::Client::willQos() const +{ + Q_D(const Client); + return d->willQos(); +} + +void QMQTT::Client::setWillQos(const quint8 willQos) +{ + Q_D(Client); + d->setWillQos(willQos); +} + +bool QMQTT::Client::willRetain() const +{ + Q_D(const Client); + return d->willRetain(); +} + +void QMQTT::Client::setWillRetain(const bool willRetain) +{ + Q_D(Client); + d->setWillRetain(willRetain); +} + +QByteArray QMQTT::Client::willMessage() const +{ + Q_D(const Client); + return d->willMessage(); +} + +void QMQTT::Client::setWillMessage(const QByteArray &willMessage) +{ + Q_D(Client); + d->setWillMessage(willMessage); +} + +QMQTT::ConnectionState QMQTT::Client::connectionState() const +{ + Q_D(const Client); + return d->connectionState(); +} + +bool QMQTT::Client::isConnectedToHost() const +{ + Q_D(const Client); + return d->isConnectedToHost(); +} + +#ifndef QT_NO_SSL +QSslConfiguration QMQTT::Client::sslConfiguration() const +{ + Q_D(const Client); + return d->sslConfiguration(); +} + +void QMQTT::Client::setSslConfiguration(const QSslConfiguration& config) +{ + Q_D(Client); + d->setSslConfiguration(config); +} +#endif // QT_NO_SSL + +void QMQTT::Client::connectToHost() +{ + Q_D(Client); + d->connectToHost(); +} + +void QMQTT::Client::onNetworkConnected() +{ + Q_D(Client); + d->onNetworkConnected(); +} + +quint16 QMQTT::Client::publish(const Message& message) +{ + Q_D(Client); + return d->publish(message); +} + +void QMQTT::Client::subscribe(const QString& topic, const quint8 qos) +{ + Q_D(Client); + d->subscribe(topic, qos); +} + +void QMQTT::Client::unsubscribe(const QString& topic) +{ + Q_D(Client); + d->unsubscribe(topic); +} + +void QMQTT::Client::onTimerPingReq() +{ + Q_D(Client); + d->onTimerPingReq(); +} + +void QMQTT::Client::onPingTimeout() +{ + Q_D(Client); + d->onPingTimeout(); +} + +void QMQTT::Client::disconnectFromHost() +{ + Q_D(Client); + d->disconnectFromHost(); +} + +void QMQTT::Client::onNetworkReceived(const QMQTT::Frame& frame) +{ + Q_D(Client); + d->onNetworkReceived(frame); +} + +void QMQTT::Client::onNetworkDisconnected() +{ + Q_D(Client); + d->onNetworkDisconnected(); +} + +void QMQTT::Client::onNetworkError(QAbstractSocket::SocketError error) +{ + Q_D(Client); + d->onNetworkError(error); +} + +#ifndef QT_NO_SSL +void QMQTT::Client::onSslErrors(const QList& errors) +{ + Q_D(Client); + d->onSslErrors(errors); +} +#endif // QT_NO_SSL + +#ifndef QT_NO_SSL +void QMQTT::Client::ignoreSslErrors() +{ + Q_D(Client); + d->ignoreSslErrors(); +} + +void QMQTT::Client::ignoreSslErrors(const QList& errors) +{ + Q_D(Client); + d->ignoreSslErrors(errors); +} +#endif // QT_NO_SSL diff --git a/mqtt/qmqtt_client.h b/mqtt/qmqtt_client.h new file mode 100644 index 0000000..da89f01 --- /dev/null +++ b/mqtt/qmqtt_client.h @@ -0,0 +1,276 @@ +/* + * qmqtt_client.h - qmqtt client header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_CLIENT_H +#define QMQTT_CLIENT_H + +#include "qmqtt_global.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef QT_WEBSOCKETS_LIB +#include +#endif // QT_WEBSOCKETS_LIB + +#ifndef QT_NO_SSL +#include +QT_FORWARD_DECLARE_CLASS(QSslError) +#endif // QT_NO_SSL + +namespace QMQTT { + +static const quint8 LIBRARY_VERSION_MAJOR = 0; +static const quint8 LIBRARY_VERSION_MINOR = 3; +static const quint8 LIBRARY_VERSION_REVISION = 1; +//static const char* LIBRARY_VERSION = "0.3.1"; + +enum MQTTVersion +{ + V3_1_0 = 3, + V3_1_1 = 4 +}; + +enum ConnectionState +{ + STATE_INIT = 0, + STATE_CONNECTING, + STATE_CONNECTED, + STATE_DISCONNECTED +}; + +enum ClientError +{ + UnknownError = 0, + SocketConnectionRefusedError, + SocketRemoteHostClosedError, + SocketHostNotFoundError, + SocketAccessError, + SocketResourceError, + SocketTimeoutError, + SocketDatagramTooLargeError, + SocketNetworkError, + SocketAddressInUseError, + SocketAddressNotAvailableError, + SocketUnsupportedSocketOperationError, + SocketUnfinishedSocketOperationError, + SocketProxyAuthenticationRequiredError, + SocketSslHandshakeFailedError, + SocketProxyConnectionRefusedError, + SocketProxyConnectionClosedError, + SocketProxyConnectionTimeoutError, + SocketProxyNotFoundError, + SocketProxyProtocolError, + SocketOperationError, + SocketSslInternalError, + SocketSslInvalidUserDataError, + SocketTemporaryError, + MqttUnacceptableProtocolVersionError=1<<16, + MqttIdentifierRejectedError, + MqttServerUnavailableError, + MqttBadUserNameOrPasswordError, + MqttNotAuthorizedError, + MqttNoPingResponse +}; + +class ClientPrivate; +class Message; +class Frame; +class NetworkInterface; + +class Q_MQTT_EXPORT Client : public QObject +{ + Q_OBJECT + Q_PROPERTY(quint16 _port READ port WRITE setPort) + Q_PROPERTY(QHostAddress _host READ host WRITE setHost) + Q_PROPERTY(QString _hostName READ hostName WRITE setHostName) + Q_PROPERTY(QString _clientId READ clientId WRITE setClientId) + Q_PROPERTY(QString _username READ username WRITE setUsername) + Q_PROPERTY(QByteArray _password READ password WRITE setPassword) + Q_PROPERTY(quint16 _keepAlive READ keepAlive WRITE setKeepAlive) + Q_PROPERTY(MQTTVersion _version READ version WRITE setVersion) + Q_PROPERTY(bool _autoReconnect READ autoReconnect WRITE setAutoReconnect) + Q_PROPERTY(int _autoReconnectInterval READ autoReconnectInterval WRITE setAutoReconnectInterval) + Q_PROPERTY(bool _cleanSession READ cleanSession WRITE setCleanSession) + Q_PROPERTY(QString _willTopic READ willTopic WRITE setWillTopic) + Q_PROPERTY(quint8 _willQos READ willQos WRITE setWillQos) + Q_PROPERTY(bool _willRetain READ willRetain WRITE setWillRetain) + Q_PROPERTY(QByteArray _willMessage READ willMessage WRITE setWillMessage) + Q_PROPERTY(QString _connectionState READ connectionState) +#ifndef QT_NO_SSL + Q_PROPERTY(QSslConfiguration _sslConfiguration READ sslConfiguration WRITE setSslConfiguration) +#endif // QT_NO_SSL + +public: + Client(const QHostAddress& host = QHostAddress::LocalHost, + const quint16 port = 1883, + QObject* parent = NULL); + +#ifndef QT_NO_SSL + Client(const QString& hostName, + const quint16 port, + const QSslConfiguration& config, + const bool ignoreSelfSigned=false, + QObject* parent = NULL); +#endif // QT_NO_SSL + + // This function is provided for backward compatibility with older versions of QMQTT. + // If the ssl parameter is true, this function will load a private key ('cert.key') and a local + // certificate ('cert.crt') from the current working directory. It will also set PeerVerifyMode + // to None. This may not be the safest way to set up an SSL connection. + Client(const QString& hostName, + const quint16 port, + const bool ssl, + const bool ignoreSelfSigned, + QObject* parent = NULL); + +#ifdef QT_WEBSOCKETS_LIB + // Create a connection over websockets + Client(const QString& url, + const QString& origin, + QWebSocketProtocol::Version version, + bool ignoreSelfSigned = false, + QObject* parent = NULL); + +#ifndef QT_NO_SSL + Client(const QString& url, + const QString& origin, + QWebSocketProtocol::Version version, + const QSslConfiguration& config, + const bool ignoreSelfSigned = false, + QObject* parent = NULL); +#endif // QT_NO_SSL +#endif // QT_WEBSOCKETS_LIB + + // for testing purposes only + Client(NetworkInterface* network, + const QHostAddress& host = QHostAddress::LocalHost, + const quint16 port = 1883, + QObject* parent = NULL); + + virtual ~Client(); + + QHostAddress host() const; + QString hostName() const; + quint16 port() const; + QString clientId() const; + QString username() const; + QByteArray password() const; + QMQTT::MQTTVersion version() const; + quint16 keepAlive() const; + bool cleanSession() const; + bool autoReconnect() const; + int autoReconnectInterval() const; + ConnectionState connectionState() const; + QString willTopic() const; + quint8 willQos() const; + bool willRetain() const; + QByteArray willMessage() const; + + bool isConnectedToHost() const; +#ifndef QT_NO_SSL + QSslConfiguration sslConfiguration() const; + void setSslConfiguration(const QSslConfiguration& config); +#endif // QT_NO_SSL + +public slots: + void setHost(const QHostAddress& host); + void setHostName(const QString& hostName); + void setPort(const quint16 port); + void setClientId(const QString& clientId); + void setUsername(const QString& username); + void setPassword(const QByteArray& password); + void setVersion(const MQTTVersion version); + void setKeepAlive(const quint16 keepAlive); + void setCleanSession(const bool cleanSession); + void setAutoReconnect(const bool value); + void setAutoReconnectInterval(const int autoReconnectInterval); + void setWillTopic(const QString& willTopic); + void setWillQos(const quint8 willQos); + void setWillRetain(const bool willRetain); + void setWillMessage(const QByteArray& willMessage); + + void connectToHost(); + void disconnectFromHost(); + + void subscribe(const QString& topic, const quint8 qos = 0); + void unsubscribe(const QString& topic); + + quint16 publish(const QMQTT::Message& message); + +#ifndef QT_NO_SSL + void ignoreSslErrors(); + void ignoreSslErrors(const QList& errors); +#endif // QT_NO_SSL + +signals: + void connected(); + void disconnected(); + void error(const QMQTT::ClientError error); + + void subscribed(const QString& topic, const quint8 qos = 0); + void unsubscribed(const QString& topic); + void published(const QMQTT::Message& message, quint16 msgid = 0); + void received(const QMQTT::Message& message); + void pingresp(); +#ifndef QT_NO_SSL + void sslErrors(const QList& errors); +#endif // QT_NO_SSL + +protected slots: + void onNetworkConnected(); + void onNetworkDisconnected(); + void onNetworkReceived(const QMQTT::Frame& frame); + void onTimerPingReq(); + void onPingTimeout(); + void onNetworkError(QAbstractSocket::SocketError error); +#ifndef QT_NO_SSL + void onSslErrors(const QList& errors); +#endif // QT_NO_SSL + +protected: + QScopedPointer d_ptr; + +private: + Q_DISABLE_COPY(Client) + Q_DECLARE_PRIVATE(Client) +}; + +} // namespace QMQTT + +Q_DECLARE_METATYPE(QMQTT::ClientError) + +#endif // QMQTT_CLIENT_H diff --git a/mqtt/qmqtt_client_p.cpp b/mqtt/qmqtt_client_p.cpp new file mode 100644 index 0000000..5f53297 --- /dev/null +++ b/mqtt/qmqtt_client_p.cpp @@ -0,0 +1,811 @@ +/* + * qmqtt_client_p.cpp - qmqtt client private + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "qmqtt_client_p.h" +#include "qmqtt_network_p.h" +#include "qmqtt_frame.h" +#include "qmqtt_message.h" + +#include +#include +#ifndef QT_NO_SSL +#include +#include +#include +#endif // QT_NO_SSL + +Q_LOGGING_CATEGORY(client, "qmqtt.client") + +static const quint8 QOS0 = 0x00; +static const quint8 QOS1 = 0x01; +static const quint8 QOS2 = 0x02; + +QMQTT::ClientPrivate::ClientPrivate(Client* qq_ptr) + : _host(QHostAddress::LocalHost) + , _port(1883) + , _gmid(1) + , _version(MQTTVersion::V3_1_0) + , _clientId(QUuid::createUuid().toString()) + , _cleanSession(false) + , _connectionState(STATE_INIT) + , _willQos(0) + , _willRetain(false) + , q_ptr(qq_ptr) +{ + setKeepAlive(300); +} + +QMQTT::ClientPrivate::~ClientPrivate() +{ +} + +void QMQTT::ClientPrivate::init(const QHostAddress& host, const quint16 port, NetworkInterface* network) +{ + Q_Q(Client); + _host = host; + _port = port; + if(network == NULL) + { + init(new Network(q)); + } + else + { + init(network); + } +} + +#ifndef QT_NO_SSL +void QMQTT::ClientPrivate::init(const QString& hostName, const quint16 port, + const QSslConfiguration &config, const bool ignoreSelfSigned) +{ + Q_Q(Client); + _hostName = hostName; + _port = port; + _ignoreSelfSigned = ignoreSelfSigned; + init(new Network(config, q)); + QObject::connect(_network.data(), &QMQTT::Network::sslErrors, q, &QMQTT::Client::onSslErrors); +} +#endif // QT_NO_SSL + +void QMQTT::ClientPrivate::init(const QString& hostName, const quint16 port, const bool ssl, + const bool ignoreSelfSigned) +{ + Q_Q(Client); + _hostName = hostName; + _port = port; + if (ssl) + { +#ifndef QT_NO_SSL + QSslConfiguration sslConf = QSslConfiguration::defaultConfiguration(); + QList certs = QSslCertificate::fromPath(QStringLiteral("./cert.crt")); + if (!certs.isEmpty()) + sslConf.setLocalCertificate(certs.first()); + QFile file(QStringLiteral("./cert.key")); + if (file.open(QIODevice::ReadOnly)) { + sslConf.setPrivateKey(QSslKey(file.readAll(), QSsl::Rsa)); + } + sslConf.setPeerVerifyMode(QSslSocket::VerifyNone); + init(hostName, port, sslConf, ignoreSelfSigned); +#else + Q_UNUSED(ignoreSelfSigned) + qCritical() << "SSL not supported in this QT build"; +#endif // QT_NO_SSL + } + else + { + init(new Network(q)); + } +} + +#ifdef QT_WEBSOCKETS_LIB +#ifndef QT_NO_SSL +void QMQTT::ClientPrivate::init(const QString& url, + const QString& origin, + QWebSocketProtocol::Version version, + const QSslConfiguration* sslConfig, + bool ignoreSelfSigned) +{ + Q_Q(Client); + _hostName = url; + _ignoreSelfSigned = ignoreSelfSigned; + init(new Network(origin, version, sslConfig, q)); +} +#endif // QT_NO_SSL + +void QMQTT::ClientPrivate::init(const QString& url, + const QString& origin, + QWebSocketProtocol::Version version) +{ + Q_Q(Client); + _hostName = url; + init(new Network(origin, version, q)); +} +#endif // QT_WEBSOCKETS_LIB + +void QMQTT::ClientPrivate::init(NetworkInterface* network) +{ + Q_Q(Client); + + _network.reset(network); + _timer.setSingleShot(true); + _pingResponseTimer.setSingleShot(true); + + QObject::connect(&_timer, &QTimer::timeout, q, &Client::onTimerPingReq); + QObject::connect(&_pingResponseTimer, &QTimer::timeout, q, &Client::onPingTimeout); + QObject::connect(_network.data(), &Network::connected, + q, &Client::onNetworkConnected); + QObject::connect(_network.data(), &Network::disconnected, + q, &Client::onNetworkDisconnected); + QObject::connect(_network.data(), &Network::received, + q, &Client::onNetworkReceived); + QObject::connect(_network.data(), &Network::error, + q, &Client::onNetworkError); +} + +void QMQTT::ClientPrivate::connectToHost() +{ + _connectionState = ConnectionState::STATE_CONNECTING; + if (_hostName.isEmpty()) + { + _network->connectToHost(_host, _port); + } + else + { + _network->connectToHost(_hostName, _port); + } +} + +void QMQTT::ClientPrivate::onNetworkConnected() +{ + sendConnect(); +} + +void QMQTT::ClientPrivate::sendConnect() +{ + quint8 header = CONNECT; + quint8 flags = 0; + + //header + Frame frame(header); + + //flags + flags = FLAG_CLEANSESS(flags, _cleanSession ? 1 : 0 ); + flags = FLAG_WILL(flags, willTopic().isEmpty() ? 0 : 1); + if (!willTopic().isEmpty()) + { + flags = FLAG_WILLQOS(flags, willQos()); + flags = FLAG_WILLRETAIN(flags, willRetain() ? 1 : 0); + } + if (!username().isEmpty()) + { + flags = FLAG_USERNAME(flags, 1); + flags = FLAG_PASSWD(flags, !password().isEmpty() ? 1 : 0); + } + + //payload + if(_version == V3_1_1) + { + frame.writeString(QStringLiteral(PROTOCOL_MAGIC_3_1_1)); + } + else + { + frame.writeString(QStringLiteral(PROTOCOL_MAGIC_3_1_0)); + } + frame.writeChar(_version); + frame.writeChar(flags); + frame.writeInt(keepAlive()); + frame.writeString(_clientId); + if(!willTopic().isEmpty()) + { + frame.writeString(willTopic()); + // According to the specs (http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718031) + // the will message gets always sent together with the topic, also when it is empty which is perfectly valid. + frame.writeByteArray(_willMessage); + } + if (!_username.isEmpty()) + { + frame.writeString(_username); + if (!_password.isEmpty()) + { + frame.writeByteArray(_password); + } + } + sendFrame(frame); +} + +quint16 QMQTT::ClientPrivate::sendPublish(const Message &message) +{ + quint16 msgid = message.id(); + + quint8 header = PUBLISH; + header = SETRETAIN(header, message.retain() ? 1 : 0); + header = SETQOS(header, message.qos()); + header = SETDUP(header, message.dup() ? 1 : 0); + Frame frame(header); + frame.writeString(message.topic()); + if(message.qos() > QOS0) { + if (msgid == 0) + msgid = nextmid(); + frame.writeInt(msgid); + } + if(!message.payload().isEmpty()) { + frame.writeRawData(message.payload()); + } + sendFrame(frame); + return msgid; +} + +void QMQTT::ClientPrivate::sendPuback(const quint8 type, const quint16 mid) +{ + Frame frame(type); + frame.writeInt(mid); + sendFrame(frame); +} + +quint16 QMQTT::ClientPrivate::sendSubscribe(const QString & topic, const quint8 qos) +{ + quint16 mid = nextmid(); + Frame frame(SETQOS(SUBSCRIBE, QOS1)); + frame.writeInt(mid); + frame.writeString(topic); + frame.writeChar(qos); + sendFrame(frame); + return mid; +} + +quint16 QMQTT::ClientPrivate::sendUnsubscribe(const QString &topic) +{ + quint16 mid = nextmid(); + Frame frame(SETQOS(UNSUBSCRIBE, QOS1)); + frame.writeInt(mid); + frame.writeString(topic); + sendFrame(frame); + return mid; +} + +void QMQTT::ClientPrivate::onTimerPingReq() +{ + if (!isConnectedToHost()) + return; + Frame frame(PINGREQ); + sendFrame(frame); + _pingResponseTimer.start(); +} + +void QMQTT::ClientPrivate::onPingTimeout() +{ + Q_Q(Client); + emit q->error(MqttNoPingResponse); + disconnectFromHost(); +} + +void QMQTT::ClientPrivate::disconnectFromHost() +{ + _connectionState = ConnectionState::STATE_DISCONNECTED; + sendDisconnect(); + _network->disconnectFromHost(); +} + +void QMQTT::ClientPrivate::sendDisconnect() +{ + Frame frame(DISCONNECT); + sendFrame(frame); +} + +void QMQTT::ClientPrivate::sendFrame(const Frame &frame) +{ + _network->sendFrame(frame); + _timer.start(); +} + +void QMQTT::ClientPrivate::stopKeepAlive() +{ + _timer.stop(); + _pingResponseTimer.stop(); +} + +quint16 QMQTT::ClientPrivate::nextmid() +{ + return _gmid++; +} + +quint16 QMQTT::ClientPrivate::publish(const Message& message) +{ + Q_Q(Client); + quint16 msgid = sendPublish(message); + + // Emit published only at QOS0 + if (message.qos() == QOS0) + emit q->published(message, msgid); + else + _midToMessage[msgid] = message; + + return msgid; +} + +void QMQTT::ClientPrivate::puback(const quint8 type, const quint16 msgid) +{ + sendPuback(type, msgid); +} + +void QMQTT::ClientPrivate::subscribe(const QString& topic, const quint8 qos) +{ + quint16 msgid = sendSubscribe(topic, qos); + _midToTopic[msgid] = topic; +} + +void QMQTT::ClientPrivate::unsubscribe(const QString& topic) +{ + quint16 msgid = sendUnsubscribe(topic); + _midToTopic[msgid] = topic; +} + +void QMQTT::ClientPrivate::onNetworkDisconnected() +{ + Q_Q(Client); + + stopKeepAlive(); + _midToTopic.clear(); + _midToMessage.clear(); + emit q->disconnected(); +} + +void QMQTT::ClientPrivate::onNetworkReceived(const QMQTT::Frame& frm) +{ + QMQTT::Frame frame(frm); + quint8 qos = 0; + bool retain, dup; + QString topic; + quint16 mid = 0; + quint8 header = frame.header(); + quint8 type = GETTYPE(header); + + switch(type) + { + case CONNACK: + frame.readChar(); + handleConnack(frame.readChar()); + break; + case PUBLISH: + qos = GETQOS(header); + retain = GETRETAIN(header); + dup = GETDUP(header); + topic = frame.readString(); + if( qos > QOS0) { + mid = frame.readInt(); + } + handlePublish(Message(mid, topic, frame.data(), qos, retain, dup)); + break; + case PUBACK: + case PUBREC: + case PUBREL: + case PUBCOMP: + mid = frame.readInt(); + handlePuback(type, mid); + break; + case SUBACK: + mid = frame.readInt(); + topic = _midToTopic.take(mid); + qos = frame.readChar(); + handleSuback(topic, qos); + break; + case UNSUBACK: + mid = frame.readInt(); + topic = _midToTopic.take(mid); + handleUnsuback(topic); + break; + case PINGRESP: + handlePingresp(); + break; + default: + break; + } +} + +void QMQTT::ClientPrivate::handleConnack(const quint8 ack) +{ + Q_Q(Client); + + switch (ack) + { + case 0: + _connectionState = ConnectionState::STATE_CONNECTED; + emit q->connected(); + break; + case 1: + emit q->error(MqttUnacceptableProtocolVersionError); + break; + case 2: + emit q->error(MqttIdentifierRejectedError); + break; + case 3: + emit q->error(MqttServerUnavailableError); + break; + case 4: + emit q->error(MqttBadUserNameOrPasswordError); + break; + case 5: + emit q->error(MqttNotAuthorizedError); + break; + default: + emit q->error(UnknownError); + break; + } +} + +void QMQTT::ClientPrivate::handlePublish(const Message& message) +{ + Q_Q(Client); + + if(message.qos() == QOS1) + { + sendPuback(PUBACK, message.id()); + } + else if(message.qos() == QOS2) + { + sendPuback(PUBREC, message.id()); + } + emit q->received(message); +} + +void QMQTT::ClientPrivate::handlePuback(const quint8 type, const quint16 msgid) +{ + Q_Q(Client); + + switch (type) + { + case PUBREC: + sendPuback(SETQOS(PUBREL, QOS1), msgid); + break; + case PUBREL: + sendPuback(PUBCOMP, msgid); + break; + case PUBACK: + case PUBCOMP: + // Emit published on PUBACK at QOS1 and on PUBCOMP at QOS2 + emit q->published(_midToMessage.take(msgid), msgid); + break; + } +} + +void QMQTT::ClientPrivate::handlePingresp() +{ + // Stop the ping response timer to prevent disconnection. It will be restarted when the next + // ping request has been sent. + _pingResponseTimer.stop(); + Q_Q(Client); + emit q->pingresp(); +} + +void QMQTT::ClientPrivate::handleSuback(const QString &topic, const quint8 qos) +{ + Q_Q(Client); + emit q->subscribed(topic, qos); +} + +void QMQTT::ClientPrivate::handleUnsuback(const QString &topic) { + Q_Q(Client); + emit q->unsubscribed(topic); +} + +bool QMQTT::ClientPrivate::autoReconnect() const +{ + return _network->autoReconnect(); +} + +void QMQTT::ClientPrivate::setAutoReconnect(const bool autoReconnect) +{ + _network->setAutoReconnect(autoReconnect); +} + +int QMQTT::ClientPrivate::autoReconnectInterval() const +{ + return _network->autoReconnectInterval(); +} + +void QMQTT::ClientPrivate::setAutoReconnectInterval(const int autoReconnectInterval) +{ + _network->setAutoReconnectInterval(autoReconnectInterval); +} + +bool QMQTT::ClientPrivate::isConnectedToHost() const +{ + return _network->isConnectedToHost(); +} + +QMQTT::ConnectionState QMQTT::ClientPrivate::connectionState() const +{ + return _connectionState; +} + +void QMQTT::ClientPrivate::setCleanSession(const bool cleanSession) +{ + _cleanSession = cleanSession; +} + +bool QMQTT::ClientPrivate::cleanSession() const +{ + return _cleanSession; +} + +void QMQTT::ClientPrivate::setKeepAlive(const quint16 keepAlive) +{ + // _timer will be started when a message is sent. + _timer.setInterval(keepAlive*1000); + // The MQTT specification does not mention a timeout value in this case, so we use 10% of the + // keep alive interval. + _pingResponseTimer.setInterval(qBound(keepAlive * 100, 10000, keepAlive * 1000)); +} + +quint16 QMQTT::ClientPrivate::keepAlive() const +{ + return _timer.interval() / 1000; +} + +void QMQTT::ClientPrivate::setPassword(const QByteArray& password) +{ + _password = password; +} + +QByteArray QMQTT::ClientPrivate::password() const +{ + return _password; +} + +void QMQTT::ClientPrivate::setUsername(const QString& username) +{ + _username = username; +} + +QString QMQTT::ClientPrivate::username() const +{ + return _username; +} + +void QMQTT::ClientPrivate::setVersion(const MQTTVersion version) +{ + _version = version; +} + +QMQTT::MQTTVersion QMQTT::ClientPrivate::version() const +{ + return _version; +} + +void QMQTT::ClientPrivate::setClientId(const QString& clientId) +{ + if(clientId.isEmpty()) + { + _clientId = QUuid::createUuid().toString(); + } + else + { + _clientId = clientId; + } +} + +QString QMQTT::ClientPrivate::clientId() const +{ + return _clientId; +} + +void QMQTT::ClientPrivate::setPort(const quint16 port) +{ + _port = port; +} + +quint16 QMQTT::ClientPrivate::port() const +{ + return _port; +} + +void QMQTT::ClientPrivate::setHost(const QHostAddress& host) +{ + _host = host; +} + +QHostAddress QMQTT::ClientPrivate::host() const +{ + return _host; +} + +void QMQTT::ClientPrivate::setHostName(const QString& hostName) +{ + _hostName = hostName; +} + +QString QMQTT::ClientPrivate::hostName() const +{ + return _hostName; +} + +QString QMQTT::ClientPrivate::willTopic() const +{ + return _willTopic; +} + +void QMQTT::ClientPrivate::setWillTopic(const QString& willTopic) +{ + _willTopic = willTopic; +} + +quint8 QMQTT::ClientPrivate::willQos() const +{ + return _willQos; +} + +void QMQTT::ClientPrivate::setWillQos(const quint8 willQos) +{ + _willQos = willQos; +} + +bool QMQTT::ClientPrivate::willRetain() const +{ + return _willRetain; +} + +void QMQTT::ClientPrivate::setWillRetain(const bool willRetain) +{ + _willRetain = willRetain; +} + +QByteArray QMQTT::ClientPrivate::willMessage() const +{ + return _willMessage; +} + +void QMQTT::ClientPrivate::setWillMessage(const QByteArray& willMessage) +{ + _willMessage = willMessage; +} + +void QMQTT::ClientPrivate::onNetworkError(QAbstractSocket::SocketError socketError) +{ + Q_Q(Client); + + switch (socketError) + { + case QAbstractSocket::ConnectionRefusedError: + emit q->error(SocketConnectionRefusedError); + break; + case QAbstractSocket::RemoteHostClosedError: + emit q->error(SocketRemoteHostClosedError); + break; + case QAbstractSocket::HostNotFoundError: + emit q->error(SocketHostNotFoundError); + break; + case QAbstractSocket::SocketAccessError: + emit q->error(SocketAccessError); + break; + case QAbstractSocket::SocketResourceError: + emit q->error(SocketResourceError); + break; + case QAbstractSocket::SocketTimeoutError: + emit q->error(SocketTimeoutError); + break; + case QAbstractSocket::DatagramTooLargeError: + emit q->error(SocketDatagramTooLargeError); + break; + case QAbstractSocket::NetworkError: + emit q->error(SocketNetworkError); + break; + case QAbstractSocket::AddressInUseError: + emit q->error(SocketAddressInUseError); + break; + case QAbstractSocket::SocketAddressNotAvailableError: + emit q->error(SocketAddressNotAvailableError); + break; + case QAbstractSocket::UnsupportedSocketOperationError: + emit q->error(SocketUnsupportedSocketOperationError); + break; + case QAbstractSocket::UnfinishedSocketOperationError: + emit q->error(SocketUnfinishedSocketOperationError); + break; + case QAbstractSocket::ProxyAuthenticationRequiredError: + emit q->error(SocketProxyAuthenticationRequiredError); + break; + case QAbstractSocket::SslHandshakeFailedError: + emit q->error(SocketSslHandshakeFailedError); + break; + case QAbstractSocket::ProxyConnectionRefusedError: + emit q->error(SocketProxyConnectionRefusedError); + break; + case QAbstractSocket::ProxyConnectionClosedError: + emit q->error(SocketProxyConnectionClosedError); + break; + case QAbstractSocket::ProxyConnectionTimeoutError: + emit q->error(SocketProxyConnectionTimeoutError); + break; + case QAbstractSocket::ProxyNotFoundError: + emit q->error(SocketProxyNotFoundError); + break; + case QAbstractSocket::ProxyProtocolError: + emit q->error(SocketProxyProtocolError); + break; + case QAbstractSocket::OperationError: + emit q->error(SocketOperationError); + break; + case QAbstractSocket::SslInternalError: + emit q->error(SocketSslInternalError); + break; + case QAbstractSocket::SslInvalidUserDataError: + emit q->error(SocketSslInvalidUserDataError); + break; + case QAbstractSocket::TemporaryError: + emit q->error(SocketTemporaryError); + break; + default: + emit q->error(UnknownError); + break; + } +} + +#ifndef QT_NO_SSL +void QMQTT::ClientPrivate::ignoreSslErrors() +{ + _network->ignoreSslErrors(); +} + +void QMQTT::ClientPrivate::ignoreSslErrors(const QList& errors) +{ + _network->ignoreSslErrors(errors); +} + +QSslConfiguration QMQTT::ClientPrivate::sslConfiguration() const +{ + return _network->sslConfiguration(); +} + +void QMQTT::ClientPrivate::setSslConfiguration(const QSslConfiguration& config) +{ + _network->setSslConfiguration(config); +} + +void QMQTT::ClientPrivate::onSslErrors(const QList& errors) +{ + Q_Q(Client); + + emit q->sslErrors(errors); + + if (!_ignoreSelfSigned) + return; + foreach (QSslError error, errors) + { + if (error.error() != QSslError::SelfSignedCertificate && + error.error() != QSslError::SelfSignedCertificateInChain) + { + return; + } + } + ignoreSslErrors(); +} +#endif // QT_NO_SSL diff --git a/mqtt/qmqtt_client_p.h b/mqtt/qmqtt_client_p.h new file mode 100644 index 0000000..03ec994 --- /dev/null +++ b/mqtt/qmqtt_client_p.h @@ -0,0 +1,183 @@ +/* + * qmqtt_client_p.h - qmqtt client private header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_CLIENT_P_H +#define QMQTT_CLIENT_P_H + +#include "qmqtt_client.h" + +#include +#include +#include +#include +#include +#include + +#ifdef QT_WEBSOCKETS_LIB +#include +#endif // QT_WEBSOCKETS_LIB + +#ifndef QT_NO_SSL +#include +QT_FORWARD_DECLARE_CLASS(QSslError) +#endif // QT_NO_SSL + +namespace QMQTT { + +class NetworkInterface; + +class ClientPrivate +{ +public: + ClientPrivate(Client* qq_ptr); + ~ClientPrivate(); + + void init(const QHostAddress& host, const quint16 port, NetworkInterface* network = NULL); +#ifndef QT_NO_SSL + void init(const QString& hostName, const quint16 port, const QSslConfiguration& config, + const bool ignoreSelfSigned=false); +#endif // QT_NO_SSL + void init(const QString& hostName, const quint16 port, const bool ssl, const bool ignoreSelfSigned); +#ifdef QT_WEBSOCKETS_LIB +#ifndef QT_NO_SSL + void init(const QString& url, + const QString& origin, + QWebSocketProtocol::Version version, + const QSslConfiguration* sslConfig, + bool ignoreSelfSigned); +#endif // QT_NO_SSL + void init(const QString& url, + const QString& origin, + QWebSocketProtocol::Version version); +#endif // QT_WEBSOCKETS_LIB + void init(NetworkInterface* network); + + QHostAddress _host; + QString _hostName; + quint16 _port; +#ifdef QT_WEBSOCKETS_LIB + QWebSocketProtocol::Version _webSocketVersion; +#endif // QT_WEBSOCKETS_LIB +#ifndef QT_NO_SSL + bool _ignoreSelfSigned; +#endif // QT_NO_SSL + quint16 _gmid; + MQTTVersion _version; + QString _clientId; + QString _username; + QByteArray _password; + bool _cleanSession; + ConnectionState _connectionState; + QScopedPointer _network; + QTimer _timer; + QTimer _pingResponseTimer; + QString _willTopic; + quint8 _willQos; + bool _willRetain; + QByteArray _willMessage; + QHash _midToTopic; + QHash _midToMessage; + + Client* const q_ptr; + + quint16 nextmid(); + void connectToHost(); + void sendConnect(); + void onTimerPingReq(); + void onPingTimeout(); + quint16 sendUnsubscribe(const QString &topic); + quint16 sendSubscribe(const QString &topic, const quint8 qos); + quint16 sendPublish(const Message &message); + void sendPuback(const quint8 type, const quint16 mid); + void sendDisconnect(); + void sendFrame(const Frame &frame); + void disconnectFromHost(); + void stopKeepAlive(); + void onNetworkConnected(); + void onNetworkDisconnected(); + quint16 publish(const Message& message); + void puback(const quint8 type, const quint16 msgid); + void subscribe(const QString& topic, const quint8 qos); + void unsubscribe(const QString& topic); + void onNetworkReceived(const QMQTT::Frame& frame); + void handleConnack(const quint8 ack); + void handlePublish(const Message& message); + void handlePuback(const quint8 type, const quint16 msgid); + void handleSuback(const QString& topic, const quint8 qos); + void handleUnsuback(const QString& topic); + void handlePingresp(); + bool autoReconnect() const; + void setAutoReconnect(const bool autoReconnect); + int autoReconnectInterval() const; + void setAutoReconnectInterval(const int autoReconnectInterval); + bool isConnectedToHost() const; + QMQTT::ConnectionState connectionState() const; + void setCleanSession(const bool cleanSession); + bool cleanSession() const; + void setKeepAlive(const quint16 keepAlive); + quint16 keepAlive() const; + void setPassword(const QByteArray& password); + QByteArray password() const; + void setUsername(const QString& username); + QString username() const; + void setVersion(const MQTTVersion); + MQTTVersion version() const; + void setClientId(const QString& clientId); + QString clientId() const; + void setPort(const quint16 port); + quint16 port() const; + void setHost(const QHostAddress& host); + QHostAddress host() const; + void setHostName(const QString& hostName); + QString hostName() const; + void setWillTopic(const QString& willTopic); + void setWillQos(const quint8 willQos); + void setWillRetain(const bool willRetain); + void setWillMessage(const QByteArray& willMessage); + QString willTopic() const; + quint8 willQos() const; + bool willRetain() const; + QByteArray willMessage() const; + void onNetworkError(QAbstractSocket::SocketError error); +#ifndef QT_NO_SSL + void ignoreSslErrors(); + void ignoreSslErrors(const QList& errors); + QSslConfiguration sslConfiguration() const; + void setSslConfiguration(const QSslConfiguration& config); + void onSslErrors(const QList& errors); +#endif // QT_NO_SSL + + Q_DECLARE_PUBLIC(Client) +}; + +} // namespace QMQTT + +#endif // QMQTT_CLIENT_P_H diff --git a/mqtt/qmqtt_frame.cpp b/mqtt/qmqtt_frame.cpp new file mode 100644 index 0000000..7f7e152 --- /dev/null +++ b/mqtt/qmqtt_frame.cpp @@ -0,0 +1,209 @@ +/* + * qmqtt_frame.cpp - qmqtt frame + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "qmqtt_frame.h" + +#include +#include + +namespace QMQTT { + +Q_LOGGING_CATEGORY(frame, "qmqtt.frame") + +Frame::Frame() + : _header(0) + , _data(QByteArray()) +{ +} + +Frame::Frame(const quint8 header) + : _header(header) + , _data(QByteArray()) +{ +} + +Frame::Frame(const quint8 header, const QByteArray &data) + : _header(header) + , _data(data) +{ +} + +Frame::Frame(const Frame& other) +{ + _header = other._header; + _data = other._data; +} + +Frame& Frame::operator=(const Frame& other) +{ + _header = other._header; + _data = other._data; + return *this; +} + +bool Frame::operator==(const Frame& other) const +{ + return _header == other._header + && _data == other._data; +} + + +Frame::~Frame() +{ +} + +quint8 Frame::header() const +{ + return _header; +} + +QByteArray Frame::data() const +{ + return _data; +} + +quint8 Frame::readChar() +{ + char c = _data.at(0); + _data.remove(0, 1); + return c; +} + +quint16 Frame::readInt() +{ + quint8 msb = static_cast(_data.at(0)); + quint8 lsb = static_cast(_data.at(1)); + _data.remove(0, 2); + return (msb << 8) | lsb; +} + +QByteArray Frame::readByteArray() +{ + quint16 len = readInt(); + QByteArray data = _data.left(len); + _data.remove(0, len); + return data; +} + +QString Frame::readString() +{ + quint16 len = readInt(); + QString s = QString::fromUtf8(_data.left(len)); + _data.remove(0, len); + return s; +} + +void Frame::writeInt(const quint16 i) +{ + _data.append(MSB(i)); + _data.append(LSB(i)); +} + +void Frame::writeByteArray(const QByteArray &data) +{ + if (data.size() > (int)USHRT_MAX) + { + qCritical("qmqtt: Binary data size bigger than %u bytes, truncate it!", USHRT_MAX); + writeInt(USHRT_MAX); + _data.append(data.left(USHRT_MAX)); + return; + } + + writeInt(data.size()); + _data.append(data); +} + +void Frame::writeString(const QString &string) +{ + QByteArray data = string.toUtf8(); + if (data.size() > (int)USHRT_MAX) + { + qCritical("qmqtt: String size bigger than %u bytes, truncate it!", USHRT_MAX); + data.resize(USHRT_MAX); + } + writeInt(data.size()); + _data.append(data); +} + +void Frame::writeChar(const quint8 c) +{ + _data.append(c); +} + +void Frame::writeRawData(const QByteArray &data) +{ + _data.append(data); +} + +void Frame::write(QDataStream &stream) const +{ + QByteArray lenbuf; + + if (!encodeLength(lenbuf, _data.size())) + { + qCritical("qmqtt: Control packet bigger than 256 MB, dropped!"); + return; + } + + stream << (quint8)_header; + if(_data.size() == 0) { + stream << (quint8)0; + return; + } + if (stream.writeRawData(lenbuf.data(), lenbuf.size()) != lenbuf.size()) + { + qCritical("qmqtt: Control packet write error!"); + return; + } + if (stream.writeRawData(_data.data(), _data.size()) != _data.size()) + { + qCritical("qmqtt: Control packet write error!"); + } +} + +bool Frame::encodeLength(QByteArray &lenbuf, int length) const +{ + lenbuf.clear(); + quint8 d; + do { + d = length % 128; + length /= 128; + if (length > 0) { + d |= 0x80; + } + lenbuf.append(d); + } while (length > 0); + + return lenbuf.size() <= 4; +} + +} // namespace QMQTT diff --git a/mqtt/qmqtt_frame.h b/mqtt/qmqtt_frame.h new file mode 100644 index 0000000..0005b82 --- /dev/null +++ b/mqtt/qmqtt_frame.h @@ -0,0 +1,137 @@ +/* + * qmqtt_frame.h - qmqtt frame heaer + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_FRAME_H +#define QMQTT_FRAME_H + +#include "qmqtt_global.h" + +#include +#include +#include + +QT_FORWARD_DECLARE_CLASS(QDataStream) + +#define PROTOCOL_MAGIC_3_1_0 "MQIsdp" +#define PROTOCOL_MAGIC_3_1_1 "MQTT" + +#define RANDOM_CLIENT_PREFIX "QMQTT-" + +#define CONNECT 0x10 +#define CONNACK 0x20 +#define PUBLISH 0x30 +#define PUBACK 0x40 +#define PUBREC 0x50 +#define PUBREL 0x60 +#define PUBCOMP 0x70 +#define SUBSCRIBE 0x80 +#define SUBACK 0x90 +#define UNSUBSCRIBE 0xA0 +#define UNSUBACK 0xB0 +#define PINGREQ 0xC0 +#define PINGRESP 0xD0 +#define DISCONNECT 0xE0 + +#define LSB(A) quint8(A & 0x00FF) +#define MSB(A) quint8((A & 0xFF00) >> 8) + +/* +|-------------------------------------- +| 7 6 5 4 | 3 | 2 1 | 0 | +| Type | DUP flag | QoS | RETAIN | +|-------------------------------------- +*/ +#define GETTYPE(HDR) (HDR & 0xF0) +#define SETQOS(HDR, Q) (HDR | ((Q) << 1)) +#define GETQOS(HDR) ((HDR & 0x06) >> 1) +#define SETDUP(HDR, D) (HDR | ((D) << 3)) +#define GETDUP(HDR) ((HDR & 0x08) >> 3) +#define SETRETAIN(HDR, R) (HDR | (R)) +#define GETRETAIN(HDR) (HDR & 0x01) + +/* +|---------------------------------------------------------------------------------- +| 7 | 6 | 5 | 4 3 | 2 | 1 | 0 | +| username | password | willretain | willqos | willflag | cleansession | reserved | +|---------------------------------------------------------------------------------- +*/ +#define FLAG_CLEANSESS(F, C) (F | ((C) << 1)) +#define FLAG_WILL(F, W) (F | ((W) << 2)) +#define FLAG_WILLQOS(F, Q) (F | ((Q) << 3)) +#define FLAG_WILLRETAIN(F, R) (F | ((R) << 5)) +#define FLAG_PASSWD(F, P) (F | ((P) << 6)) +#define FLAG_USERNAME(F, U) (F | ((U) << 7)) + +namespace QMQTT { + +class Q_MQTT_EXPORT Frame +{ +public: + explicit Frame(); + explicit Frame(const quint8 header); + explicit Frame(const quint8 header, const QByteArray &data); + virtual ~Frame(); + + Frame(const Frame& other); + Frame& operator=(const Frame& other); + + bool operator==(const Frame& other) const; + inline bool operator!=(const Frame& other) const + { return !operator==(other); } + + quint8 header() const; + QByteArray data() const; + + quint16 readInt(); + quint8 readChar(); + QByteArray readByteArray(); + QString readString(); + + void writeInt(const quint16 i); + void writeChar(const quint8 c); + void writeByteArray(const QByteArray &data); + void writeString(const QString &string); + void writeRawData(const QByteArray &data); + + //TODO: FIXME LATER + void write(QDataStream &stream) const; + bool encodeLength(QByteArray &lenbuf, int length) const; + +private: + quint8 _header; + QByteArray _data; +}; + +} // namespace QMQTT + +Q_DECLARE_METATYPE(QMQTT::Frame) + +#endif // QMQTT_FRAME_H diff --git a/mqtt/qmqtt_global.h b/mqtt/qmqtt_global.h new file mode 100644 index 0000000..d633168 --- /dev/null +++ b/mqtt/qmqtt_global.h @@ -0,0 +1,48 @@ +/* + * qmqtt_global.h - qmqtt libray global + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_GLOBAL_H +#define QMQTT_GLOBAL_H + +#include + +#ifndef QT_STATIC +# if defined(QT_BUILD_QMQTT_LIB) +# define Q_MQTT_EXPORT Q_DECL_EXPORT +# else +# define Q_MQTT_EXPORT Q_DECL_IMPORT +# endif +#else +# define Q_MQTT_EXPORT +#endif + +#endif // QMQTT_GLOBAL_H + diff --git a/mqtt/qmqtt_message.cpp b/mqtt/qmqtt_message.cpp new file mode 100644 index 0000000..7beec31 --- /dev/null +++ b/mqtt/qmqtt_message.cpp @@ -0,0 +1,136 @@ +/* + * qmqtt_message.cpp - qmqtt message + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "qmqtt_message.h" +#include "qmqtt_message_p.h" + +namespace QMQTT { + +Message::Message() + : d(new MessagePrivate) +{ +} + +Message::Message(const quint16 id, const QString &topic, const QByteArray &payload, + const quint8 qos, const bool retain, const bool dup) + : d(new MessagePrivate(id, topic, payload, qos, retain, dup)) +{ +} + +Message::Message(const Message &other) + : d(other.d) +{ +} + +Message::~Message() +{ +} + +Message &Message::operator=(const Message &other) +{ + d = other.d; + return *this; +} + +bool Message::operator==(const Message &other) const +{ + if (d == other.d) + return true; + return d->id == other.d->id + && d->qos == other.d->qos + && d->retain == other.d->retain + && d->dup == other.d->dup + && d->topic == other.d->topic + && d->payload == other.d->payload; +} + +quint16 Message::id() const +{ + return d->id; +} + +void Message::setId(const quint16 id) +{ + d->id = id; +} + +quint8 Message::qos() const +{ + return d->qos; +} + +void Message::setQos(const quint8 qos) +{ + d->qos = qos; +} + +bool Message::retain() const +{ + return d->retain; +} + +void Message::setRetain(const bool retain) +{ + d->retain = retain; +} + +bool Message::dup() const +{ + return d->dup; +} + +void Message::setDup(const bool dup) +{ + d->dup = dup; +} + +QString Message::topic() const +{ + return d->topic; +} + +void Message::setTopic(const QString &topic) +{ + d->topic = topic; +} + +QByteArray Message::payload() const +{ + return d->payload; +} + +void Message::setPayload(const QByteArray &payload) +{ + d->payload = payload; +} + +} // namespace QMQTT diff --git a/mqtt/qmqtt_message.h b/mqtt/qmqtt_message.h new file mode 100644 index 0000000..3dbaa02 --- /dev/null +++ b/mqtt/qmqtt_message.h @@ -0,0 +1,96 @@ +/* + * qmqtt_message.h - qmqtt message header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_MESSAGE_H +#define QMQTT_MESSAGE_H + +#include "qmqtt_global.h" + +#include +#include +#include +#include + +namespace QMQTT { + +class MessagePrivate; + +class Q_MQTT_EXPORT Message +{ +public: + Message(); + explicit Message(const quint16 id, const QString &topic, const QByteArray &payload, + const quint8 qos = 0, const bool retain = false, const bool dup = false); + Message(const Message &other); + ~Message(); + + Message &operator=(const Message &other); +#ifdef Q_COMPILER_RVALUE_REFS + inline Message &operator=(Message &&other) Q_DECL_NOTHROW + { swap(other); return *this; } +#endif + + bool operator==(const Message &other) const; + inline bool operator!=(const Message &other) const + { return !operator==(other); } + + inline void swap(Message &other) Q_DECL_NOTHROW + { qSwap(d, other.d); } + + quint16 id() const; + void setId(const quint16 id); + + quint8 qos() const; + void setQos(const quint8 qos); + + bool retain() const; + void setRetain(const bool retain); + + bool dup() const; + void setDup(const bool dup); + + QString topic() const; + void setTopic(const QString &topic); + + QByteArray payload() const; + void setPayload(const QByteArray &payload); + +private: + QSharedDataPointer d; +}; + +} // namespace QMQTT + +Q_DECLARE_SHARED(QMQTT::Message) + +Q_DECLARE_METATYPE(QMQTT::Message) + +#endif // QMQTT_MESSAGE_H diff --git a/mqtt/qmqtt_message_p.h b/mqtt/qmqtt_message_p.h new file mode 100644 index 0000000..148019c --- /dev/null +++ b/mqtt/qmqtt_message_p.h @@ -0,0 +1,83 @@ +/* + * qmqtt_message.h - qmqtt message private header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_MESSAGE_P_H +#define QMQTT_MESSAGE_P_H + +#include +#include +#include + +namespace QMQTT { + +class MessagePrivate : public QSharedData +{ +public: + inline MessagePrivate() + : QSharedData(), + id(0), + qos(0), + retain(false), + dup(false) + {} + + inline MessagePrivate(const MessagePrivate &other) + : QSharedData(other), + id(other.id), + qos(other.qos), + retain(other.retain), + dup(other.dup), + topic(other.topic), + payload(other.payload) + {} + + inline MessagePrivate(quint16 id, const QString &topic, const QByteArray &payload, + quint8 qos, bool retain, bool dup) + : QSharedData(), + id(id), + qos(qos), + retain(retain), + dup(dup), + topic(topic), + payload(payload) + {} + + quint16 id; + quint8 qos : 2; + quint8 retain: 1; + quint8 dup: 1; + QString topic; + QByteArray payload; +}; + +} // namespace QMQTT + +#endif // QMQTT_MESSAGE_P_H diff --git a/mqtt/qmqtt_network.cpp b/mqtt/qmqtt_network.cpp new file mode 100644 index 0000000..b4f55ab --- /dev/null +++ b/mqtt/qmqtt_network.cpp @@ -0,0 +1,295 @@ +/* + * qmqtt_network.cpp - qmqtt network + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "qmqtt_network_p.h" +#include "qmqtt_socket_p.h" +#include "qmqtt_ssl_socket_p.h" +#include "qmqtt_timer_p.h" +#include "qmqtt_websocket_p.h" +#include "qmqtt_frame.h" + +#include + +const quint16 DEFAULT_PORT = 1883; +const quint16 DEFAULT_SSL_PORT = 8883; +const bool DEFAULT_AUTORECONNECT = false; +const int DEFAULT_AUTORECONNECT_INTERVAL_MS = 5000; + +QMQTT::Network::Network(QObject* parent) + : NetworkInterface(parent) + , _port(DEFAULT_PORT) + , _autoReconnect(DEFAULT_AUTORECONNECT) + , _autoReconnectInterval(DEFAULT_AUTORECONNECT_INTERVAL_MS) + , _socket(new QMQTT::Socket) + , _autoReconnectTimer(new QMQTT::Timer) + , _readState(Header) +{ + initialize(); +} + +#ifndef QT_NO_SSL +QMQTT::Network::Network(const QSslConfiguration& config, QObject *parent) + : NetworkInterface(parent) + , _port(DEFAULT_SSL_PORT) + , _autoReconnect(DEFAULT_AUTORECONNECT) + , _autoReconnectInterval(DEFAULT_AUTORECONNECT_INTERVAL_MS) + , _socket(new QMQTT::SslSocket(config)) + , _autoReconnectTimer(new QMQTT::Timer) + , _readState(Header) +{ + initialize(); + connect(_socket, &QMQTT::SslSocket::sslErrors, this, &QMQTT::Network::sslErrors); +} +#endif // QT_NO_SSL + +#ifdef QT_WEBSOCKETS_LIB +#ifndef QT_NO_SSL +QMQTT::Network::Network(const QString& origin, + QWebSocketProtocol::Version version, + const QSslConfiguration* sslConfig, + QObject* parent) + : NetworkInterface(parent) + , _port(DEFAULT_SSL_PORT) + , _autoReconnect(DEFAULT_AUTORECONNECT) + , _autoReconnectInterval(DEFAULT_AUTORECONNECT_INTERVAL_MS) + , _socket(new QMQTT::WebSocket(origin, version, sslConfig)) + , _autoReconnectTimer(new QMQTT::Timer) + , _readState(Header) +{ + initialize(); +} +#endif // QT_NO_SSL + +QMQTT::Network::Network(const QString& origin, + QWebSocketProtocol::Version version, + QObject* parent) + : NetworkInterface(parent) + , _port(DEFAULT_PORT) + , _autoReconnect(DEFAULT_AUTORECONNECT) + , _autoReconnectInterval(DEFAULT_AUTORECONNECT_INTERVAL_MS) + , _socket(new QMQTT::WebSocket(origin, version)) + , _autoReconnectTimer(new QMQTT::Timer) + , _readState(Header) +{ + initialize(); +} +#endif // QT_WEBSOCKETS_LIB + +QMQTT::Network::Network(SocketInterface* socketInterface, TimerInterface* timerInterface, + QObject* parent) + : NetworkInterface(parent) + , _port(DEFAULT_PORT) + , _autoReconnect(DEFAULT_AUTORECONNECT) + , _autoReconnectInterval(DEFAULT_AUTORECONNECT_INTERVAL_MS) + , _socket(socketInterface) + , _autoReconnectTimer(timerInterface) + , _readState(Header) +{ + initialize(); +} + +void QMQTT::Network::initialize() +{ + _socket->setParent(this); + _autoReconnectTimer->setParent(this); + _autoReconnectTimer->setSingleShot(true); + _autoReconnectTimer->setInterval(_autoReconnectInterval); + + QObject::connect(_socket, &SocketInterface::connected, this, &Network::connected); + QObject::connect(_socket, &SocketInterface::disconnected, this, &Network::onDisconnected); + QObject::connect(_socket->ioDevice(), &QIODevice::readyRead, this, &Network::onSocketReadReady); + QObject::connect( + _autoReconnectTimer, &TimerInterface::timeout, + this, static_cast(&Network::connectToHost)); + QObject::connect(_socket, + static_cast(&SocketInterface::error), + this, &Network::onSocketError); +} + +QMQTT::Network::~Network() +{ +} + +bool QMQTT::Network::isConnectedToHost() const +{ + return _socket->state() == QAbstractSocket::ConnectedState; +} + +void QMQTT::Network::connectToHost(const QHostAddress& host, const quint16 port) +{ + // Reset the hostname, because if it is not empty connectToHost() will use it instead of _host. + _hostName.clear(); + _host = host; + _port = port; + connectToHost(); +} + +void QMQTT::Network::connectToHost(const QString& hostName, const quint16 port) +{ + _hostName = hostName; + _port = port; + connectToHost(); +} + +void QMQTT::Network::connectToHost() +{ + _readState = Header; + if (_hostName.isEmpty()) + { + _socket->connectToHost(_host, _port); + } + else + { + _socket->connectToHost(_hostName, _port); + } +} + +void QMQTT::Network::onSocketError(QAbstractSocket::SocketError socketError) +{ + emit error(socketError); + if(_autoReconnect) + { + _autoReconnectTimer->start(); + } +} + +void QMQTT::Network::sendFrame(const Frame& frame) +{ + if(_socket->state() == QAbstractSocket::ConnectedState) + { + QDataStream out(_socket->ioDevice()); + frame.write(out); + } +} + +void QMQTT::Network::disconnectFromHost() +{ + _socket->disconnectFromHost(); +} + +QAbstractSocket::SocketState QMQTT::Network::state() const +{ + return _socket->state(); +} + +bool QMQTT::Network::autoReconnect() const +{ + return _autoReconnect; +} + +void QMQTT::Network::setAutoReconnect(const bool autoReconnect) +{ + _autoReconnect = autoReconnect; +} + +int QMQTT::Network::autoReconnectInterval() const +{ + return _autoReconnectInterval; +} + +void QMQTT::Network::setAutoReconnectInterval(const int autoReconnectInterval) +{ + _autoReconnectInterval = autoReconnectInterval; + _autoReconnectTimer->setInterval(_autoReconnectInterval); +} + +void QMQTT::Network::onSocketReadReady() +{ + QIODevice *ioDevice = _socket->ioDevice(); + // Only read the available (cached) bytes, so the read will never block. + QByteArray data = ioDevice->read(ioDevice->bytesAvailable()); + foreach(char byte, data) { + switch (_readState) { + case Header: + _header = static_cast(byte); + _readState = Length; + _length = 0; + _shift = 0; + _data.resize(0); // keep allocated buffer + break; + case Length: + _length |= (byte & 0x7F) << _shift; + _shift += 7; + if ((byte & 0x80) != 0) + break; + if (_length == 0) { + _readState = Header; + Frame frame(_header, _data); + emit received(frame); + break; + } + _readState = PayLoad; + _data.reserve(_length); + break; + case PayLoad: + _data.append(byte); + --_length; + if (_length > 0) + break; + _readState = Header; + Frame frame(_header, _data); + emit received(frame); + break; + } + } +} + +void QMQTT::Network::onDisconnected() +{ + emit disconnected(); + if(_autoReconnect) + { + _autoReconnectTimer->start(); + } +} + +#ifndef QT_NO_SSL +void QMQTT::Network::ignoreSslErrors(const QList& errors) +{ + _socket->ignoreSslErrors(errors); +} + +void QMQTT::Network::ignoreSslErrors() +{ + _socket->ignoreSslErrors(); +} + +QSslConfiguration QMQTT::Network::sslConfiguration() const +{ + return _socket->sslConfiguration(); +} + +void QMQTT::Network::setSslConfiguration(const QSslConfiguration& config) +{ + _socket->setSslConfiguration(config); +} +#endif // QT_NO_SSL diff --git a/mqtt/qmqtt_network_p.h b/mqtt/qmqtt_network_p.h new file mode 100644 index 0000000..8dc77fc --- /dev/null +++ b/mqtt/qmqtt_network_p.h @@ -0,0 +1,139 @@ +/* + * qmqtt_network_p.h - qmqtt network private header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_NETWORK_P_H +#define QMQTT_NETWORK_P_H + +#include "qmqtt_networkinterface.h" + +#include +#include +#include +#include + +#ifdef QT_WEBSOCKETS_LIB +#include +#endif // QT_WEBSOCKETS_LIB + +#ifndef QT_NO_SSL +#include +QT_FORWARD_DECLARE_CLASS(QSslError) +#endif // QT_NO_SSL + +namespace QMQTT { + +class SocketInterface; +class TimerInterface; +class Frame; + +class Network : public NetworkInterface +{ + Q_OBJECT + +public: + Network(QObject* parent = NULL); +#ifndef QT_NO_SSL + Network(const QSslConfiguration& config, QObject* parent = NULL); +#endif // QT_NO_SSL +#ifdef QT_WEBSOCKETS_LIB +#ifndef QT_NO_SSL + Network(const QString& origin, + QWebSocketProtocol::Version version, + const QSslConfiguration* sslConfig, + QObject* parent = NULL); +#endif // QT_NO_SSL + Network(const QString& origin, + QWebSocketProtocol::Version version, + QObject* parent = NULL); +#endif // QT_WEBSOCKETS_LIB + Network(SocketInterface* socketInterface, TimerInterface* timerInterface, + QObject* parent = NULL); + ~Network(); + + void sendFrame(const Frame &frame); + bool isConnectedToHost() const; + bool autoReconnect() const; + void setAutoReconnect(const bool autoReconnect); + QAbstractSocket::SocketState state() const; + int autoReconnectInterval() const; + void setAutoReconnectInterval(const int autoReconnectInterval); +#ifndef QT_NO_SSL + void ignoreSslErrors(const QList& errors); + QSslConfiguration sslConfiguration() const; + void setSslConfiguration(const QSslConfiguration& config); +#endif // QT_NO_SSL + +public slots: + void connectToHost(const QHostAddress& host, const quint16 port); + void connectToHost(const QString& hostName, const quint16 port); + void disconnectFromHost(); +#ifndef QT_NO_SSL + void ignoreSslErrors(); +#endif // QT_NO_SSL + +protected slots: + void onSocketError(QAbstractSocket::SocketError socketError); + +protected: + void initialize(); + + quint16 _port; + QHostAddress _host; + QString _hostName; + bool _autoReconnect; + int _autoReconnectInterval; + SocketInterface* _socket; + TimerInterface* _autoReconnectTimer; + + enum ReadState { + Header, + Length, + PayLoad + }; + + ReadState _readState; + quint8 _header; + int _length; + int _shift; + QByteArray _data; + +protected slots: + void onSocketReadReady(); + void onDisconnected(); + void connectToHost(); + +private: + Q_DISABLE_COPY(Network) +}; + +} // namespace QMQTT + +#endif // QMQTT_NETWORK_P_H diff --git a/mqtt/qmqtt_networkinterface.h b/mqtt/qmqtt_networkinterface.h new file mode 100644 index 0000000..c2d927d --- /dev/null +++ b/mqtt/qmqtt_networkinterface.h @@ -0,0 +1,92 @@ +/* + * qmqtt_networkinterface.h - qmqtt network interface header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_NETWORK_INTERFACE_H +#define QMQTT_NETWORK_INTERFACE_H + +#include "qmqtt_global.h" + +#include +#include +#include +#include +#include + +#ifndef QT_NO_SSL +#include +QT_FORWARD_DECLARE_CLASS(QSslError) +#endif // QT_NO_SSL + +namespace QMQTT { + +class Frame; + +class Q_MQTT_EXPORT NetworkInterface : public QObject +{ + Q_OBJECT +public: + explicit NetworkInterface(QObject* parent = NULL) : QObject(parent) {} + virtual ~NetworkInterface() {} + + virtual void sendFrame(const Frame& frame) = 0; + virtual bool isConnectedToHost() const = 0; + virtual bool autoReconnect() const = 0; + virtual void setAutoReconnect(const bool autoReconnect) = 0; + virtual int autoReconnectInterval() const = 0; + virtual void setAutoReconnectInterval(const int autoReconnectInterval) = 0; + virtual QAbstractSocket::SocketState state() const = 0; +#ifndef QT_NO_SSL + virtual void ignoreSslErrors(const QList& errors) = 0; + virtual QSslConfiguration sslConfiguration() const = 0; + virtual void setSslConfiguration(const QSslConfiguration& config) = 0; +#endif // QT_NO_SSL + +public slots: + virtual void connectToHost(const QHostAddress& host, const quint16 port) = 0; + virtual void connectToHost(const QString& hostName, const quint16 port) = 0; + virtual void disconnectFromHost() = 0; +#ifndef QT_NO_SSL + virtual void ignoreSslErrors() = 0; +#endif // QT_NO_SSL + +signals: + void connected(); + void disconnected(); + void received(const QMQTT::Frame& frame); + void error(QAbstractSocket::SocketError error); +#ifndef QT_NO_SSL + void sslErrors(const QList& errors); +#endif // QT_NO_SSL +}; + +} // namespace QMQTT + +#endif // QMQTT_NETWORK_INTERFACE_H diff --git a/mqtt/qmqtt_routedmessage.h b/mqtt/qmqtt_routedmessage.h new file mode 100644 index 0000000..54af227 --- /dev/null +++ b/mqtt/qmqtt_routedmessage.h @@ -0,0 +1,71 @@ +/* + * qmqtt_router.h - qmqtt router + * + * Copyright (c) 2013 Ery Lee + * Router added by Niklas Wulf + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_ROUTEDMESSAGE_H +#define QMQTT_ROUTEDMESSAGE_H + +#include "qmqtt_message.h" + +#include +#include +#include + +namespace QMQTT { + +class RouteSubscription; + +class Q_MQTT_EXPORT RoutedMessage +{ +public: + inline RoutedMessage() + {} + inline RoutedMessage(const Message &message) + : _message(message) + {} + + inline const Message &message() const + { return _message; } + inline QHash parameters() const + { return _parameters; } + +private: + friend class RouteSubscription; + + Message _message; + QHash _parameters; +}; + +} // namespace QMQTT + +Q_DECLARE_METATYPE(QMQTT::RoutedMessage) + +#endif // QMQTT_ROUTEDMESSAGE_H diff --git a/mqtt/qmqtt_router.cpp b/mqtt/qmqtt_router.cpp new file mode 100644 index 0000000..f8fe309 --- /dev/null +++ b/mqtt/qmqtt_router.cpp @@ -0,0 +1,67 @@ +/* + * qmqtt_router.cpp - qmqtt router + * + * Copyright (c) 2013 Ery Lee + * Router added by Niklas Wulf + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "qmqtt_router.h" +#include "qmqtt_client.h" +#include "qmqtt_routesubscription.h" + +#include + +namespace QMQTT { + +Q_LOGGING_CATEGORY(router, "qmqtt.router") + +Router::Router(Client *parent) : QObject(parent), _client(parent) +{ +} + +RouteSubscription *Router::subscribe(const QString &route) +{ + RouteSubscription *subscription = new RouteSubscription(this); + subscription->setRoute(route); + connect(_client, &Client::connected, subscription, [this, subscription]() { + _client->subscribe(subscription->_topic, 0); + }); + if (_client->isConnectedToHost()) + _client->subscribe(subscription->_topic, 0); + connect(_client, &Client::received, subscription, &RouteSubscription::routeMessage); + return subscription; +} + +Client *Router::client() const +{ + return _client; +} + +} // namespace QMQTT + diff --git a/mqtt/qmqtt_router.h b/mqtt/qmqtt_router.h new file mode 100644 index 0000000..84988b0 --- /dev/null +++ b/mqtt/qmqtt_router.h @@ -0,0 +1,60 @@ +/* + * qmqtt_router.h - qmqtt router + * + * Copyright (c) 2013 Ery Lee + * Router added by Niklas Wulf + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_ROUTER_H +#define QMQTT_ROUTER_H + +#include "qmqtt_global.h" + +#include + +namespace QMQTT { + +class Client; +class RouteSubscription; + +class Q_MQTT_EXPORT Router : public QObject +{ + Q_OBJECT +public: + explicit Router(Client *parent = 0); + + RouteSubscription *subscribe(const QString &route); + Client *client() const; + +private: + Client *_client; +}; + +} // namespace QMQTT + +#endif // QMQTT_ROUTER_H diff --git a/mqtt/qmqtt_routesubscription.cpp b/mqtt/qmqtt_routesubscription.cpp new file mode 100644 index 0000000..81c4f93 --- /dev/null +++ b/mqtt/qmqtt_routesubscription.cpp @@ -0,0 +1,117 @@ +/* + * qmqtt_router.cpp - qmqtt router + * + * Copyright (c) 2013 Ery Lee + * Router added by Niklas Wulf + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "qmqtt_routesubscription.h" +#include "qmqtt_router.h" +#include "qmqtt_client.h" +#include "qmqtt_routedmessage.h" + +#include +#include +#include +#include +#include + +namespace QMQTT { + +Q_LOGGING_CATEGORY(routerSubscription, "qmqtt.routersubscription") + +RouteSubscription::RouteSubscription(Router *parent) + : QObject(parent), + _client(parent->client()) +{ + Q_ASSERT(!_client.isNull()); +} + +RouteSubscription::~RouteSubscription() +{ + if (Q_LIKELY(!_client.isNull() && _client->isConnectedToHost())) + _client->unsubscribe(_topic); +} + +QString RouteSubscription::route() const +{ + return _topic; +} + +void RouteSubscription::setRoute(const QString &route) +{ + QRegularExpression parameterNamesRegExp(QStringLiteral("\\:([a-zA-Z0-9]+)")); // note how names must not contain dashes or underscores + + // Remove paramter names to get the actual topic "route" + QString topic = route; + topic.remove(parameterNamesRegExp); + + // Remove the MQTT wildcards to get a regular expression, which matches the parameters + QString parameterRegExp = route; + parameterRegExp + .remove(QLatin1Char('+')) + .replace(parameterNamesRegExp, QStringLiteral("([a-zA-Z0-9_-]+)")) // note how parameter values may contain dashes or underscores + .remove(QLatin1Char('#')) + .replace(QLatin1String("$"), QLatin1String("\\$")); + + // Extract the parameter names + QRegularExpressionMatchIterator it = parameterNamesRegExp.globalMatch(route); + QStringList names; + while(it.hasNext()) { + QRegularExpressionMatch match = it.next(); + QString parameterName = match.captured(1); + names << parameterName; + } + + _topic = topic; + _parameterNames = names; + _regularExpression = QRegularExpression(parameterRegExp); +} + +void RouteSubscription::routeMessage(const Message &message) +{ + QString topic = message.topic(); + QRegularExpressionMatch match = _regularExpression.match(topic); + if(!match.hasMatch()) { + return; + } + + RoutedMessage routedMessage(message); + + for(int i = 0, c = _parameterNames.size(); i < c; ++i) { + QString name = _parameterNames.at(i); + QString value = match.captured(i + 1); + + routedMessage._parameters.insert(name, value); + } + + emit received(routedMessage); +} + +} // namespace QMQTT diff --git a/mqtt/qmqtt_routesubscription.h b/mqtt/qmqtt_routesubscription.h new file mode 100644 index 0000000..bd0608f --- /dev/null +++ b/mqtt/qmqtt_routesubscription.h @@ -0,0 +1,79 @@ +/* + * qmqtt_router.h - qmqtt router + * + * Copyright (c) 2013 Ery Lee + * Router added by Niklas Wulf + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_ROUTESUBSCRIPTION_H +#define QMQTT_ROUTESUBSCRIPTION_H + +#include "qmqtt_global.h" + +#include +#include +#include +#include +#include + +namespace QMQTT { + +class Client; +class Message; +class RoutedMessage; +class Router; + +class Q_MQTT_EXPORT RouteSubscription : public QObject +{ + Q_OBJECT +public: + ~RouteSubscription(); + + QString route() const; + +signals: + void received(const RoutedMessage &message); + +private slots: + void routeMessage(const Message &message); + +private: + friend class Router; + + explicit RouteSubscription(Router *parent = 0); + void setRoute(const QString &route); + + QPointer _client; + QString _topic; + QRegularExpression _regularExpression; + QStringList _parameterNames; +}; + +} // namespace QMQTT + +#endif // QMQTT_ROUTESUBSCRIPTION_H diff --git a/mqtt/qmqtt_socket.cpp b/mqtt/qmqtt_socket.cpp new file mode 100644 index 0000000..f0628f0 --- /dev/null +++ b/mqtt/qmqtt_socket.cpp @@ -0,0 +1,81 @@ +/* + * qmqtt_socket.cpp - qmqtt socket + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "qmqtt_socket_p.h" + +#include + +QMQTT::Socket::Socket(QObject* parent) + : SocketInterface(parent) + , _socket(new QTcpSocket(this)) +{ + connect(_socket.data(), &QTcpSocket::connected, this, &SocketInterface::connected); + connect(_socket.data(), &QTcpSocket::disconnected, this, &SocketInterface::disconnected); + connect(_socket.data(), + static_cast(&QTcpSocket::error), + this, + static_cast(&SocketInterface::error)); +} + +QMQTT::Socket::~Socket() +{ +} + +QIODevice *QMQTT::Socket::ioDevice() +{ + return _socket.data(); +} + +void QMQTT::Socket::connectToHost(const QHostAddress& address, quint16 port) +{ + _socket->connectToHost(address, port); +} + +void QMQTT::Socket::connectToHost(const QString& hostName, quint16 port) +{ + _socket->connectToHost(hostName, port); +} + +void QMQTT::Socket::disconnectFromHost() +{ + _socket->disconnectFromHost(); +} + +QAbstractSocket::SocketState QMQTT::Socket::state() const +{ + return _socket->state(); +} + +QAbstractSocket::SocketError QMQTT::Socket::error() const +{ + return _socket->error(); +} diff --git a/mqtt/qmqtt_socket_p.h b/mqtt/qmqtt_socket_p.h new file mode 100644 index 0000000..248a42d --- /dev/null +++ b/mqtt/qmqtt_socket_p.h @@ -0,0 +1,69 @@ +/* + * qmqtt_socket_p.h - qmqtt socket private header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_SOCKET_P_H +#define QMQTT_SOCKET_P_H + +#include "qmqtt_socketinterface.h" + +#include +#include +#include +#include +#include + +QT_FORWARD_DECLARE_CLASS(QIODevice) +QT_FORWARD_DECLARE_CLASS(QTcpSocket) + +namespace QMQTT +{ + +class Socket : public SocketInterface +{ + Q_OBJECT +public: + explicit Socket(QObject* parent = NULL); + virtual ~Socket(); + + virtual QIODevice *ioDevice(); + void connectToHost(const QHostAddress& address, quint16 port); + void connectToHost(const QString& hostName, quint16 port); + void disconnectFromHost(); + QAbstractSocket::SocketState state() const; + QAbstractSocket::SocketError error() const; + +protected: + QScopedPointer _socket; +}; + +} + +#endif // QMQTT_SOCKET_P_H diff --git a/mqtt/qmqtt_socketinterface.h b/mqtt/qmqtt_socketinterface.h new file mode 100644 index 0000000..97965b1 --- /dev/null +++ b/mqtt/qmqtt_socketinterface.h @@ -0,0 +1,84 @@ +/* + * qmqtt_socketinterface.h - qmqtt socket interface header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_SOCKET_INTERFACE_H +#define QMQTT_SOCKET_INTERFACE_H + +#include "qmqtt_global.h" + +#include +#include +#include +#include +#include + +#ifndef QT_NO_SSL +#include +QT_FORWARD_DECLARE_CLASS(QSslError) +#endif // QT_NO_SSL + +QT_FORWARD_DECLARE_CLASS(QIODevice) + +namespace QMQTT +{ + +class Q_MQTT_EXPORT SocketInterface : public QObject +{ + Q_OBJECT +public: + explicit SocketInterface(QObject* parent = NULL) : QObject(parent) {} + virtual ~SocketInterface() {} + + virtual QIODevice* ioDevice() = 0; + virtual void connectToHost(const QHostAddress& address, quint16 port) = 0; + virtual void connectToHost(const QString& hostName, quint16 port) = 0; + virtual void disconnectFromHost() = 0; + virtual QAbstractSocket::SocketState state() const = 0; + virtual QAbstractSocket::SocketError error() const = 0; +#ifndef QT_NO_SSL + virtual void ignoreSslErrors(const QList& errors) { Q_UNUSED(errors); } + virtual void ignoreSslErrors() {} + virtual QSslConfiguration sslConfiguration() const { return QSslConfiguration(); } + virtual void setSslConfiguration(const QSslConfiguration& config) { Q_UNUSED(config); } +#endif // QT_NO_SSL + +signals: + void connected(); + void disconnected(); + void error(QAbstractSocket::SocketError socketError); +#ifndef QT_NO_SSL + void sslErrors(const QList& errors); +#endif // QT_NO_SSL +}; + +} + +#endif // QMQTT_SOCKET_INTERFACE_H diff --git a/mqtt/qmqtt_ssl_socket.cpp b/mqtt/qmqtt_ssl_socket.cpp new file mode 100644 index 0000000..2d3a808 --- /dev/null +++ b/mqtt/qmqtt_ssl_socket.cpp @@ -0,0 +1,112 @@ +/* + * qmqtt_ssl_socket.cpp - qmqtt SSL socket + * + * Copyright (c) 2013 Ery Lee + * Copyright (c) 2016 Matthias Dieter Wallnöfer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "qmqtt_ssl_socket_p.h" + +#ifndef QT_NO_SSL + +#include +#include + +QMQTT::SslSocket::SslSocket(const QSslConfiguration& config, QObject* parent) + : SocketInterface(parent) + , _socket(new QSslSocket(this)) +{ + _socket->setSslConfiguration(config); + connect(_socket.data(), &QSslSocket::encrypted, this, &SocketInterface::connected); + connect(_socket.data(), &QSslSocket::disconnected, this, &SocketInterface::disconnected); + connect(_socket.data(), + static_cast(&QSslSocket::error), + this, + static_cast(&SocketInterface::error)); + connect(_socket.data(), + static_cast&)>(&QSslSocket::sslErrors), + this, + &SslSocket::sslErrors); +} + +QMQTT::SslSocket::~SslSocket() +{ +} + +QIODevice *QMQTT::SslSocket::ioDevice() +{ + return _socket.data(); +} + +void QMQTT::SslSocket::connectToHost(const QHostAddress& address, quint16 port) +{ + _socket->connectToHostEncrypted(address.toString(), port); +} + +void QMQTT::SslSocket::connectToHost(const QString& hostName, quint16 port) +{ + _socket->connectToHostEncrypted(hostName, port); +} + +void QMQTT::SslSocket::disconnectFromHost() +{ + _socket->disconnectFromHost(); +} + +QAbstractSocket::SocketState QMQTT::SslSocket::state() const +{ + return _socket->state(); +} + +QAbstractSocket::SocketError QMQTT::SslSocket::error() const +{ + return _socket->error(); +} + +void QMQTT::SslSocket::ignoreSslErrors(const QList& errors) +{ + _socket->ignoreSslErrors(errors); +} + +void QMQTT::SslSocket::ignoreSslErrors() +{ + _socket->ignoreSslErrors(); +} + +QSslConfiguration QMQTT::SslSocket::sslConfiguration() const +{ + return _socket->sslConfiguration(); +} + +void QMQTT::SslSocket::setSslConfiguration(const QSslConfiguration& config) +{ + _socket->setSslConfiguration(config); +} + +#endif // QT_NO_SSL diff --git a/mqtt/qmqtt_ssl_socket_p.h b/mqtt/qmqtt_ssl_socket_p.h new file mode 100644 index 0000000..46ddc33 --- /dev/null +++ b/mqtt/qmqtt_ssl_socket_p.h @@ -0,0 +1,79 @@ +/* + * qmqtt_ssl_socket_p.h - qmqtt SSL socket private header + * + * Copyright (c) 2013 Ery Lee + * Copyright (c) 2016 Matthias Dieter Wallnöfer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_SSL_SOCKET_P_H +#define QMQTT_SSL_SOCKET_P_H + +#ifndef QT_NO_SSL + +#include "qmqtt_socketinterface.h" + +#include +#include +#include +#include +#include + +QT_FORWARD_DECLARE_CLASS(QSslSocket) +QT_FORWARD_DECLARE_CLASS(QSslError) +#include + +namespace QMQTT +{ + +class SslSocket : public SocketInterface +{ + Q_OBJECT +public: + explicit SslSocket(const QSslConfiguration& config, QObject* parent = NULL); + virtual ~SslSocket(); + + virtual QIODevice *ioDevice(); + void connectToHost(const QHostAddress& address, quint16 port); + void connectToHost(const QString& hostName, quint16 port); + void disconnectFromHost(); + QAbstractSocket::SocketState state() const; + QAbstractSocket::SocketError error() const; + void ignoreSslErrors(const QList& errors); + void ignoreSslErrors(); + QSslConfiguration sslConfiguration() const; + void setSslConfiguration(const QSslConfiguration& config); + +protected: + QScopedPointer _socket; +}; + +} + +#endif // QT_NO_SSL + +#endif // QMQTT_SSL_SOCKET_P_H diff --git a/mqtt/qmqtt_timer.cpp b/mqtt/qmqtt_timer.cpp new file mode 100644 index 0000000..a971d83 --- /dev/null +++ b/mqtt/qmqtt_timer.cpp @@ -0,0 +1,73 @@ +/* + * qmqtt_timer.cpp - qmqtt timer + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "qmqtt_timer_p.h" + +QMQTT::Timer::Timer(QObject* parent) + : TimerInterface(parent) +{ + connect(&_timer, &QTimer::timeout, this, &TimerInterface::timeout); +} + +QMQTT::Timer::~Timer() +{ +} + +bool QMQTT::Timer::isSingleShot() const +{ + return _timer.isSingleShot(); +} + +void QMQTT::Timer::setSingleShot(bool singleShot) +{ + _timer.setSingleShot(singleShot); +} + +int QMQTT::Timer::interval() const +{ + return _timer.interval(); +} + +void QMQTT::Timer::setInterval(int msec) +{ + _timer.setInterval(msec); +} + +void QMQTT::Timer::start() +{ + _timer.start(); +} + +void QMQTT::Timer::stop() +{ + _timer.stop(); +} diff --git a/mqtt/qmqtt_timer_p.h b/mqtt/qmqtt_timer_p.h new file mode 100644 index 0000000..093a141 --- /dev/null +++ b/mqtt/qmqtt_timer_p.h @@ -0,0 +1,62 @@ +/* + * qmqtt_timer_p.h - qmqtt timer private header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_TIMER_P_H +#define QMQTT_TIMER_P_H + +#include "qmqtt_timerinterface.h" + +#include +#include + +namespace QMQTT { + +class Timer : public TimerInterface +{ + Q_OBJECT +public: + explicit Timer(QObject *parent = 0); + virtual ~Timer(); + + bool isSingleShot() const; + void setSingleShot(bool singleShot); + int interval() const; + void setInterval(int msec); + void start(); + void stop(); + +protected: + QTimer _timer; +}; + +} + +#endif // QMQTT_TIMER_P_H diff --git a/mqtt/qmqtt_timerinterface.h b/mqtt/qmqtt_timerinterface.h new file mode 100644 index 0000000..4f2cfcb --- /dev/null +++ b/mqtt/qmqtt_timerinterface.h @@ -0,0 +1,62 @@ +/* + * qmqtt_timerinterface.h - qmqtt timer interface header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_TIMER_INTERFACE_H +#define QMQTT_TIMER_INTERFACE_H + +#include "qmqtt_global.h" + +#include + +namespace QMQTT { + +class Q_MQTT_EXPORT TimerInterface : public QObject +{ + Q_OBJECT +public: + explicit TimerInterface(QObject* parent = NULL) : QObject(parent) {} + virtual ~TimerInterface() {} + + virtual bool isSingleShot() const = 0; + virtual void setSingleShot(bool singleShot) = 0; + virtual int interval() const = 0; + virtual void setInterval(int msec) = 0; + virtual void start() = 0; + virtual void stop() = 0; + +signals: + void timeout(); +}; + +} + +#endif // QMQTT_TIMER_INTERFACE_H + diff --git a/mqtt/qmqtt_websocket.cpp b/mqtt/qmqtt_websocket.cpp new file mode 100644 index 0000000..c824786 --- /dev/null +++ b/mqtt/qmqtt_websocket.cpp @@ -0,0 +1,103 @@ +#ifdef QT_WEBSOCKETS_LIB + +#include "qmqtt_websocket_p.h" + +#include +#include +#include + +#ifndef QT_NO_SSL +QMQTT::WebSocket::WebSocket(const QString& origin, + QWebSocketProtocol::Version version, + const QSslConfiguration* sslConfig, + QObject* parent) + : SocketInterface(parent) + , _socket(new QWebSocket(origin, version, this)) + , _ioDevice(new WebSocketIODevice(_socket, this)) +{ + initialize(); + if (sslConfig != NULL) + _socket->setSslConfiguration(*sslConfig); + connect(_socket, &QWebSocket::sslErrors, this, &WebSocket::sslErrors); +} +#endif // QT_NO_SSL + +QMQTT::WebSocket::WebSocket(const QString& origin, + QWebSocketProtocol::Version version, + QObject* parent) + : SocketInterface(parent) + , _socket(new QWebSocket(origin, version, this)) + , _ioDevice(new WebSocketIODevice(_socket, this)) +{ + initialize(); +} + +void QMQTT::WebSocket::initialize() +{ + connect(_socket, &QWebSocket::connected, this, &WebSocket::connected); + connect(_socket, &QWebSocket::disconnected, this, &WebSocket::disconnected); + connect(_socket, + static_cast(&QWebSocket::error), + this, + static_cast(&SocketInterface::error)); +} + +QMQTT::WebSocket::~WebSocket() +{ +} + +void QMQTT::WebSocket::connectToHost(const QHostAddress& address, quint16 port) +{ + Q_UNUSED(address) + Q_UNUSED(port) + qFatal("No supported"); +} + +void QMQTT::WebSocket::connectToHost(const QString& hostName, quint16 port) +{ + Q_UNUSED(port) + QUrl url(hostName); + QNetworkRequest request(url); + request.setRawHeader("Sec-WebSocket-Protocol", "mqtt"); + _ioDevice->connectToHost(request); +} + +void QMQTT::WebSocket::disconnectFromHost() +{ + _socket->close(); +} + +QAbstractSocket::SocketState QMQTT::WebSocket::state() const +{ + return _socket->state(); +} + +QAbstractSocket::SocketError QMQTT::WebSocket::error() const +{ + return _socket->error(); +} + +#ifndef QT_NO_SSL +void QMQTT::WebSocket::ignoreSslErrors(const QList& errors) +{ + _socket->ignoreSslErrors(errors); +} + +void QMQTT::WebSocket::ignoreSslErrors() +{ + _socket->ignoreSslErrors(); +} + +QSslConfiguration QMQTT::WebSocket::sslConfiguration() const +{ + return _socket->sslConfiguration(); +} + +void QMQTT::WebSocket::setSslConfiguration(const QSslConfiguration& config) +{ + _socket->setSslConfiguration(config); +} + +#endif // QT_NO_SSL + +#endif // QT_WEBSOCKETS_LIB diff --git a/mqtt/qmqtt_websocket_p.h b/mqtt/qmqtt_websocket_p.h new file mode 100644 index 0000000..6d159fa --- /dev/null +++ b/mqtt/qmqtt_websocket_p.h @@ -0,0 +1,102 @@ +/* + * qmqtt_websocket_p.h - qmqtt socket private header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +#ifndef QMQTT_WEBSOCKET_H +#define QMQTT_WEBSOCKET_H + +#ifdef QT_WEBSOCKETS_LIB + +#include +#include + +#include +#include +#include +#include +#include +#include + +QT_FORWARD_DECLARE_CLASS(QIODevice) + +#ifndef QT_NO_SSL +#include +QT_FORWARD_DECLARE_CLASS(QSslError) +#endif // QT_NO_SSL + +namespace QMQTT +{ + +class WebSocket : public SocketInterface +{ + Q_OBJECT +public: +#ifndef QT_NO_SSL + WebSocket(const QString& origin, + QWebSocketProtocol::Version version, + const QSslConfiguration* sslConfig, + QObject* parent = NULL); +#endif // QT_NO_SSL + + WebSocket(const QString& origin, + QWebSocketProtocol::Version version, + QObject* parent = NULL); + + virtual ~WebSocket(); + + QIODevice *ioDevice() + { + return _ioDevice; + } + + void connectToHost(const QHostAddress& address, quint16 port); + void connectToHost(const QString& hostName, quint16 port); + void disconnectFromHost(); + QAbstractSocket::SocketState state() const; + QAbstractSocket::SocketError error() const; +#ifndef QT_NO_SSL + void ignoreSslErrors(const QList& errors); + void ignoreSslErrors(); + QSslConfiguration sslConfiguration() const; + void setSslConfiguration(const QSslConfiguration& config); +#endif // QT_NO_SSL + +private: + void initialize(); + + QWebSocket *_socket; + WebSocketIODevice *_ioDevice; +}; + +} + +#endif // QT_WEBSOCKETS_LIB + +#endif // QMQTT_WEBSOCKET_H diff --git a/mqtt/qmqtt_websocketiodevice.cpp b/mqtt/qmqtt_websocketiodevice.cpp new file mode 100644 index 0000000..478d3c3 --- /dev/null +++ b/mqtt/qmqtt_websocketiodevice.cpp @@ -0,0 +1,48 @@ +#ifdef QT_WEBSOCKETS_LIB + +#include "qmqtt_websocketiodevice_p.h" + +#include + +QMQTT::WebSocketIODevice::WebSocketIODevice(QWebSocket *socket, QObject *parent) + : QIODevice(parent) + , _webSocket(socket) +{ + connect(_webSocket, &QWebSocket::bytesWritten, this, &WebSocketIODevice::bytesWritten); + connect(_webSocket, &QWebSocket::binaryMessageReceived, + this, &WebSocketIODevice::binaryMessageReceived); +} + +bool QMQTT::WebSocketIODevice::connectToHost(const QNetworkRequest &request) +{ + _webSocket->open(request); + return QIODevice::open(QIODevice::ReadWrite); +} + +qint64 QMQTT::WebSocketIODevice::bytesAvailable() const +{ + return _buffer.count(); +} + +void QMQTT::WebSocketIODevice::binaryMessageReceived(const QByteArray &frame) +{ + _buffer.append(frame); + emit readyRead(); +} + +qint64 QMQTT::WebSocketIODevice::readData(char *data, qint64 maxSize) +{ + int size = qMin(static_cast(maxSize), _buffer.count()); + for (int i=0; i(maxSize)); + return _webSocket->sendBinaryMessage(d); +} + +#endif // QT_WEBSOCKETS_LIB diff --git a/mqtt/qmqtt_websocketiodevice_p.h b/mqtt/qmqtt_websocketiodevice_p.h new file mode 100644 index 0000000..e936220 --- /dev/null +++ b/mqtt/qmqtt_websocketiodevice_p.h @@ -0,0 +1,86 @@ +/* + * qmqtt_socketinterface.h - qmqtt socket interface header + * + * Copyright (c) 2013 Ery Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of mqttc nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#ifndef QMQTT_WEBSOCKETIODEVICE_H +#define QMQTT_WEBSOCKETIODEVICE_H + +#ifdef QT_WEBSOCKETS_LIB + +#include +#include +#include +#include + +QT_FORWARD_DECLARE_CLASS(QWebSocket) +QT_FORWARD_DECLARE_CLASS(QNetworkRequest) +QT_FORWARD_DECLARE_CLASS(QSslError) + +namespace QMQTT +{ + +class WebSocketIODevice : public QIODevice +{ + Q_OBJECT +public: + explicit WebSocketIODevice(QWebSocket *socket, QObject *parent = NULL); + + bool connectToHost(const QNetworkRequest &request); + + virtual qint64 bytesAvailable() const; + +signals: + void connected(); + + void disconnected(); + + void error(QAbstractSocket::SocketError error); + + void sslErrors(const QList &errors); + +protected: + virtual qint64 readData(char *data, qint64 maxSize); + + virtual qint64 writeData(const char *data, qint64 maxSize); + +private slots: + void binaryMessageReceived(const QByteArray &frame); + +private: + QByteArray _buffer; + QWebSocket *_webSocket; +}; + +} + +#endif // QT_WEBSOCKETS_LIB + +#endif // QMQTT_WEBSOCKETIODEVICE_H diff --git a/simplecrypt.cpp b/simplecrypt.cpp new file mode 100644 index 0000000..79a16ba --- /dev/null +++ b/simplecrypt.cpp @@ -0,0 +1,252 @@ +/* +Copyright (c) 2011, Andre Somers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Rathenau Instituut, Andre Somers nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR #######; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#include "simplecrypt.h" +#include +#include +#include +#include +#include +#include + +SimpleCrypt::SimpleCrypt(): + m_key(0), + m_compressionMode(CompressionAuto), + m_protectionMode(ProtectionChecksum), + m_lastError(ErrorNoError) +{ + qsrand(uint(QDateTime::currentMSecsSinceEpoch() & 0xFFFF)); +} + +SimpleCrypt::SimpleCrypt(quint64 key): + m_key(key), + m_compressionMode(CompressionAuto), + m_protectionMode(ProtectionChecksum), + m_lastError(ErrorNoError) +{ + qsrand(uint(QDateTime::currentMSecsSinceEpoch() & 0xFFFF)); + splitKey(); +} + +void SimpleCrypt::setKey(quint64 key) +{ + m_key = key; + splitKey(); +} + +void SimpleCrypt::splitKey() +{ + m_keyParts.clear(); + m_keyParts.resize(8); + for (int i=0;i<8;i++) { + quint64 part = m_key; + for (int j=i; j>0; j--) + part = part >> 8; + part = part & 0xff; + m_keyParts[i] = static_cast(part); + } +} + +QByteArray SimpleCrypt::encryptToByteArray(const QString& plaintext) +{ + QByteArray plaintextArray = plaintext.toUtf8(); + return encryptToByteArray(plaintextArray); +} + +QByteArray SimpleCrypt::encryptToByteArray(QByteArray plaintext) +{ + if (m_keyParts.isEmpty()) { + qWarning() << "No key set."; + m_lastError = ErrorNoKeySet; + return QByteArray(); + } + + + QByteArray ba = plaintext; + + CryptoFlags flags = CryptoFlagNone; + if (m_compressionMode == CompressionAlways) { + ba = qCompress(ba, 9); //maximum compression + flags |= CryptoFlagCompression; + } else if (m_compressionMode == CompressionAuto) { + QByteArray compressed = qCompress(ba, 9); + if (compressed.count() < ba.count()) { + ba = compressed; + flags |= CryptoFlagCompression; + } + } + + QByteArray integrityProtection; + if (m_protectionMode == ProtectionChecksum) { + flags |= CryptoFlagChecksum; + QDataStream s(&integrityProtection, QIODevice::WriteOnly); + s << qChecksum(ba.constData(), ba.length()); + } else if (m_protectionMode == ProtectionHash) { + flags |= CryptoFlagHash; + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(ba); + + integrityProtection += hash.result(); + } + + //prepend a random char to the string + char randomChar = char(qrand() & 0xFF); + ba = randomChar + integrityProtection + ba; + + int pos(0); + char lastChar(0); + + int cnt = ba.count(); + + while (pos < cnt) { + ba[pos] = ba.at(pos) ^ m_keyParts.at(pos % 8) ^ lastChar; + lastChar = ba.at(pos); + ++pos; + } + + QByteArray resultArray; + resultArray.append(char(0x03)); //version for future updates to algorithm + resultArray.append(char(flags)); //encryption flags + resultArray.append(ba); + + m_lastError = ErrorNoError; + return resultArray; +} + +QString SimpleCrypt::encryptToString(const QString& plaintext) +{ + QByteArray plaintextArray = plaintext.toUtf8(); + QByteArray cypher = encryptToByteArray(plaintextArray); + QString cypherString = QString::fromLatin1(cypher.toBase64()); + return cypherString; +} + +QString SimpleCrypt::encryptToString(QByteArray plaintext) +{ + QByteArray cypher = encryptToByteArray(plaintext); + QString cypherString = QString::fromLatin1(cypher.toBase64()); + return cypherString; +} + +QString SimpleCrypt::decryptToString(const QString &cyphertext) +{ + QByteArray cyphertextArray = QByteArray::fromBase64(cyphertext.toLatin1()); + QByteArray plaintextArray = decryptToByteArray(cyphertextArray); + QString plaintext = QString::fromUtf8(plaintextArray, plaintextArray.size()); + + return plaintext; +} + +QString SimpleCrypt::decryptToString(QByteArray cypher) +{ + QByteArray ba = decryptToByteArray(cypher); + QString plaintext = QString::fromUtf8(ba, ba.size()); + + return plaintext; +} + +QByteArray SimpleCrypt::decryptToByteArray(const QString& cyphertext) +{ + QByteArray cyphertextArray = QByteArray::fromBase64(cyphertext.toLatin1()); + QByteArray ba = decryptToByteArray(cyphertextArray); + + return ba; +} + +QByteArray SimpleCrypt::decryptToByteArray(QByteArray cypher) +{ + if (m_keyParts.isEmpty()) { + qWarning() << "No key set."; + m_lastError = ErrorNoKeySet; + return QByteArray(); + } + + QByteArray ba = cypher; + + if( cypher.count() < 3 ) + return QByteArray(); + + char version = ba.at(0); + + if (version !=3) { //we only work with version 3 + m_lastError = ErrorUnknownVersion; + qWarning() << "Invalid version or not a cyphertext."; + return QByteArray(); + } + + CryptoFlags flags = CryptoFlags(ba.at(1)); + + ba = ba.mid(2); + int pos(0); + int cnt(ba.count()); + char lastChar = 0; + + while (pos < cnt) { + char currentChar = ba[pos]; + ba[pos] = ba.at(pos) ^ lastChar ^ m_keyParts.at(pos % 8); + lastChar = currentChar; + ++pos; + } + + ba = ba.mid(1); //chop off the random number at the start + + bool integrityOk(true); + if (flags.testFlag(CryptoFlagChecksum)) { + if (ba.length() < 2) { + m_lastError = ErrorIntegrityFailed; + return QByteArray(); + } + quint16 storedChecksum; + { + QDataStream s(&ba, QIODevice::ReadOnly); + s >> storedChecksum; + } + ba = ba.mid(2); + quint16 checksum = qChecksum(ba.constData(), ba.length()); + integrityOk = (checksum == storedChecksum); + } else if (flags.testFlag(CryptoFlagHash)) { + if (ba.length() < 20) { + m_lastError = ErrorIntegrityFailed; + return QByteArray(); + } + QByteArray storedHash = ba.left(20); + ba = ba.mid(20); + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(ba); + integrityOk = (hash.result() == storedHash); + } + + if (!integrityOk) { + m_lastError = ErrorIntegrityFailed; + return QByteArray(); + } + + if (flags.testFlag(CryptoFlagCompression)) + ba = qUncompress(ba); + + m_lastError = ErrorNoError; + return ba; +} diff --git a/simplecrypt.h b/simplecrypt.h new file mode 100644 index 0000000..e275639 --- /dev/null +++ b/simplecrypt.h @@ -0,0 +1,225 @@ +/* +Copyright (c) 2011, Andre Somers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Rathenau Instituut, Andre Somers nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR #######; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef SIMPLECRYPT_H +#define SIMPLECRYPT_H +#include +#include +#include + +/** + @short Simple encryption and decryption of strings and byte arrays + + This class provides a simple implementation of encryption and decryption + of strings and byte arrays. + + @warning The encryption provided by this class is NOT strong encryption. It may + help to shield things from curious eyes, but it will NOT stand up to someone + determined to break the encryption. Don't say you were not warned. + + The class uses a 64 bit key. Simply create an instance of the class, set the key, + and use the encryptToString() method to calculate an encrypted version of the input string. + To decrypt that string again, use an instance of SimpleCrypt initialized with + the same key, and call the decryptToString() method with the encrypted string. If the key + matches, the decrypted version of the string will be returned again. + + If you do not provide a key, or if something else is wrong, the encryption and + decryption function will return an empty string or will return a string containing nonsense. + lastError() will return a value indicating if the method was succesful, and if not, why not. + + SimpleCrypt is prepared for the case that the encryption and decryption + algorithm is changed in a later version, by prepending a version identifier to the cypertext. + */ +class SimpleCrypt +{ +public: + /** + CompressionMode describes if compression will be applied to the data to be + encrypted. + */ + enum CompressionMode { + CompressionAuto, /*!< Only apply compression if that results in a shorter plaintext. */ + CompressionAlways, /*!< Always apply compression. Note that for short inputs, a compression may result in longer data */ + CompressionNever /*!< Never apply compression. */ + }; + /** + IntegrityProtectionMode describes measures taken to make it possible to detect problems with the data + or wrong decryption keys. + + Measures involve adding a checksum or a cryptograhpic hash to the data to be encrypted. This + increases the length of the resulting cypertext, but makes it possible to check if the plaintext + appears to be valid after decryption. + */ + enum IntegrityProtectionMode { + ProtectionNone, /*!< The integerity of the encrypted data is not protected. It is not really possible to detect a wrong key, for instance. */ + ProtectionChecksum,/*!< A simple checksum is used to verify that the data is in order. If not, an empty string is returned. */ + ProtectionHash /*!< A cryptographic hash is used to verify the integrity of the data. This method produces a much stronger, but longer check */ + }; + /** + Error describes the type of error that occured. + */ + enum Error { + ErrorNoError, /*!< No error occurred. */ + ErrorNoKeySet, /*!< No key was set. You can not encrypt or decrypt without a valid key. */ + ErrorUnknownVersion, /*!< The version of this data is unknown, or the data is otherwise not valid. */ + ErrorIntegrityFailed, /*!< The integrity check of the data failed. Perhaps the wrong key was used. */ + }; + + /** + Constructor. + + Constructs a SimpleCrypt instance without a valid key set on it. + */ + SimpleCrypt(); + /** + Constructor. + + Constructs a SimpleCrypt instance and initializes it with the given @arg key. + */ + explicit SimpleCrypt(quint64 key); + + /** + (Re-) initializes the key with the given @arg key. + */ + void setKey(quint64 key); + /** + Returns true if SimpleCrypt has been initialized with a key. + */ + bool hasKey() const {return !m_keyParts.isEmpty();} + + /** + Sets the compression mode to use when encrypting data. The default mode is Auto. + + Note that decryption is not influenced by this mode, as the decryption recognizes + what mode was used when encrypting. + */ + void setCompressionMode(CompressionMode mode) {m_compressionMode = mode;} + /** + Returns the CompressionMode that is currently in use. + */ + CompressionMode compressionMode() const {return m_compressionMode;} + + /** + Sets the integrity mode to use when encrypting data. The default mode is Checksum. + + Note that decryption is not influenced by this mode, as the decryption recognizes + what mode was used when encrypting. + */ + void setIntegrityProtectionMode(IntegrityProtectionMode mode) {m_protectionMode = mode;} + /** + Returns the IntegrityProtectionMode that is currently in use. + */ + IntegrityProtectionMode integrityProtectionMode() const {return m_protectionMode;} + + /** + Returns the last error that occurred. + */ + Error lastError() const {return m_lastError;} + + /** + Encrypts the @arg plaintext string with the key the class was initialized with, and returns + a cyphertext the result. The result is a base64 encoded version of the binary array that is the + actual result of the string, so it can be stored easily in a text format. + */ + QString encryptToString(const QString& plaintext) ; + /** + Encrypts the @arg plaintext QByteArray with the key the class was initialized with, and returns + a cyphertext the result. The result is a base64 encoded version of the binary array that is the + actual result of the encryption, so it can be stored easily in a text format. + */ + QString encryptToString(QByteArray plaintext) ; + /** + Encrypts the @arg plaintext string with the key the class was initialized with, and returns + a binary cyphertext in a QByteArray the result. + + This method returns a byte array, that is useable for storing a binary format. If you need + a string you can store in a text file, use encryptToString() instead. + */ + QByteArray encryptToByteArray(const QString& plaintext) ; + /** + Encrypts the @arg plaintext QByteArray with the key the class was initialized with, and returns + a binary cyphertext in a QByteArray the result. + + This method returns a byte array, that is useable for storing a binary format. If you need + a string you can store in a text file, use encryptToString() instead. + */ + QByteArray encryptToByteArray(QByteArray plaintext) ; + + /** + Decrypts a cyphertext string encrypted with this class with the set key back to the + plain text version. + + If an error occured, such as non-matching keys between encryption and decryption, + an empty string or a string containing nonsense may be returned. + */ + QString decryptToString(const QString& cyphertext) ; + /** + Decrypts a cyphertext string encrypted with this class with the set key back to the + plain text version. + + If an error occured, such as non-matching keys between encryption and decryption, + an empty string or a string containing nonsense may be returned. + */ + QByteArray decryptToByteArray(const QString& cyphertext) ; + /** + Decrypts a cyphertext binary encrypted with this class with the set key back to the + plain text version. + + If an error occured, such as non-matching keys between encryption and decryption, + an empty string or a string containing nonsense may be returned. + */ + QString decryptToString(QByteArray cypher) ; + /** + Decrypts a cyphertext binary encrypted with this class with the set key back to the + plain text version. + + If an error occured, such as non-matching keys between encryption and decryption, + an empty string or a string containing nonsense may be returned. + */ + QByteArray decryptToByteArray(QByteArray cypher) ; + + //enum to describe options that have been used for the encryption. Currently only one, but + //that only leaves room for future extensions like adding a cryptographic hash... + enum CryptoFlag{CryptoFlagNone = 0, + CryptoFlagCompression = 0x01, + CryptoFlagChecksum = 0x02, + CryptoFlagHash = 0x04 + }; + Q_DECLARE_FLAGS(CryptoFlags, CryptoFlag); +private: + + void splitKey(); + + quint64 m_key; + QVector m_keyParts; + CompressionMode m_compressionMode; + IntegrityProtectionMode m_protectionMode; + Error m_lastError; +}; +Q_DECLARE_OPERATORS_FOR_FLAGS(SimpleCrypt::CryptoFlags) + +#endif // SimpleCrypt_H diff --git a/ui/mainsettingsdialog.ui b/ui/mainsettingsdialog.ui index cce67e8..678280e 100644 --- a/ui/mainsettingsdialog.ui +++ b/ui/mainsettingsdialog.ui @@ -7,7 +7,7 @@ 0 0 965 - 591 + 668 @@ -287,10 +287,10 @@ - false + true - Remote Connection: + MQTT Remote CAN Settings: @@ -300,6 +300,9 @@ + + + @@ -307,16 +310,30 @@ - - - - - + + - Auto start remote hosting + User Name: + + + + + + + + + + Password + + + + + + + QLineEdit::Password @@ -371,7 +388,6 @@ cbInfoAutoExpand lineRemoteHost lineRemotePort - cbAutoStartRemote diff --git a/ui/newconnectiondialog.ui b/ui/newconnectiondialog.ui index de55127..26eeb74 100644 --- a/ui/newconnectiondialog.ui +++ b/ui/newconnectiondialog.ui @@ -6,8 +6,8 @@ 0 0 - 356 - 282 + 359 + 291 @@ -33,7 +33,7 @@ - + false @@ -46,7 +46,14 @@ - Network connection + Network Connection + + + + + + + MQTT Connection From 2da2855bfa3af467712674dc663e637517057a67 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 5 Aug 2020 21:53:21 -0400 Subject: [PATCH 52/58] A bit more work on MQTT support. It can actually connect to an MQTT broker now. --- SavvyCAN.pro | 2 + connections/canconfactory.cpp | 3 + connections/mqtt_bus.cpp | 205 ++++++++++++++++++++++++++++ connections/mqtt_bus.h | 66 +++++++++ connections/newconnectiondialog.cpp | 12 +- mqtt/qmqtt.h | 4 +- 6 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 connections/mqtt_bus.cpp create mode 100644 connections/mqtt_bus.h diff --git a/SavvyCAN.pro b/SavvyCAN.pro index b946b25..d52b170 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -17,6 +17,7 @@ TARGET = SavvyCAN TEMPLATE = app SOURCES += main.cpp\ + connections/mqtt_bus.cpp \ mqtt/qmqtt_client.cpp \ mqtt/qmqtt_client_p.cpp \ mqtt/qmqtt_frame.cpp \ @@ -91,6 +92,7 @@ SOURCES += main.cpp\ HEADERS += mainwindow.h \ can_structs.h \ canframemodel.h \ \ + connections/mqtt_bus.h \ mqtt/qmqtt.h \ mqtt/qmqtt_client.h \ mqtt/qmqtt_client_p.h \ diff --git a/connections/canconfactory.cpp b/connections/canconfactory.cpp index d506524..345b0ef 100644 --- a/connections/canconfactory.cpp +++ b/connections/canconfactory.cpp @@ -2,6 +2,7 @@ #include "canconfactory.h" #include "serialbusconnection.h" #include "gvretserial.h" +#include "mqtt_bus.h" using namespace CANCon; @@ -14,6 +15,8 @@ CANConnection* CanConFactory::create(type pType, QString pPortName, QString pDri return new GVRetSerial(pPortName, false); case REMOTE: return new GVRetSerial(pPortName, true); //it's a special case of GVRET connected over TCP/IP so it uses the same class + case MQTT: + return new MQTT_BUS(pPortName); default: {} } diff --git a/connections/mqtt_bus.cpp b/connections/mqtt_bus.cpp new file mode 100644 index 0000000..fb10a96 --- /dev/null +++ b/connections/mqtt_bus.cpp @@ -0,0 +1,205 @@ +#include +#include +#include +#include +#include +#include + +#include "mqtt_bus.h" + +MQTT_BUS::MQTT_BUS(QString topicName) : + CANConnection(topicName, "mqtt_client", CANCon::MQTT, 1, 4000, true), + mTimer(this) /*NB: set this as parent of timer to manage it from working thread */ +{ + sendDebug("MQTT_BUS()"); + + crypto = new SimpleCrypt(Q_UINT64_C(0xdeadbeefface6285)); + + isAutoRestart = false; + this->topicName = topicName; + + timeBasis = 0; + lastSystemTimeBasis = 0; + + readSettings(); +} + + +MQTT_BUS::~MQTT_BUS() +{ + delete crypto; + stop(); + sendDebug("~MQTT_BUS"); +} + +void MQTT_BUS::sendDebug(const QString debugText) +{ + qDebug() << debugText; + debugOutput(debugText); +} + +void MQTT_BUS::piStarted() +{ + QSettings settings; + QString userName = settings.value("Remote/User", "Anonymous").toString(); + QString host = settings.value("Remote/Host", "api.savvycan.com").toString(); + int port = settings.value("Remote/Port", 8333).toInt(); + QByteArray encPass = settings.value("Remote/Pass", "").toByteArray(); + QByteArray password = crypto->decryptToByteArray(encPass); + + QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); + mqttClient = new QMQTT::Client(host, port, sslConfig); + + connect(mqttClient, &QMQTT::Client::connected, this, &MQTT_BUS::clientConnected); + connect(mqttClient, &QMQTT::Client::error, this, &MQTT_BUS::clientErrored); + + //qDebug() << "User: " << userName << " Pass: " << password; + mqttClient->setClientId(genRandomClientID()); + mqttClient->setUsername(userName); + mqttClient->setPassword(password); + mqttClient->connectToHost(); +} + +//MQTT required a client ID and they cannot be the same for any two clients. But, they really aren't super exciting +//or important to be named explicitly. Perhaps it might be nice to be able to see it for debugging though. +QString MQTT_BUS::genRandomClientID() +{ + QString output; + output.reserve(12); + QRandomGenerator gen; + for (int i = 0; i < 12; i++) + { + int val = gen.bounded(0, 62); + if (val < 26) output.append(QChar('A'+val)); + else if (val < 52) output.append(QChar('a'+val-26)); + else output.append(QChar('0'+val-52)); + } + qDebug() << "Client ID: " << output; + return output; +} + +void MQTT_BUS::clientErrored(const QMQTT::ClientError error) +{ + qDebug() << QString::number(error); +} + +void MQTT_BUS::piSuspend(bool pSuspend) +{ + /* update capSuspended */ + setCapSuspended(pSuspend); + + /* flush queue if we are suspended */ + if(isCapSuspended()) + getQueue().flush(); +} + + +void MQTT_BUS::piStop() +{ + mTimer.stop(); + disconnectDevice(); +} + + +bool MQTT_BUS::piGetBusSettings(int pBusIdx, CANBus& pBus) +{ + return getBusConfig(pBusIdx, pBus); +} + + +void MQTT_BUS::piSetBusSettings(int pBusIdx, CANBus bus) +{ + /* sanity checks */ + if( (pBusIdx < 0) || pBusIdx >= getNumBuses()) + return; + + /* copy bus config */ + setBusConfig(pBusIdx, bus); + //we don't really update anything. We're just here to listen and perhaps send frames. +} + + +bool MQTT_BUS::piSendFrame(const CANFrame& frame) +{ + QByteArray buffer; + int c; + quint32 ID; + + //qDebug() << "Sending out GVRET frame with id " << frame.ID << " on bus " << frame.bus; + + framesRapid++; + + // Doesn't make sense to send an error frame + // to an adapter + if (frame.frameId() & 0x20000000) { + return true; + } + ID = frame.frameId(); + if (frame.hasExtendedFrameFormat()) ID |= 1u << 31; + + buffer[0] = (char)0xF1; //start of a command over serial + buffer[1] = 0; //command ID for sending a CANBUS frame + buffer[2] = (char)(ID & 0xFF); //four bytes of ID LSB first + buffer[3] = (char)(ID >> 8); + buffer[4] = (char)(ID >> 16); + buffer[5] = (char)(ID >> 24); + buffer[6] = (char)((frame.bus) & 3); + buffer[7] = (char)frame.payload().length(); + for (c = 0; c < frame.payload().length(); c++) + { + buffer[8 + c] = frame.payload()[c]; + } + buffer[8 + frame.payload().length()] = 0; + + return true; +} + + + +/****************************************************************/ + +void MQTT_BUS::readSettings() +{ + QSettings settings; + +} + +void MQTT_BUS::clientConnected() +{ + sendDebug("Connecting to MQTT Broker!"); + + setStatus(CANCon::CONNECTED); + CANConStatus stats; + stats.conStatus = getStatus(); + stats.numHardwareBuses = 1;//mNumBuses; + emit status(stats); +} + +void MQTT_BUS::disconnectDevice() { + + setStatus(CANCon::NOT_CONNECTED); + CANConStatus stats; + stats.conStatus = getStatus(); + stats.numHardwareBuses = mNumBuses; + emit status(stats); +} + +void MQTT_BUS::rebuildLocalTimeBasis() +{ + + //qDebug() << "Rebuilding GVRET time base. GVRET local base = " << buildTimeBasis; + + /* + our time basis is the value we have to modulate the main system basis by in order + to sync the GVRET timestamps to the rest of the system. + The rest of the system uses CANConManager::getInstance()->getTimeBasis as the basis. + GVRET returns to us the current time since boot up in microseconds. + timeAtGVRETSync stores the "system" timestamp when the GVRET timestamp was retrieved. + */ + /* + lastSystemTimeBasis = CANConManager::getInstance()->getTimeBasis(); + int64_t systemDelta = timeAtGVRETSync - lastSystemTimeBasis; + int32_t localDelta = buildTimeBasis - systemDelta; + timeBasis = -localDelta; + */ +} diff --git a/connections/mqtt_bus.h b/connections/mqtt_bus.h new file mode 100644 index 0000000..40df9ff --- /dev/null +++ b/connections/mqtt_bus.h @@ -0,0 +1,66 @@ +#ifndef MQTTBUS_H +#define MQTTBUS_H + +#include +#include +#include +#include "mqtt/qmqtt.h" + +/*************/ +#include +/*************/ + +#include "canframemodel.h" +#include "canconnection.h" +#include "canconmanager.h" +#include "simplecrypt.h" + +class MQTT_BUS : public CANConnection +{ + Q_OBJECT + +public: + MQTT_BUS(QString topicName); + virtual ~MQTT_BUS(); + +protected: + + virtual void piStarted(); + virtual void piStop(); + virtual void piSetBusSettings(int pBusIdx, CANBus pBus); + virtual bool piGetBusSettings(int pBusIdx, CANBus& pBus); + virtual void piSuspend(bool pSuspend); + virtual bool piSendFrame(const CANFrame&) ; + + void disconnectDevice(); + +private slots: + void clientConnected(); + void clientErrored(const QMQTT::ClientError error); + +private: + void readSettings(); + void rebuildLocalTimeBasis(); + void sendDebug(const QString debugText); + QString genRandomClientID(); + SimpleCrypt *crypto; + +protected: + QTimer mTimer; + QThread mThread; + + QMQTT::Client *mqttClient; + QString topicName; + + bool isAutoRestart; + int framesRapid; + CANFrame buildFrame; + qint64 buildTimestamp; + quint32 buildId; + QByteArray buildData; + uint32_t buildTimeBasis; + int32_t timeBasis; + uint64_t lastSystemTimeBasis; +}; + +#endif // MQTT_BUS_H diff --git a/connections/newconnectiondialog.cpp b/connections/newconnectiondialog.cpp index d2bd9e7..25fa899 100644 --- a/connections/newconnectiondialog.cpp +++ b/connections/newconnectiondialog.cpp @@ -52,7 +52,7 @@ void NewConnectionDialog::handleConnTypeChanged() if (ui->rbGVRET->isChecked()) selectSerial(); if (ui->rbSocketCAN->isChecked()) selectSocketCan(); if (ui->rbRemote->isChecked()) selectRemote(); - + if (ui->rbMQTT->isChecked()) selectMQTT(); } void NewConnectionDialog::handleDeviceTypeChanged() @@ -123,7 +123,12 @@ void NewConnectionDialog::setPortName(CANCon::type pType, QString pPortName, QSt break; case CANCon::SERIALBUS: ui->rbSocketCAN->setChecked(true); - //you can't configure any of the below three with socketcan so dim them out + break; + case CANCon::REMOTE: + ui->rbRemote->setChecked(true); + break; + case CANCon::MQTT: + ui->rbMQTT->setChecked(true); break; default: {} } @@ -157,6 +162,9 @@ void NewConnectionDialog::setPortName(CANCon::type pType, QString pPortName, QSt else ui->cbPort->addItem(pPortName); break; } + case CANCon::MQTT: + ui->cbPort->setCurrentText(pPortName); + break; default: {} } } diff --git a/mqtt/qmqtt.h b/mqtt/qmqtt.h index 6e607dd..63f5470 100644 --- a/mqtt/qmqtt.h +++ b/mqtt/qmqtt.h @@ -32,7 +32,7 @@ #ifndef QMQTT_H #define QMQTT_H -#include -#include +#include "qmqtt_message.h" +#include "qmqtt_client.h" #endif // QMQTT_H From 45d045649c772320948c1e27ae34246428735ce3 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Thu, 6 Aug 2020 21:39:07 -0400 Subject: [PATCH 53/58] Got MQTT reception working. You'd need a python script that isn't public yet in order to do this. Sorry... --- connections/mqtt_bus.cpp | 39 +++++++++++++++++++++++++++++++++++++++ connections/mqtt_bus.h | 1 + 2 files changed, 40 insertions(+) diff --git a/connections/mqtt_bus.cpp b/connections/mqtt_bus.cpp index fb10a96..d566d1d 100644 --- a/connections/mqtt_bus.cpp +++ b/connections/mqtt_bus.cpp @@ -164,10 +164,49 @@ void MQTT_BUS::readSettings() } +void MQTT_BUS::clientMessageReceived(const QMQTT::Message& message) +{ + uint64_t timeBasis = CANConManager::getInstance()->getTimeBasis(); + + + /* drop frame if capture is suspended */ + if(isCapSuspended()) + return; + + CANFrame* frame_p = getQueue().get(); + if(frame_p) + { + uint32_t frameID = message.topic().split("/")[1].toInt(); + uint64_t timeStamp = message.payload()[0] + (message.payload()[1] << 8) + (message.payload()[2] << 16) + (message.payload()[3] << 24) + + ((uint64_t)message.payload()[4] << 32ull) + ((uint64_t)message.payload()[5] << 40ull) + + ((uint64_t)message.payload()[6] << 48ull) + ((uint64_t)message.payload()[7] << 56ull); + int flags = message.payload()[8]; + frame_p->setPayload(message.payload().right(message.payload().count() - 9)); + frame_p->bus = 0; + frame_p->setExtendedFrameFormat(flags & 1); + frame_p->setFrameId(frameID); + frame_p->setFrameType(QCanBusFrame::DataFrame); + frame_p->isReceived = true; + if (useSystemTime) + { + frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, QDateTime::currentMSecsSinceEpoch() * 1000ul)); + } + else frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp)); + + checkTargettedFrame(*frame_p); + + /* enqueue frame */ + getQueue().queue(); + } +} + void MQTT_BUS::clientConnected() { sendDebug("Connecting to MQTT Broker!"); + mqttClient->subscribe(topicName + "/+", 0); //subscribe to all sub topics to grab the frames. + connect(mqttClient, &QMQTT::Client::received, this, &MQTT_BUS::clientMessageReceived); + setStatus(CANCon::CONNECTED); CANConStatus stats; stats.conStatus = getStatus(); diff --git a/connections/mqtt_bus.h b/connections/mqtt_bus.h index 40df9ff..8e9c156 100644 --- a/connections/mqtt_bus.h +++ b/connections/mqtt_bus.h @@ -37,6 +37,7 @@ protected: private slots: void clientConnected(); void clientErrored(const QMQTT::ClientError error); + void clientMessageReceived(const QMQTT::Message& message); private: void readSettings(); From b667351cbbe7482acfd0628596baf3263abda887 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Aug 2020 18:30:13 -0400 Subject: [PATCH 54/58] Remove OSX CI build for now. It's failing and I don't know why yet. --- .travis.yml | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index f165ff1..7f6721a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,27 +43,27 @@ matrix: tags: true #deploy is only done on tagged builds draft: false - - os: osx - osx_image: xcode10 - before_install: - - brew install qt5 - - brew link qt5 --force - script: - - qmake CONFIG+=release -spec macx-xcode SavvyCAN.pro - - xcodebuild - - mkdir Release/SavvyCAN.app/Contents/MacOS/help - - cp -R help/*.* Release/SavvyCAN.app/Contents/MacOS/help - - cd Release - - ls SavvyCAN.app/Contents/MacOS - - macdeployqt SavvyCAN.app -dmg - deploy: - provider: releases - skip_cleanup: true - api_key: $GITHUB_TOKEN - file: SavvyCAN.dmg - on: - tags: true - draft: false +# - os: osx +# osx_image: xcode10 +# before_install: +# - brew install qt5 +# - brew link qt5 --force +# script: +# - qmake CONFIG+=release -spec macx-xcode SavvyCAN.pro +# - xcodebuild +# - mkdir Release/SavvyCAN.app/Contents/MacOS/help +# - cp -R help/*.* Release/SavvyCAN.app/Contents/MacOS/help +# - cd Release +# - ls SavvyCAN.app/Contents/MacOS +# - macdeployqt SavvyCAN.app -dmg +# deploy: +# provider: releases +# skip_cleanup: true +# api_key: $GITHUB_TOKEN +# file: SavvyCAN.dmg +# on: +# tags: true +# draft: false branches: except: From 4887ac8e628b8570100dee972ca0f64f551a082a Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Aug 2020 18:30:43 -0400 Subject: [PATCH 55/58] Many fixes to file loading to prevent crashes and make failure more clear. --- framefileio.cpp | 88 +++++++++++++++++++++++++++++-------------------- mainwindow.cpp | 9 +++-- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/framefileio.cpp b/framefileio.cpp index 828a56c..a841b84 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -28,7 +28,7 @@ bool FrameFileIO::saveFrameFile(QString &fileName, const QVector* fram filters.append(QString(tr("CRTD Logs (*.crt *.crtd *.CRT *.CRTD)"))); filters.append(QString(tr("Generic ID/Data CSV (*.csv *.CSV)"))); filters.append(QString(tr("BusMaster Log (*.log *.LOG)"))); - filters.append(QString(tr("Microchip Log (*.can *.CAN)"))); + filters.append(QString(tr("Microchip Log (*.can *.CAN *.log *.LOG)"))); filters.append(QString(tr("Vector Trace Files (*.trace *.TRACE)"))); filters.append(QString(tr("IXXAT MiniLog (*.csv *.CSV)"))); filters.append(QString(tr("CAN-DO Log (*.can *.avc *.evc *.qcc *.CAN *.AVC *.EVC *.QCC)"))); @@ -149,7 +149,7 @@ bool FrameFileIO::loadFrameFile(QString &fileName, QVector* frameCache filters.append(QString(tr("CRTD Logs (*.crt *.crtd *.CRT *.CRTD)"))); filters.append(QString(tr("Generic ID/Data CSV (*.csv *.CSV)"))); filters.append(QString(tr("BusMaster Log (*.log *.LOG)"))); - filters.append(QString(tr("Microchip Log (*.can *.CAN)"))); + filters.append(QString(tr("Microchip Log (*.can *.CAN *.log *.LOG)"))); filters.append(QString(tr("Vector trace files (*.trace *.TRACE)"))); filters.append(QString(tr("IXXAT MiniLog (*.csv *.CSV)"))); filters.append(QString(tr("CAN-DO Log (*.avc *.can *.evc *.qcc *.AVC *.CAN *.EVC *.QCC)"))); @@ -216,9 +216,12 @@ bool FrameFileIO::loadFrameFile(QString &fileName, QVector* frameCache } else { - QMessageBox msgBox; - msgBox.setText("File load completed with errors.\r\nPerhaps you selected the wrong file type?"); - msgBox.exec(); + if (dialog.selectedNameFilter() != filters[0]) + { + QMessageBox msgBox; + msgBox.setText("File load completed with errors.\r\nPerhaps you selected the wrong file type?"); + msgBox.exec(); + } return false; } } @@ -226,7 +229,7 @@ bool FrameFileIO::loadFrameFile(QString &fileName, QVector* frameCache } -//Try every format by first uses the "is" functions which try to detect whether a given file is a good match to that +//Try every format by first using the "is" functions which try to detect whether a given file is a good match to that //file format or not. Those functions are much less tolerant than the load functions and so should help to discriminate //whether a file could be loaded or not by a given loader. The loader return is still used in case the guess was wrong. bool FrameFileIO::autoDetectLoadFile(QString filename, QVector* frames) @@ -417,6 +420,9 @@ bool FrameFileIO::autoDetectLoadFile(QString filename, QVector* frames } } + QMessageBox msgBox; + msgBox.setText("Could not autodetect the file type.\rPlease try to manually select the file format."); + msgBox.exec(); qDebug() << "Nothing worked... sorry..."; return false; } @@ -804,8 +810,9 @@ bool FrameFileIO::loadCANHackerFile(QString filename, QVector* frames) thisFrame.isReceived = true; thisFrame.setFrameType(QCanBusFrame::DataFrame); thisFrame.bus = 0; - QByteArray bytes( tokens[2].toInt(nullptr, 16), 0); - for (int d = 0; d < thisFrame.payload().length(); d++) + int numBytes = tokens[2].toInt(nullptr, 16); + QByteArray bytes( numBytes, 0); + for (int d = 0; d < numBytes; d++) { if (tokens[d + 3] != "") { @@ -923,7 +930,7 @@ bool FrameFileIO::loadCANOpenFile(QString filename, QVector* frames) thisFrame.bus = 0; QList dataTok = tokens[11].simplified().split(' '); QByteArray bytes(dataTok.length(), 0); - for (int d = 0; d < thisFrame.payload().length(); d++) + for (int d = 0; d < dataTok.length(); d++) { if (dataTok[d] != "") { @@ -1126,7 +1133,8 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) thisFrame.setFrameId(line.mid(28, 8).simplified().toUInt(nullptr, 16)); if (thisFrame.frameId() < 0x1FFFFFFF) { - QByteArray bytes(line.mid(38,1).toInt(), 0); + int numBytes = line.mid(38,1).toInt(); + QByteArray bytes(numBytes, 0); thisFrame.isReceived = true; thisFrame.bus = 0; if (line.at(28) == ' ') { @@ -1140,7 +1148,7 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) } else { QList tokens = line.mid(41, thisFrame.payload().length() * 3).split(' '); thisFrame.setFrameType(QCanBusFrame::DataFrame); - for (int d = 0; d < thisFrame.payload().length(); d++) + for (int d = 0; d < numBytes; d++) { if (tokens[d] != "") { @@ -1159,7 +1167,8 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) thisFrame.setFrameId(line.mid(25, 8).simplified().toUInt(nullptr, 16)); if (thisFrame.frameId() < 0x1FFFFFFF) { - QByteArray bytes(line.mid(37,2).trimmed().toInt(), 0); + int numBytes = line.mid(37,2).trimmed().toInt(); + QByteArray bytes(numBytes, 0); qDebug() << thisFrame.payload().length(); thisFrame.isReceived = true; thisFrame.bus = 0; @@ -1173,7 +1182,7 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) } else { QList tokens = line.mid(40, thisFrame.payload().length() * 3).split(' '); thisFrame.setFrameType(QCanBusFrame::DataFrame); - for (int d = 0; d < thisFrame.payload().length(); d++) + for (int d = 0; d < numBytes; d++) { if (tokens[d] != "") { @@ -1592,7 +1601,7 @@ bool FrameFileIO::loadNativeCSVFile(QString filename, QVector* frames) if (lng + 5 > tokens.length()) lng = tokens.length() - 5; QByteArray bytes(lng, 0); for (int c = 0; c < 8; c++) bytes[c] = 0; - for (int d = 0; d < thisFrame.payload().length(); d++) + for (int d = 0; d < lng; d++) bytes[d] = static_cast(tokens[5 + d].toInt(nullptr, 16)); thisFrame.setPayload(bytes); } @@ -1607,7 +1616,7 @@ bool FrameFileIO::loadNativeCSVFile(QString filename, QVector* frames) if (lng + 6 > tokens.length()) lng = tokens.length() - 6; QByteArray bytes(lng, 0); for (int c = 0; c < 8; c++) bytes[c] = 0; - for (int d = 0; d < thisFrame.payload().length(); d++) + for (int d = 0; d < lng; d++) bytes[d] = static_cast(tokens[6 + d].toInt(nullptr, 16)); thisFrame.setPayload(bytes); } @@ -1876,7 +1885,7 @@ bool FrameFileIO::loadGenericCSVFile(QString filename, QVector* frames thisFrame.setFrameType(QCanBusFrame::DataFrame); QList dataTok = tokens[1].split(' '); QByteArray bytes(dataTok.length(), 0); - for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast(dataTok[d].toInt(nullptr, 16)); + for (int d = 0; d < dataTok.length(); d++) bytes[d] = static_cast(dataTok[d].toInt(nullptr, 16)); thisFrame.setPayload(bytes); frames->append(thisFrame); } @@ -2072,14 +2081,14 @@ bool FrameFileIO::loadLogFile(QString filename, QVector* frames) thisFrame.setExtendedFrameFormat(true); thisFrame.setFrameType(QCanBusFrame::RemoteRequestFrame); } - thisFrame.bus = tokens[2].toInt() - 1; + thisFrame.bus = tokens[2].toInt(); int lng = tokens[5].toInt(); if (lng > 8) lng = 8; if (lng < 0) lng = 0; QByteArray bytes(lng, 0); if (thisFrame.frameType() != QCanBusFrame::RemoteRequestFrame) { - for (int d = 0; d < thisFrame.payload().length(); d++) + for (int d = 0; d < lng; d++) bytes[d] = static_cast(tokens[d + 6].toInt(nullptr, 16)); } thisFrame.setPayload(bytes); @@ -2275,9 +2284,10 @@ bool FrameFileIO::loadIXXATFile(QString filename, QVector* frames) thisFrame.setFrameType(QCanBusFrame::DataFrame); QStringList dataToks = Utility::unQuote(tokens[4]).simplified().split(' '); - QByteArray bytes(dataToks.length(), 0); - if (thisFrame.payload().length() > 8) return false; - for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast(dataToks[d].toInt(nullptr, 16)); + int numBytes = dataToks.length(); + QByteArray bytes(numBytes, 0); + if (numBytes > 8) return false; + for (int d = 0; d < numBytes; d++) bytes[d] = static_cast(dataToks[d].toInt(nullptr, 16)); thisFrame.setPayload(bytes); frames->append(thisFrame); } @@ -2448,11 +2458,12 @@ bool FrameFileIO::loadCANDOFile(QString filename, QVector* frames) lastTimeStamp = tempStamp; thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, tempStamp)); thisFrame.setFrameId(((uData[3] & 0x0F) * 256 + uData[2]) & 0x7FF); - QByteArray bytes(uData[3] >> 4, 0); + int numBytes = uData[3] >> 4; + QByteArray bytes(numBytes, 0); - if (thisFrame.payload().length() <= 8 && thisFrame.frameId() <= 0x7FF) + if (numBytes <= 8 && thisFrame.frameId() <= 0x7FF) { - for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = data[4 + d]; + for (int d = 0; d < numBytes; d++) bytes[d] = data[4 + d]; thisFrame.setPayload(bytes); frames->append(thisFrame); } @@ -2639,10 +2650,11 @@ bool FrameFileIO::loadMicrochipFile(QString filename, QVector* frames) if (thisFrame.frameId() <= 0x7FF) thisFrame.setExtendedFrameFormat(false); else thisFrame.setExtendedFrameFormat(true); thisFrame.bus = 0; - QByteArray bytes(tokens[3].toInt(), 0); + int numBytes = tokens[3].toInt(); + QByteArray bytes(numBytes, 0); if (thisFrame.payload().length() > 8) thisFrame.payload().resize(8); if (thisFrame.payload().length() + 4 > tokens.length()) thisFrame.payload().resize( tokens.length() - 4 ); - for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast( Utility::ParseStringToNum(tokens[4 + d]) ); + for (int d = 0; d < numBytes; d++) bytes[d] = static_cast( Utility::ParseStringToNum(tokens[4 + d]) ); thisFrame.setPayload(bytes); frames->append(thisFrame); } @@ -2863,11 +2875,12 @@ bool FrameFileIO::loadTraceFile(QString filename, QVector* frames) else thisFrame.setExtendedFrameFormat(true); thisFrame.bus = 0; thisFrame.setFrameType(QCanBusFrame::DataFrame); - QByteArray bytes(tokens[3].toInt(), 0); - if (thisFrame.payload().length() > 8) thisFrame.payload().resize(8); + int numBytes = tokens[3].toInt(); + if (numBytes > 8) numBytes = 8; + QByteArray bytes(numBytes, 0); QList dataToks = tokens[4].split(' '); - if (thisFrame.payload().length() > dataToks.length()) thisFrame.payload().resize(dataToks.length()); - for (int d = 0; d < thisFrame.payload().length(); d++) bytes[d] = static_cast(dataToks[d].toInt(nullptr, 16)); + //if (numBytes > dataToks.length()) thisFrame.payload().resize(dataToks.length()); + for (int d = 0; d < numBytes; d++) bytes[d] = static_cast(dataToks[d].toInt(nullptr, 16)); thisFrame.setPayload(bytes); frames->append(thisFrame); } @@ -3211,8 +3224,9 @@ bool FrameFileIO::loadCanDumpFile(QString filename, QVector* frames) if (thisFrame.frameId() > 0x7FF) thisFrame.setExtendedFrameFormat(true); else thisFrame.setExtendedFrameFormat(false); thisFrame.setFrameType(QCanBusFrame::DataFrame); - QByteArray bytes(tokens[3].at(1) - '0', 0); - for (int c = 0; c < thisFrame.payload().length(); c++) + int numBytes = tokens[3].at(1) - '0'; + QByteArray bytes(numBytes, 0); + for (int c = 0; c < numBytes; c++) { if ((4 + c) < tokens.size()) bytes[c] = static_cast(tokens[4 + c].toInt(nullptr, 16)); } @@ -3346,8 +3360,9 @@ bool FrameFileIO::loadLawicelFile(QString filename, QVector* frames) thisFrame.setFrameType(QCanBusFrame::DataFrame); thisFrame.bus = 0; line.remove(0, 3); - QByteArray bytes(line.length() / 2, 0); - for (int d = 0; d < bytes.length(); d++) + int numBytes = line.length() / 2; + QByteArray bytes(numBytes, 0); + for (int d = 0; d < numBytes; d++) { bytes[d] = static_cast(line.mid(d * 2, 2).toInt(nullptr, 16)); } @@ -3448,8 +3463,9 @@ bool FrameFileIO::loadKvaserFile(QString filename, QVector *frames, bo if (thisFrame.frameId() > 0x7FF) thisFrame.setExtendedFrameFormat(true); else thisFrame.setExtendedFrameFormat(false); thisFrame.setFrameType(QCanBusFrame::DataFrame); - QByteArray bytes(line.mid(21, 3).simplified().toInt(), 0); - for (int i = 0; i < 8; i++) { + int numBytes = line.mid(21, 3).simplified().toInt(); + QByteArray bytes(numBytes, 0); + for (int i = 0; i < numBytes; i++) { bytes[i] = line.mid(25 + i * 4, 3).simplified().toInt(nullptr, base); } thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, line.mid(57, 14).simplified().toDouble() * 1000000)); diff --git a/mainwindow.cpp b/mainwindow.cpp index 4b32a54..d492eb4 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -657,10 +657,13 @@ void MainWindow::handleLoadFile() if (!loadResult) { - confirmDialog = QMessageBox::question(this, "Error Loading", "Do you want to salvage what could be loaded?", + if (tempFrames.count() > 0) //only ask if at least one frame was decoded. + { + confirmDialog = QMessageBox::question(this, "Error Loading", "Do you want to salvage what could be loaded?", QMessageBox::Yes|QMessageBox::No); - if (confirmDialog == QMessageBox::Yes) { - loadResult = true; + if (confirmDialog == QMessageBox::Yes) { + loadResult = true; + } } } From df1ef510da5c08bfc73bd349da0efa82e54e2e93 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Sun, 16 Aug 2020 18:57:02 -0400 Subject: [PATCH 56/58] Windows build dies if QMQTT isn't set to statically link. This should fix that. --- mqtt/qmqtt_global.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mqtt/qmqtt_global.h b/mqtt/qmqtt_global.h index d633168..07e8447 100644 --- a/mqtt/qmqtt_global.h +++ b/mqtt/qmqtt_global.h @@ -34,15 +34,15 @@ #include -#ifndef QT_STATIC -# if defined(QT_BUILD_QMQTT_LIB) -# define Q_MQTT_EXPORT Q_DECL_EXPORT -# else -# define Q_MQTT_EXPORT Q_DECL_IMPORT -# endif -#else -# define Q_MQTT_EXPORT -#endif +//#ifndef QT_STATIC +//# if defined(QT_BUILD_QMQTT_LIB) +//# define Q_MQTT_EXPORT Q_DECL_EXPORT +//# else +//# define Q_MQTT_EXPORT Q_DECL_IMPORT +//# endif +//#else +#define Q_MQTT_EXPORT +//#endif #endif // QMQTT_GLOBAL_H From 3fa9f14651b2e9ad343d3ce3701aa3a007dabe48 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 18 Aug 2020 22:43:41 -0400 Subject: [PATCH 57/58] Changes to MQTT interface to make it work properly with current python script --- connections/mqtt_bus.cpp | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/connections/mqtt_bus.cpp b/connections/mqtt_bus.cpp index d566d1d..41f8df3 100644 --- a/connections/mqtt_bus.cpp +++ b/connections/mqtt_bus.cpp @@ -5,6 +5,7 @@ #include #include +#include "utility.h" #include "mqtt_bus.h" MQTT_BUS::MQTT_BUS(QString topicName) : @@ -134,22 +135,28 @@ bool MQTT_BUS::piSendFrame(const CANFrame& frame) if (frame.frameId() & 0x20000000) { return true; } - ID = frame.frameId(); - if (frame.hasExtendedFrameFormat()) ID |= 1u << 31; - buffer[0] = (char)0xF1; //start of a command over serial - buffer[1] = 0; //command ID for sending a CANBUS frame - buffer[2] = (char)(ID & 0xFF); //four bytes of ID LSB first - buffer[3] = (char)(ID >> 8); - buffer[4] = (char)(ID >> 16); - buffer[5] = (char)(ID >> 24); - buffer[6] = (char)((frame.bus) & 3); - buffer[7] = (char)frame.payload().length(); - for (c = 0; c < frame.payload().length(); c++) + QMQTT::Message msg; + QByteArray bytes; + + msg.setTopic(topicName + "/s/" + QString::number(frame.frameId())); + uint8_t flags = 0; + if (frame.hasExtendedFrameFormat()) flags += 1; + if (frame.frameType() == QCanBusFrame::RemoteRequestFrame) flags += 2; + if (frame.hasFlexibleDataRateFormat()) flags += 4; + if (frame.frameType() == QCanBusFrame::ErrorFrame) flags += 8; + + uint64_t micros = QDateTime::currentMSecsSinceEpoch() * 1000ull; + for (int x = 0; x < 8; x++) { - buffer[8 + c] = frame.payload()[c]; + bytes.append(micros & 0xFF); + micros = micros / 256; } - buffer[8 + frame.payload().length()] = 0; + bytes.append(flags); + bytes.append(frame.payload()); + + msg.setPayload(bytes); + mqttClient->publish(msg); return true; } From 18e20a2ee209a0ab384a0b614d5273dfdf5931de Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 21 Sep 2020 22:04:28 -0400 Subject: [PATCH 58/58] Fix UDS interpretation of read by ID, Fix up display issues with interpreted messages on main screen. --- bus_protocols/uds_handler.cpp | 10 +++++----- canframemodel.cpp | 4 ++-- filterutility.cpp | 5 ++++- mainwindow.cpp | 12 +++++++++--- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/bus_protocols/uds_handler.cpp b/bus_protocols/uds_handler.cpp index b2dd193..697f758 100644 --- a/bus_protocols/uds_handler.cpp +++ b/bus_protocols/uds_handler.cpp @@ -512,13 +512,13 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) if (dataLen > 3) { int routineID; - routineID = (data[1] * 256 + data[2]); + routineID = (data[2] * 256 + data[3]); buildString.append("\nRoutine ID: " + Utility::formatHexNum(routineID)); } if (dataLen > 4) { buildString.append("\nParameter bytes to routine: "); - for (int i = 3; i < dataLen; i++) + for (int i = 4; i < dataLen; i++) { buildString.append(Utility::formatHexNum(data[i]) + " "); } @@ -529,13 +529,13 @@ QString UDS_HANDLER::getDetailedMessageAnalysis(const UDS_MESSAGE &msg) if (dataLen > 2) { int routineID; - routineID = (data[1] * 256 + data[2]); + routineID = (data[2] * 256 + data[3]); buildString.append("\nRoutine ID: " + Utility::formatHexNum(routineID)); } - if (dataLen > 3) + if (dataLen > 4) { buildString.append("\nBytes returned by routine: "); - for (int i = 3; i < dataLen; i++) + for (int i = 4; i < dataLen; i++) { buildString.append(Utility::formatHexNum(data[i]) + " "); } diff --git a/canframemodel.cpp b/canframemodel.cpp index 486a1da..1340918 100644 --- a/canframemodel.cpp +++ b/canframemodel.cpp @@ -463,8 +463,8 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const DBC_MESSAGE *msg = dbcHandler->findMessage(thisFrame); if (msg != nullptr) { - tempString.append(" \n"); - tempString.append(msg->name + "\n" + msg->comment + "\n"); + tempString.append(" <" + msg->name + ">\n"); + if (msg->comment.length() > 1) tempString.append(msg->comment + "\n"); for (int j = 0; j < msg->sigHandler->getCount(); j++) { QString sigString; diff --git a/filterutility.cpp b/filterutility.cpp index 657deec..72cc3ed 100644 --- a/filterutility.cpp +++ b/filterutility.cpp @@ -55,8 +55,11 @@ QListWidgetItem * FilterUtility::createFilterItem(int32_t id, QListWidget* paren QListWidgetItem *thisItem = new QListWidgetItem(parent); QString filterItemName = Utility::formatCANID(id); + //Note, there are multiple filter labeling preferences. There is one in main settings to globally + //enable or disable them all. Then each loaded DBC file also can be selected on/off + //Both must be enabled for you to see labeling. if (settings.value("Main/FilterLabeling", false).toBool()) - { + { // Filter labeling (show interpreted frame names next to the CAN addr ID) MatchingCriteria_t matchingCriteria; DBC_MESSAGE *msg = dbcHandler->findMessageForFilter(id,&matchingCriteria); diff --git a/mainwindow.cpp b/mainwindow.cpp index d492eb4..a80a9f3 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -51,9 +51,15 @@ MainWindow::MainWindow(QWidget *parent) : readSettings(); - //QHeaderView *verticalHeader = ui->canFramesView->verticalHeader(); - //verticalHeader->setSectionResizeMode(QHeaderView::Fixed); - //verticalHeader->setDefaultSectionSize(10); + QHeaderView *verticalHeader = ui->canFramesView->verticalHeader(); + verticalHeader->setSectionResizeMode(QHeaderView::Fixed); + QSettings settings; + int fontSize = settings.value("Main/FontSize", 9).toUInt(); + QFont sysFont = QFont(); //get default font + sysFont.setPointSize(fontSize); + verticalHeader->setDefaultSectionSize(sysFont.pixelSize()); + ui->canFramesView->setFont(sysFont); + QHeaderView *HorzHdr = ui->canFramesView->horizontalHeader(); HorzHdr->setStretchLastSection(true); //causes the data column to automatically fill the tableview connect(HorzHdr, SIGNAL(sectionClicked(int)), this, SLOT(headerClicked(int)));