1// This file is about the layout (Preproess() and Group()) of the IPMI time line view.
2// The layout happens according to the following sequence that is very similar to how
3// the layout is done for DBus messages:
4//
5// 1. User clicks any of the checkboxes for the grouping fields (NetFN, CMD)
6// 2. OnGroupByConditionChanged() is called
7// 3. OnGroupByConditionChanged() calls PreProcess() and Group()
8// 4. PreProcess() takes the IPMI messages extracted from the DBus capture
9//    (g_ipmi_parsed_entries), and determines the start time.
10// 5. Group() takes the IPMI messages, and the list of keys, and groups the messages
11//    by the keys. The output is picked up by GenerateTimeLine(), which writes the
12//    timeline data into the Intervals and Titles arrays. The draw loop immediately
13//    picks up the updated Intervals and Titles arrays and draws on the canvas
14//    accordingly.
15
16const {dialog} = require('electron');
17const {fs} = require('file-system');
18const {util} = require('util');
19const {exec} = require('child_process');
20
21// Main view objects
22var ipmi_timeline_view = new IPMITimelineView();
23ipmi_timeline_view.IsTimeDistributionEnabled = true;
24
25var btn_start_capture = document.getElementById('btn_start_capture');
26var select_capture_mode = document.getElementById('select_capture_mode');
27var capture_info = document.getElementById('capture_info');
28
29var radio_open_file = document.getElementById('radio_open_file');
30var radio_capture = document.getElementById('radio_capture');
31var title_open_file = document.getElementById('title_open_file');
32var title_capture = document.getElementById('title_capture');
33
34// Set up Electron-related stuff here; Electron does not allow inlining button
35// events
36document.getElementById('c1').addEventListener(
37    'click', OnGroupByConditionChanged);  // NetFN
38document.getElementById('c2').addEventListener(
39    'click', OnGroupByConditionChanged);  // CMD
40
41// Zoom in button
42document.getElementById('btn_zoom_in').addEventListener('click', function() {
43  ipmi_timeline_view.BeginZoomAnimation(0.5);
44  boost_asio_handler_timeline_view.BeginZoomAnimation(0.5);
45});
46
47// Zoom out button
48document.getElementById('btn_zoom_out').addEventListener('click', function() {
49  ipmi_timeline_view.BeginZoomAnimation(-1);
50  boost_asio_handler_timeline_view.BeginZoomAnimation(-1);
51});
52
53// Pan left button
54document.getElementById('btn_pan_left').addEventListener('click', function() {
55  ipmi_timeline_view.BeginPanScreenAnimaton(-0.5);
56  boost_asio_handler_timeline_view.BeginPanScreenAnimaton(-0.5);
57});
58
59// Pan right button
60document.getElementById('btn_pan_right').addEventListener('click', function() {
61  ipmi_timeline_view.BeginPanScreenAnimaton(0.5);
62  boost_asio_handler_timeline_view.BeginPanScreenAnimaton(0.5);
63});
64
65// Reset zoom button
66document.getElementById('btn_zoom_reset').addEventListener('click', function() {
67  ipmi_timeline_view.BeginSetBoundaryAnimation(
68      RANGE_LEFT_INIT, RANGE_RIGHT_INIT)
69  dbus_timeline_view.BeginSetBoundaryAnimation(
70      RANGE_LEFT_INIT, RANGE_RIGHT_INIT)
71  boost_asio_handler_timeline_view.BeginSetBoundaryAnimation(
72      RANGE_LEFT_INIT, RANGE_RIGHT_INIT)
73})
74
75// Generate replay
76document.getElementById('gen_replay_ipmitool1')
77    .addEventListener('click', function() {
78      GenerateIPMIToolIndividualCommandReplay(HighlightedRequests);
79    });
80document.getElementById('gen_replay_ipmitool2')
81    .addEventListener('click', function() {
82      GenerateIPMIToolExecListReplay(HighlightedRequests);
83    });
84document.getElementById('gen_replay_ipmid_legacy')
85    .addEventListener('click', function() {
86      GenerateBusctlReplayLegacyInterface(HighlightedRequests);
87    });
88document.getElementById('gen_replay_ipmid_new')
89    .addEventListener('click', function() {
90      GenerateBusctlReplayNewInterface(HighlightedRequests);
91    });
92document.getElementById('btn_start_capture')
93    .addEventListener('click', function() {
94      let h = document.getElementById('text_hostname').value;
95      g_capture_state = 'started';
96      StartCapture(h);
97    });
98
99// For capture mode
100document.getElementById('btn_stop_capture')
101    .addEventListener('click', function() {
102      StopCapture();
103    });
104document.getElementById('select_capture_mode')
105    .addEventListener('click', OnCaptureModeChanged);
106radio_open_file.addEventListener('click', OnAppModeChanged);
107radio_capture.addEventListener('click', OnAppModeChanged);
108
109radio_open_file.click();
110
111// App mode: open file or capture
112function OnAppModeChanged() {
113  title_open_file.style.display = 'none';
114  title_capture.style.display = 'none';
115  if (radio_open_file.checked) {
116    title_open_file.style.display = 'block';
117  }
118  if (radio_capture.checked) {
119    title_capture.style.display = 'block';
120  }
121}
122
123// Capture mode: Live capture or staged capture
124function OnCaptureModeChanged() {
125  let x = select_capture_mode;
126  let i = capture_info;
127  let desc = '';
128  switch (x.value) {
129    case 'live':
130      desc = 'Live: read BMC\'s dbus-monitor console output directly';
131      g_capture_mode = 'live';
132      break;
133    case 'staged':
134      desc =
135          'Staged, IPMI only: Store BMC\'s dbus-monitor output in a file and transfer back for display';
136      g_capture_mode = 'staged';
137      break;
138    case 'staged2':
139      desc =
140          'Staged, DBus + IPMI: Store BMC\'s busctl output in a file and transfer back for display';
141      g_capture_mode = 'staged2';
142      break;
143  }
144  i.textContent = desc;
145}
146
147// Data
148var HistoryHistogram = [];
149// var Data_IPMI = []
150
151// =====================
152
153let Intervals = [];
154let Titles = [];
155let HighlightedRequests = [];
156let GroupBy = [];
157let GroupByStr = '';
158
159// (NetFn, Cmd) -> [ Bucket Indexes ]
160// Normalized (0~1) bucket index for the currently highlighted IPMI requests
161let IpmiVizHistHighlighted = {};
162let HistogramThresholds = {};
163
164function IsIntersected(i0, i1) {
165  return (!((i0[1] < i1[0]) || (i0[0] > i1[1])));
166}
167
168function IsIntersectedPixelCoords(i0, i1) {
169  if (i0[1] == undefined || isNaN(i0[1])) {
170    return (Math.abs(i0[0] - i1[0]) < 5);
171  } else {
172    return (IsIntersected(i0, i1));
173  }
174}
175
176var NetFnCmdToDescription = {
177  '6, 1': 'App-GetDeviceId',
178  '6, 3': 'App-WarmReset',
179  '10, 64': 'Storage-GetSelInfo',
180  '10, 35': 'Storage-GetSdr',
181  '4, 32': 'Sensor-GetDeviceSDRInfo',
182  '4, 34': 'Sensor-ReserveDeviceSDRRepo',
183  '4, 47': 'Sensor-GetSensorType',
184  '10, 34': 'Storage-ReserveSdrRepository',
185  '46, 50': 'OEM Extension',
186  '4, 39': 'Sensor-GetSensorThresholds',
187  '4, 45': 'Sensor-GetSensorReading',
188  '10, 67': 'Storage-GetSelEntry',
189  '58, 196': 'IBM_OEM',
190  '10, 32': 'Storage-GetSdrRepositoryInfo',
191  '4, 33': 'Sensor-GetDeviceSDR',
192  '6, 54': 'App-Get BT Interface Capabilities',
193  '10, 17': 'Storage-ReadFruData',
194  '10, 16': 'Storage-GetFruInventoryAreaInfo',
195  '4, 2': 'Sensor-PlatformEvent',
196  '4, 48': 'Sensor-SetSensor',
197  '6, 34': 'App-ResetWatchdogTimer'
198};
199
200const CANVAS_H = document.getElementById('my_canvas_ipmi').height;
201const CANVAS_W = document.getElementById('my_canvas_ipmi').width;
202
203var LowerBoundTime = RANGE_LEFT_INIT;
204var UpperBoundTime = RANGE_RIGHT_INIT;
205var LastTimeLowerBound;
206var LastTimeUpperBound;
207// Dirty flags for determining when to redraw the canvas
208let IsCanvasDirty = true;
209let IsHighlightDirty = false;
210// Animating left and right boundaries
211let IsAnimating = false;
212let LowerBoundTimeTarget = LowerBoundTime;
213let UpperBoundTimeTarget = UpperBoundTime;
214// For keyboard interaction: arrow keys and Shift
215let CurrDeltaX = 0;         // Proportion of Canvas to scroll per frame.
216let CurrDeltaZoom = 0;      // Delta zoom per frame.
217let CurrShiftFlag = false;  // Whether the Shift key is depressed
218
219// TODO: these variables are shared across all views but are now in ipmi_timeline_vis.js, need to move to some other location some time
220const LEFT_MARGIN = 640
221const RIGHT_MARGIN = 1390;
222const LINE_HEIGHT = 15;
223const LINE_SPACING = 17;
224const YBEGIN = 22 + LINE_SPACING;
225const TOP_HORIZONTAL_SCROLLBAR_HEIGHT = YBEGIN - LINE_HEIGHT / 2; // ybegin is the center of the 1st line of the text so need to minus line_height/2
226const BOTTOM_HORIZONTAL_SCROLLBAR_HEIGHT = LINE_HEIGHT;
227const TEXT_Y0 = 3;
228const HISTOGRAM_W = 100, HISTOGRAM_H = LINE_SPACING;
229const HISTOGRAM_X = 270;
230// If some request's time is beyond the right tail, it's considered "too long"
231// If some request's time is below the left tail it's considered "good"
232// const HISTOGRAM_LEFT_TAIL_WIDTH = 0.05, HISTOGRAM_RIGHT_TAIL_WIDTH = 0.05;
233// temporarily disabled for now
234const HISTOGRAM_LEFT_TAIL_WIDTH = -1, HISTOGRAM_RIGHT_TAIL_WIDTH = -1;
235const SCROLL_BAR_WIDTH = 16;
236
237let IpmiVizHistogramImageData = {};  // Image data for rendered histogram
238
239// Input is the data that's completed layout
240// is_free_x:     Should each histogram has its own X range or not
241// num_buckets: # of buckets for histograms
242// theta: top and bottom portion to cut
243function ComputeHistogram(num_buckets = 30, is_free_x = true) {
244  let global_lb = Infinity, global_ub = -Infinity;
245  IpmiVizHistogramImageData = {};
246  // Global minimal and maximal values
247  for (let i = 0; i < Intervals.length; i++) {
248    let interval = Intervals[i];
249    let l = Math.min.apply(Math, interval.map(function(x) {
250      return x[1] - x[0];
251    }));
252    let u = Math.max.apply(Math, interval.map(function(x) {
253      return x[1] - x[0];
254    }));
255    global_lb = Math.min(l, global_lb);
256    global_ub = Math.max(u, global_ub);
257  }
258
259  HistoryHistogram = [];
260  for (let i = 0; i < Intervals.length; i++) {
261    let interval = Intervals[i];
262    let lb = global_lb, ub = global_ub;
263    if (is_free_x == true) {
264      lb = Math.min.apply(Math, interval.map(function(x) {
265        return x[1] - x[0];
266      }));
267      ub = Math.max.apply(Math, interval.map(function(x) {
268        return x[1] - x[0];
269      }));
270    }
271    const EPS = 1e-2;
272    if (lb == ub) ub = lb + EPS;
273    let line = [lb * 1000000, ub * 1000000];  // to usec
274    let buckets = [];
275    for (let j = 0; j < num_buckets; j++) buckets.push(0);
276    for (let j = 0; j < interval.length; j++) {
277      let t = interval[j][1] - interval[j][0];
278      let bucket_idx = parseInt(t / ((ub - lb) / num_buckets));
279      buckets[bucket_idx]++;
280    }
281    line.push(buckets);
282    HistoryHistogram[Titles[i].title] = line;
283  }
284}
285
286function Preprocess(data) {
287  preprocessed = [];
288  let StartingUsec_IPMI;
289
290  if (g_StartingSec == undefined) {
291    StartingUsec_IPMI = undefined;
292  } else {
293    StartingUsec_IPMI = g_StartingSec * 1000000;
294  }
295
296  for (let i = 0; i < data.length; i++) {
297    let entry = data[i].slice();
298    let lb = entry[2], ub = entry[3];
299
300    // Only when IPMI view is present (i.e. no DBus pcap is loaded)
301    if (i == 0 && StartingUsec_IPMI == undefined) {
302      StartingUsec_IPMI = lb;
303    }
304
305    entry[2] = lb - StartingUsec_IPMI;
306    entry[3] = ub - StartingUsec_IPMI;
307    preprocessed.push(entry);
308  }
309  return preprocessed;
310}
311
312let SHOW_BLOB_DETAILS = true;
313function Group(data, groupBy) {
314  let grouped = {};
315
316  // If has netfn and cmd: use "NetFN, CMD" as key
317  // Otherwise, use "NetFN" as key
318  // This distinction is made if the user chooses to label operation on each
319  // blob individually
320
321  // Key:   blob name
322  // Value: the commands that operate on the particular blob
323  let sid2blobid = {}
324
325  for (let n = 0; n < data.length; n++) {
326    const p = data[n];
327    const netfn = p[0], cmd = p[1], req = p[4], res = p[5];
328    if (netfn == 46 && cmd == 128) {
329      const oen = req[0] + req[1] * 256 + req[2] * 65536;
330      if (oen == 0xc2cf) {  // Blob operations
331        const blobcmd =
332            req[3];  // Refer to https://github.com/openbmc/phosphor-ipmi-blobs
333
334        // The IPMI blob commands are visible on DBus, another WIP command-line tool that
335        // utilizes this fact to show information about blobs can be found here:
336        // https://gerrit.openbmc-project.xyz/c/openbmc/openbmc-tools/+/41451
337
338        let sid, blobid;
339
340        // layout of req
341        //  0  1  2   3  4  5   6  7  8  9  10 ...
342        // CF C2 00 CMD [CRC ] [ other stuff  ]
343
344        // layout of res
345        //  0  1  2   3  4   5   6  7  8  ...
346        // CF C2 00  [CRC ] [other stuff]
347
348        // Determining blob id and session ID
349        switch (blobcmd) {
350          case 3:
351          case 4:
352          case 5:
353          case 6:
354          case 9:
355          case 10: {
356            const sid = req[6] + req[7] * 256;
357            blobid = sid2blobid[sid];
358            if (blobid != undefined) {
359              p.key = blobid;
360            }
361            break;
362          }
363          case 7:
364          case 8: {
365            blobid = '';
366            for (let i = 6; i < req.length; i++) {
367              blobid += String.fromCharCode(req[i]);
368            }
369            break;
370          }
371        }
372
373        switch (blobcmd) {
374          case 2: {  // open
375            blobid = '';
376            for (let i = 8; i < req.length; i++) {
377              if (req[i] == 0)
378                break;
379              else
380                blobid += String.fromCharCode(req[i]);
381            }
382            p.key = blobid;
383            sid = res[5] + res[6] * 256;  // session_id
384            sid2blobid[sid] = blobid;
385            break;
386          }
387          case 3: {  // Read
388
389            break;
390          }
391          case 4: {  // Write
392            const offset =
393                req[8] + req[9] * 256 + req[10] * 65536 + req[11] * 16777216;
394            p.offset = offset;
395            break;
396          }
397          case 5: {  // Commit
398            break;
399          }
400          case 6: {  // Close
401            break;
402          }
403        }
404      }
405    }
406  }
407
408  const idxes = {'NetFN': 0, 'CMD': 1};
409
410  //
411  for (let n = 0; n < data.length; n++) {
412    const p = data[n];
413    let key = '';
414    if (p.key != undefined)
415      key = p.key;
416    else if (p[0] != '' && p[1] != '') {
417      for (let i = 0; i < groupBy.length; i++) {
418        if (i > 0) {
419          key += ', ';
420        }
421        key += p[idxes[groupBy[i]]];
422      }
423    }
424
425    if (grouped[key] == undefined) {
426      grouped[key] = [];
427    }
428    grouped[key].push(p);
429  }
430
431  return grouped;
432}
433
434function GenerateTimeLine(grouped) {
435  const keys = Object.keys(grouped);
436  let sortedKeys = keys.slice();
437  // If NetFN and CMD are both selected, sort by NetFN then CMD
438  // In this case, all "keys" are string-encoded integer pairs
439  if (keys.length > 0 && ipmi_timeline_view.GroupBy.length == 2) {
440    sortedKeys = sortedKeys.sort(function(a, b) {
441      a = a.split(',');
442      b = b.split(',');
443      if (a.length == 2 && b.length == 2) {
444        let aa = parseInt(a[0]) * 256 + parseInt(a[1]);
445        let bb = parseInt(b[0]) * 256 + parseInt(b[1]);
446        return aa < bb ? -1 : (aa > bb ? 1 : 0);
447      } else {
448        return a < b ? -1 : (a > b ? 1 : 0);
449      }
450    });
451  }
452
453  Intervals = [];
454  Titles = [];
455  for (let i = 0; i < sortedKeys.length; i++) {
456    Titles.push({"header":false, "title":sortedKeys[i], "intervals_idxes":[i]});
457    line = [];
458    for (let j = 0; j < grouped[sortedKeys[i]].length; j++) {
459      let entry = grouped[sortedKeys[i]][j];
460      // Lower bound, Upper bound, and a reference to the original request
461      line.push([
462        parseFloat(entry[2]) / 1000000, parseFloat(entry[3]) / 1000000, entry,
463        'ok', 0
464      ]);
465    }
466    Intervals.push(line);
467  }
468
469  ipmi_timeline_view.Intervals = Intervals.slice();
470  ipmi_timeline_view.Titles = Titles.slice();
471  ipmi_timeline_view.LayoutForOverlappingIntervals();
472}
473
474function OnGroupByConditionChanged() {
475  const tags = ['c1', 'c2'];
476  const v = ipmi_timeline_view;
477  v.GroupBy = [];
478  v.GroupByStr = '';
479  for (let i = 0; i < tags.length; i++) {
480    let cb = document.getElementById(tags[i]);
481    if (cb.checked) {
482      v.GroupBy.push(cb.value);
483      if (v.GroupByStr.length > 0) {
484        v.GroupByStr += ', ';
485      }
486      v.GroupByStr += cb.value;
487    }
488  }
489  let preproc = Preprocess(Data_IPMI);
490  grouped = Group(preproc, v.GroupBy);
491  GenerateTimeLine(grouped);
492
493  IsCanvasDirty = true;
494  ipmi_timeline_view.IsCanvasDirty = true;
495}
496
497function MapXCoord(x, left_margin, right_margin, rl, rr) {
498  let ret = left_margin + (x - rl) / (rr - rl) * (right_margin - left_margin);
499  if (ret < left_margin) {
500    ret = left_margin;
501  } else if (ret > right_margin) {
502    ret = right_margin;
503  }
504  return ret;
505}
506
507function draw_timeline(ctx) {
508  ipmi_timeline_view.Render(ctx);
509}
510
511
512window.addEventListener('keydown', function() {
513  if (event.keyCode == 37) {  // Left Arrow
514    ipmi_timeline_view.CurrDeltaX = -0.004;
515    dbus_timeline_view.CurrDeltaX = -0.004;
516  } else if (event.keyCode == 39) {  // Right arrow
517    ipmi_timeline_view.CurrDeltaX = 0.004;
518    dbus_timeline_view.CurrDeltaX = 0.004;
519  } else if (event.keyCode == 16) {  // Shift
520    ipmi_timeline_view.CurrShiftFlag = true;
521    dbus_timeline_view.CurrShiftFlag = true;
522  } else if (event.keyCode == 38) {  // Up arrow
523    ipmi_timeline_view.CurrDeltaZoom = 0.01;
524    dbus_timeline_view.CurrDeltaZoom = 0.01;
525  } else if (event.keyCode == 40) {  // Down arrow
526    ipmi_timeline_view.CurrDeltaZoom = -0.01;
527    dbus_timeline_view.CurrDeltaZoom = -0.01;
528  }
529});
530
531window.addEventListener('keyup', function() {
532  if (event.keyCode == 37 || event.keyCode == 39) {
533    ipmi_timeline_view.CurrDeltaX = 0;
534    dbus_timeline_view.CurrDeltaX = 0;
535  } else if (event.keyCode == 16) {
536    ipmi_timeline_view.CurrShiftFlag = false;
537    dbus_timeline_view.CurrShiftFlag = false;
538  } else if (event.keyCode == 38 || event.keyCode == 40) {
539    ipmi_timeline_view.CurrDeltaZoom = 0;
540    dbus_timeline_view.CurrDeltaZoom = 0;
541  }
542});
543
544function MouseXToTimestamp(x) {
545  let ret = (x - LEFT_MARGIN) / (RIGHT_MARGIN - LEFT_MARGIN) *
546          (UpperBoundTime - LowerBoundTime) +
547      LowerBoundTime;
548  ret = Math.max(ret, LowerBoundTime);
549  ret = Math.min(ret, UpperBoundTime);
550  return ret;
551}
552
553let HighlightedRegion = {t0: -999, t1: -999};
554
555function IsHighlighted() {
556  return (HighlightedRegion.t0 != -999 && HighlightedRegion.t1 != -999);
557}
558
559function Unhighlight() {
560  HighlightedRegion.t0 = -999;
561  HighlightedRegion.t1 = -999;
562}
563
564function UnhighlightIfEmpty() {
565  if (HighlightedRegion.t0 == HighlightedRegion.t1) {
566    Unhighlight();
567    return true;
568  }
569  return false;
570}
571
572let MouseState = {
573  hovered: true,
574  pressed: false,
575  x: 0,
576  y: 0,
577  hoveredVisibleLineIndex: -999,
578  hoveredSide: undefined,
579  IsHoveredOverHorizontalScrollbar: function() {
580    if (this.hoveredSide == "top_horizontal_scrollbar") return true;
581    else if (this.hoveredSide == "bottom_horizontal_scrollbar") return true;
582    else return false;
583  }
584};
585let Canvas = document.getElementById('my_canvas_ipmi');
586
587Canvas.onmousemove = function(event) {
588  const v = ipmi_timeline_view;
589  v.MouseState.x = event.pageX - this.offsetLeft;
590  v.MouseState.y = event.pageY - this.offsetTop;
591  if (v.MouseState.pressed == true &&
592      v.MouseState.hoveredSide == 'timeline') {  // Update highlighted area
593    v.HighlightedRegion.t1 = v.MouseXToTimestamp(v.MouseState.x);
594  }
595  v.OnMouseMove();
596  v.IsCanvasDirty = true;
597
598  v.linked_views.forEach(function(u) {
599    u.MouseState.x = event.pageX - Canvas.offsetLeft;
600    u.MouseState.y = 0;                  // Do not highlight any entry
601    if (u.MouseState.pressed == true &&
602        u.MouseState.hoveredSide == 'timeline') {  // Update highlighted area
603      u.HighlightedRegion.t1 = u.MouseXToTimestamp(u.MouseState.x);
604    }
605    u.OnMouseMove();
606    u.IsCanvasDirty = true;
607  });
608};
609
610Canvas.onmouseover = function() {
611  ipmi_timeline_view.OnMouseMove();
612};
613
614Canvas.onmouseleave = function() {
615  ipmi_timeline_view.OnMouseLeave();
616};
617
618Canvas.onmousedown = function(event) {
619  if (event.button == 0) {  // Left mouse button
620    ipmi_timeline_view.OnMouseDown();
621  }
622};
623
624Canvas.onmouseup = function(event) {
625  if (event.button == 0) {
626    ipmi_timeline_view.OnMouseUp();
627    // page-specific, not view-specific
628    let hint = document.getElementById('highlight_hint');
629    if (ipmi_timeline_view.UnhighlightIfEmpty()) {
630      hint.style.display = 'none';
631    } else {
632      hint.style.display = 'block';
633    }
634  }
635};
636
637Canvas.onwheel = function(event) {
638  ipmi_timeline_view.OnMouseWheel(event);
639};
640
641// This function is not specific to TimelineView so putting it here
642function OnHighlightedChanged(reqs) {
643  let x = document.getElementById('ipmi_replay');
644  let i = document.getElementById('ipmi_replay_output');
645  let cnt = document.getElementById('highlight_count');
646  cnt.innerHTML = '' + reqs.length;
647  i.style.display = 'none';
648  if (reqs.length > 0) {
649    x.style.display = 'block';
650  } else
651    x.style.display = 'none';
652  let o = document.getElementById('ipmi_replay_output');
653  o.style.display = 'none';
654  o.textContent = '';
655}
656
657function ToHexString(bytes, prefix, sep) {
658  let ret = '';
659  for (let i = 0; i < bytes.length; i++) {
660    if (i > 0) {
661      ret += sep;
662    }
663    ret += prefix + bytes[i].toString(16);
664  }
665  return ret;
666}
667
668function ToASCIIString(bytes) {
669  ret = '';
670  for (let i = 0; i < bytes.length; i++) {
671    ret = ret + String.fromCharCode(bytes[i]);
672  }
673  return ret;
674}
675
676function ShowReplayOutputs(x, ncols) {
677  let o = document.getElementById('ipmi_replay_output');
678  o.cols = ncols;
679  o.style.display = 'block';
680  o.textContent = x;
681}
682
683function GenerateIPMIToolIndividualCommandReplay(reqs) {
684  let x = '';
685  for (let i = 0; i < reqs.length; i++) {
686    let req = reqs[i];
687    // [0]: NetFN, [1]: cmd, [4]: payload
688    // NetFN and cmd are DECIMAL while payload is HEXADECIMAL.
689    x = x + 'ipmitool raw ' + req[0] + ' ' + req[1] + ' ' +
690        ToHexString(req[4], '0x', ' ') + '\n';
691  }
692  ShowReplayOutputs(x, 80);
693}
694
695function GenerateIPMIToolExecListReplay(reqs) {
696  console.log(reqs.length);
697  let x = '';
698  for (let i = 0; i < reqs.length; i++) {
699    let req = reqs[i];
700    x = x + 'raw ' +
701        ToHexString([req[0]].concat([req[1]]).concat(req[4]), '0x', ' ') + '\n';
702  }
703  ShowReplayOutputs(x, 80);
704}
705
706function GenerateBusctlReplayLegacyInterface(reqs) {
707  console.log(reqs.length);
708  let serial = 0;
709  let x = '';
710  for (let i = 0; i < reqs.length; i++) {
711    let req = reqs[i];
712    x = x +
713        'busctl --system emit  /org/openbmc/HostIpmi/1 org.openbmc.HostIpmi ReceivedMessage yyyyay ';
714    x = x + serial + ' ' + req[0] + ' 0 ' + req[1] + ' ' + req[4].length + ' ' +
715        ToHexString(req[4], '0x', ' ') + '\n';
716    serial = (serial + 1) % 256;
717  }
718  ShowReplayOutputs(x, 120);
719}
720
721function GenerateBusctlReplayNewInterface(reqs) {
722  console.log(reqs.length);
723  let x = '';
724  for (let i = 0; i < reqs.length; i++) {
725    let req = reqs[i];
726    x = x +
727        'busctl --system call xyz.openbmc_project.Ipmi.Host /xyz/openbmc_project/Ipmi xyz.openbmc_project.Ipmi.Server execute yyyaya{sv} ';
728    x = x + req[0] + ' 0 ' + req[1] + ' ' + req[4].length + ' ' +
729        ToHexString(req[4], '0x', ' ');
730    +' 0\n';
731  }
732  ShowReplayOutputs(x, 150);
733}
734