/* wireless_timeline.cpp * GUI to show an 802.11 wireless timeline of packets * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * Copyright 2012 Parc Inc and Samsung Electronics * Copyright 2015, 2016 & 2017 Cisco Inc * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "wireless_timeline.h" #include #include #include #include #include #include #include #include "globals.h" #include "../../log.h" #include #include #include "frame_tvbuff.h" #include #include #include "wireshark_application.h" #include "wsutil/utf8_entities.h" #ifdef Q_OS_WIN #include "wsutil/file_util.h" #include #endif #include #include #include #include #include "packet_list.h" #include #include "ui/main_statusbar.h" /* we start rendering this number of microseconds left of the left edge - to ensure * NAV lines are drawn correctly, and that small errors in time order don't prevent some * frames from being rendered. * These errors in time order can come from generators that record PHY rate incorrectly * in some circumstances. */ #define RENDER_EARLY 40000 const float fraction = 0.8F; const float base = 0.1F; class pcolor : public QColor { public: inline pcolor(float red, float green, float blue) : QColor( (int) (255*(red * fraction + base)), (int) (255*(green * fraction + base)), (int) (255*(blue * fraction + base)) ) { } }; static void reset_rgb(float rgb[TIMELINE_HEIGHT][3]) { int i; for (i = 0; i < TIMELINE_HEIGHT; i++) rgb[i][0] = rgb[i][1] = rgb[i][2] = 1.0; } static void render_pixels(QPainter &p, gint x, gint width, float rgb[TIMELINE_HEIGHT][3], float ratio) { int previous = 0, i; for (i = 1; i <= TIMELINE_HEIGHT; i++) { if (i != TIMELINE_HEIGHT && rgb[previous][0] == rgb[i][0] && rgb[previous][1] == rgb[i][1] && rgb[previous][2] == rgb[i][2]) continue; if (rgb[previous][0] != 1.0 || rgb[previous][1] != 1.0 || rgb[previous][2] != 1.0) { p.fillRect(QRectF(x/ratio, previous, width/ratio, i-previous), pcolor(rgb[previous][0],rgb[previous][1],rgb[previous][2])); } previous = i; } reset_rgb(rgb); } static void render_rectangle(QPainter &p, gint x, gint width, guint height, int dfilter, float r, float g, float b, float ratio) { p.fillRect(QRectF(x/ratio, TIMELINE_HEIGHT/2-height, width/ratio, dfilter ? height * 2 : height), pcolor(r,g,b)); } static void accumulate_rgb(float rgb[TIMELINE_HEIGHT][3], int height, int dfilter, float width, float red, float green, float blue) { int i; for (i = TIMELINE_HEIGHT/2-height; i < (TIMELINE_HEIGHT/2 + (dfilter ? height : 0)); i++) { rgb[i][0] = rgb[i][0] - width + width * red; rgb[i][1] = rgb[i][1] - width + width * green; rgb[i][2] = rgb[i][2] - width + width * blue; } } void WirelessTimeline::mousePressEvent(QMouseEvent *event) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) start_x = last_x = event->localPos().x(); #else start_x = last_x = event->posF().x(); #endif } void WirelessTimeline::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() == Qt::NoButton) return; #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) qreal offset = event->localPos().x() - last_x; last_x = event->localPos().x(); #else qreal offset = event->posF().x() - last_x; last_x = event->posF().x(); #endif qreal shift = ((qreal) (end_tsf - start_tsf))/width() * offset; start_tsf -= shift; end_tsf -= shift; clip_tsf(); // TODO: scroll by moving pixels and redraw only exposed area // render(p, ...) // then update full widget only on release. update(); } void WirelessTimeline::mouseReleaseEvent(QMouseEvent *event) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) QPointF localPos = event->localPos(); #else QPointF localPos = event->posF(); #endif qreal offset = localPos.x() - start_x; /* if this was a drag, ignore it */ if (std::abs(offset) > 3) return; /* this was a click */ guint num = find_packet(localPos.x()); if (num == 0) return; frame_data *fdata = frame_data_sequence_find(cfile.frames, num); if (!fdata->flags.passed_dfilter && fdata->prev_dis_num > 0) num = fdata->prev_dis_num; cf_goto_frame(&cfile, num); } void WirelessTimeline::clip_tsf() { // did we go past the start of the file? if (((gint64) start_tsf) < ((gint64) first->start_tsf)) { // align the start of the file at the left edge guint64 shift = first->start_tsf - start_tsf; start_tsf += shift; end_tsf += shift; } if (end_tsf > last->end_tsf) { guint64 shift = end_tsf - last->end_tsf; start_tsf -= shift; end_tsf -= shift; } } void WirelessTimeline::selectedFrameChanged(int frameNum) { Q_UNUSED(frameNum); if (isHidden()) return; if (cfile.current_frame) { struct wlan_radio *wr = get_wlan_radio(cfile.current_frame->num); guint left_margin = 0.9 * start_tsf + 0.1 * end_tsf; guint right_margin = 0.1 * start_tsf + 0.9 * end_tsf; guint64 half_window = (end_tsf - start_tsf)/2; if (wr) { // are we to the left of the left margin? if (wr->start_tsf < left_margin) { // scroll the left edge back to the left margin guint64 offset = left_margin - wr->start_tsf; if (offset < half_window) { // small movement; keep packet to margin start_tsf -= offset; end_tsf -= offset; } else { // large movement; move packet to center of window guint64 center = (wr->start_tsf + wr->end_tsf)/2; start_tsf = center - half_window; end_tsf = center + half_window; } } else if (wr->end_tsf > right_margin) { guint64 offset = wr->end_tsf - right_margin; if (offset < half_window) { start_tsf += offset; end_tsf += offset; } else { guint64 center = (wr->start_tsf + wr->end_tsf)/2; start_tsf = center - half_window; end_tsf = center + half_window; } } clip_tsf(); update(); } } } /* given an x position find which packet that corresponds to. * if it's inter frame space the subsequent packet is returned */ guint WirelessTimeline::find_packet(qreal x_position) { guint64 x_time = start_tsf + (x_position/width() * (end_tsf - start_tsf)); return find_packet_tsf(x_time); } void WirelessTimeline::captureFileReadStarted(capture_file *cf) { capfile = cf; hide(); // TODO: hide or grey the toolbar controls } void WirelessTimeline::captureFileReadFinished() { /* All frames must be included in packet list */ if (cfile.count == 0 || g_hash_table_size(radio_packet_list) != cfile.count) return; /* check that all frames have start and end tsf time and are reasonable time order. * packet timing reference seems to be off a little on some generators, which * causes frequent IFS values in the range 0 to -30. Some generators emit excessive * data when an FCS error happens, and this results in the duration calculation for * the error frame being excessively long. This can cause larger negative IFS values * (-30 to -1000) for the subsequent frame. Ignore these cases, as they don't seem * to impact the GUI too badly. If the TSF reference point is set wrong (TSF at * start of frame when it is at the end) then larger negative offsets are often * seen. Don't display the timeline in these cases. */ /* TODO: update GUI to handle captures with occasional frames missing TSF data */ /* TODO: indicate error message to the user */ for (guint32 n = 1; n < cfile.count; n++) { struct wlan_radio *w = get_wlan_radio(n); if (w->start_tsf == 0 || w->end_tsf == 0) { statusbar_push_temporary_msg("Packet number %u does not include TSF timestamp, not showing timeline.", n); return; } if (w->ifs < -RENDER_EARLY) { statusbar_push_temporary_msg("Packet number %u has large negative jump in TSF, not showing timeline. Perhaps TSF reference point is set wrong?", n); return; } } first = get_wlan_radio(1); last = get_wlan_radio(cfile.count); start_tsf = first->start_tsf; end_tsf = last->end_tsf; /* TODO: only reset the zoom level if the file is changed, not on redissection */ zoom_level = 0; show(); selectedFrameChanged(0); // TODO: show or ungrey the toolbar controls update(); } void WirelessTimeline::appInitialized() { register_tap_listener("wlan_radio_timeline", this, NULL, TL_REQUIRES_NOTHING, tap_timeline_reset, tap_timeline_packet, NULL/*tap_draw_cb tap_draw*/); } void WirelessTimeline::resizeEvent(QResizeEvent*) { // TODO adjust scrollbar } // Calculate the x position on the GUI from the timestamp int WirelessTimeline::position(guint64 tsf, float ratio) { int position = -100; if (tsf != G_MAXUINT64) { position = ((double) tsf - start_tsf)*width()*ratio/(end_tsf-start_tsf); } return position; } WirelessTimeline::WirelessTimeline(QWidget *parent) : QWidget(parent) { setHidden(true); zoom_level = 1.0; setFixedHeight(TIMELINE_HEIGHT); first_packet = 1; setMouseTracking(true); radio_packet_list = NULL; connect(wsApp, SIGNAL(appInitialized()), this, SLOT(appInitialized())); } void WirelessTimeline::setPacketList(PacketList *packet_list) { this->packet_list = packet_list; } void WirelessTimeline::tap_timeline_reset(void* tapdata) { WirelessTimeline* timeline = (WirelessTimeline*)tapdata; if (timeline->radio_packet_list != NULL) { g_hash_table_destroy(timeline->radio_packet_list); } timeline->hide(); timeline->radio_packet_list = g_hash_table_new(g_direct_hash, g_direct_equal); } gboolean WirelessTimeline::tap_timeline_packet(void *tapdata, packet_info* pinfo, epan_dissect_t* edt _U_, const void *data) { WirelessTimeline* timeline = (WirelessTimeline*)tapdata; struct wlan_radio *wlan_radio_info = (struct wlan_radio *)data; /* Save the radio information in our own (GUI) hashtable */ g_hash_table_insert(timeline->radio_packet_list, GUINT_TO_POINTER(pinfo->num), wlan_radio_info); return FALSE; } struct wlan_radio* WirelessTimeline::get_wlan_radio(guint32 packet_num) { return (struct wlan_radio*)g_hash_table_lookup(radio_packet_list, GUINT_TO_POINTER(packet_num)); } void WirelessTimeline::doToolTip(struct wlan_radio *wr, QPoint pos, int x) { if (x < position(wr->start_tsf, 1.0)) { QToolTip::showText(pos, QString("Inter frame space %1 " UTF8_MICRO_SIGN "s").arg(wr->ifs)); } else { QToolTip::showText(pos, QString("Total duration %1 " UTF8_MICRO_SIGN "s\nNAV %2 " UTF8_MICRO_SIGN "s") .arg(wr->end_tsf-wr->start_tsf).arg(wr->nav)); } } bool WirelessTimeline::event(QEvent *event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); guint packet = find_packet(helpEvent->pos().x()); if (packet) { doToolTip(get_wlan_radio(packet), helpEvent->globalPos(), helpEvent->x()); } else { QToolTip::hideText(); event->ignore(); } return true; } return QWidget::event(event); } void WirelessTimeline::wheelEvent(QWheelEvent *event) { // "Most mouse types work in steps of 15 degrees, in which case the delta // value is a multiple of 120; i.e., 120 units * 1/8 = 15 degrees" double steps = event->delta() / 120.0; if (steps != 0.0) { zoom_level += steps; if (zoom_level < 0) zoom_level = 0; if (zoom_level > TIMELINE_MAX_ZOOM) zoom_level = TIMELINE_MAX_ZOOM; zoom((float)event->pos().x() / width()); } } void WirelessTimeline::bgColorizationProgress(int first, int last) { if (isHidden()) return; struct wlan_radio *first_wr = get_wlan_radio(first); struct wlan_radio *last_wr = get_wlan_radio(last-1); int x = position(first_wr->start_tsf, 1); int x_end = position(last_wr->end_tsf, 1); update(x, 0, x_end-x+1, height()); } // zoom at relative position 0.0 <= x_fraction <= 1.0. void WirelessTimeline::zoom(double x_fraction) { /* adjust the zoom around the selected packet */ guint64 file_range = last->end_tsf - first->start_tsf; guint64 center = start_tsf + x_fraction * (end_tsf - start_tsf); guint64 span = pow(file_range, 1.0 - zoom_level / TIMELINE_MAX_ZOOM); start_tsf = center - span * x_fraction; end_tsf = center + span * (1.0 - x_fraction); clip_tsf(); update(); } int WirelessTimeline::find_packet_tsf(guint64 tsf) { if (cfile.count < 1) return 0; if (cfile.count < 2) return 1; guint32 min_count = 1; guint32 max_count = cfile.count-1; guint64 min_tsf = get_wlan_radio(min_count)->end_tsf; guint64 max_tsf = get_wlan_radio(max_count)->end_tsf; for (;;) { if (tsf >= max_tsf) return max_count+1; if (tsf < min_tsf) return min_count; guint32 middle = (min_count + max_count)/2; if (middle == min_count) return middle+1; guint64 middle_tsf = get_wlan_radio(middle)->end_tsf; if (tsf >= middle_tsf) { min_count = middle; min_tsf = middle_tsf; } else { max_count = middle; max_tsf = middle_tsf; } }; } void WirelessTimeline::paintEvent(QPaintEvent *qpe) { QPainter p(this); // painting is done in device pixels in the x axis, get the ratio here float ratio = 1.0; #if QT_VERSION >= QT_VERSION_CHECK(5, 1, 0) ratio = p.device()->devicePixelRatio(); #endif unsigned int packet; double zoom; int last_x=-1; int left = qpe->rect().left()*ratio; int right = qpe->rect().right()*ratio; float rgb[TIMELINE_HEIGHT][3]; reset_rgb(rgb); zoom = ((double) width())/(end_tsf - start_tsf) * ratio; /* background is light grey */ p.fillRect(0, 0, width(), TIMELINE_HEIGHT, QColor(240,240,240)); /* background of packets visible in packet_list is white */ int top = packet_list->indexAt(QPoint(0,0)).row(); int bottom = packet_list->indexAt(QPoint(0,packet_list->viewport()->height())).row(); PacketListModel *model = packet_list->packetListModel(); int x1 = top == -1 ? 0 : position(get_wlan_radio(model->getRowFdata(top)->num)->start_tsf, ratio); int x2 = bottom == -1 ? width() : position(get_wlan_radio(model->getRowFdata(bottom)->num)->end_tsf, ratio); p.fillRect(QRectF(x1/ratio, 0, (x2-x1+1)/ratio, TIMELINE_HEIGHT), Qt::white); /* background of current packet is blue */ if (cfile.current_frame) { struct wlan_radio *wr = get_wlan_radio(cfile.current_frame->num); if (wr) { x1 = position(wr->start_tsf, ratio); x2 = position(wr->end_tsf, ratio); p.fillRect(QRectF(x1/ratio, 0, (x2-x1+1)/ratio, TIMELINE_HEIGHT), Qt::blue); } } QGraphicsScene qs; for (packet = find_packet_tsf(start_tsf + left/zoom - RENDER_EARLY); packet <= cfile.count; packet++) { frame_data *fdata = frame_data_sequence_find(cfile.frames, packet); struct wlan_radio *ri = get_wlan_radio(fdata->num); float x, width, red, green, blue; if (ri == NULL) continue; gint8 rssi = ri->aggregate ? ri->aggregate->rssi : ri->rssi; guint height = (rssi+100)/2; gint end_nav; /* leave a margin above the packets so the selected packet can be seen */ if (height > TIMELINE_HEIGHT/2-6) height = TIMELINE_HEIGHT/2-6; /* ensure shortest packets are clearly visible */ if (height < 2) height = 2; /* skip frames we don't have start and end data for */ /* TODO: show something, so it's clear a frame is missing */ if (ri->start_tsf == 0 || ri->end_tsf == 0) continue; x = ((gint64) (ri->start_tsf - start_tsf))*zoom; /* is there a previous anti-aliased pixel to output */ if (last_x >= 0 && ((int) x) != last_x) { /* write it out now */ render_pixels(p, last_x, 1, rgb, ratio); last_x = -1; } /* does this packet start past the right edge of the window? */ if (x >= right) { break; } width = (ri->end_tsf - ri->start_tsf)*zoom; if (width < 0) { continue; } /* is this packet completely to the left of the displayed area? */ // TODO clip NAV line properly if we are displaying it if ((x + width) < left) continue; /* remember the first displayed packet */ if (first_packet < 0) first_packet = packet; if (fdata->color_filter) { const color_t *c = &((color_filter_t *) fdata->color_filter)->fg_color; red = c->red / 65535.0; green = c->green / 65535.0; blue = c->blue / 65535.0; } else { red = green = blue = 0.0; } /* record NAV field at higher magnifications */ end_nav = x + width + ri->nav*zoom; if (zoom >= 0.01 && ri->nav && end_nav > 0) { gint y = 2*(packet % (TIMELINE_HEIGHT/2)); qs.addLine(QLineF((x+width)/ratio, y, end_nav/ratio, y), QPen(pcolor(red,green,blue))); } /* does this rectangle fit within one pixel? */ if (((int) x) == ((int) (x+width))) { /* accumulate it for later rendering together * with all other sub pixels that fall within this * pixel */ last_x = x; accumulate_rgb(rgb, height, fdata->flags.passed_dfilter, width, red, green, blue); } else { /* it spans more than 1 pixel. * first accumulate the part that does fit */ float partial = ((int) x) + 1 - x; accumulate_rgb(rgb, height, fdata->flags.passed_dfilter, partial, red, green, blue); /* and render it */ render_pixels(p, (int) x, 1, rgb, ratio); last_x = -1; x += partial; width -= partial; /* are there any whole pixels of width left to draw? */ if (width > 1.0) { render_rectangle(p, x, width, height, fdata->flags.passed_dfilter, red, green, blue, ratio); x += (int) width; width -= (int) width; } /* is there a partial pixel left */ if (width > 0.0) { last_x = x; accumulate_rgb(rgb, height, fdata->flags.passed_dfilter, width, red, green, blue); } } } // draw the NAV lines last, so they appear on top of the packets qs.render(&p, rect(), rect()); } /* * Editor modelines * * Local Variables: * c-basic-offset: 4 * tab-width: 8 * indent-tabs-mode: nil * End: * * ex: set shiftwidth=4 tabstop=8 expandtab: * :indentSize=4:tabSize=8:noTabs=true: */