package org.yamcs.ui.packetviewer;
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.prefs.Preferences;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextPane;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.ProgressMonitor;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.plaf.SplitPaneUI;
import javax.swing.plaf.basic.BasicSplitPaneUI;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import javax.swing.text.StyledDocument;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yamcs.ConfigurationException;
import org.yamcs.YConfiguration;
import org.yamcs.YamcsException;
import org.yamcs.api.MediaType;
import org.yamcs.api.YamcsConnectionProperties;
import org.yamcs.api.rest.RestClient;
import org.yamcs.api.ws.ConnectionListener;
import org.yamcs.api.ws.WebSocketRequest;
import org.yamcs.archive.PacketWithTime;
import org.yamcs.parameter.ParameterRequestManager;
import org.yamcs.parameter.ParameterValue;
import org.yamcs.protobuf.Yamcs.TmPacketData;
import org.yamcs.protobuf.YamcsManagement.YamcsInstance;
import org.yamcs.ui.PrefsObject;
import org.yamcs.ui.YamcsConnector;
import org.yamcs.utils.CcsdsPacket;
import org.yamcs.utils.TimeEncoding;
import org.yamcs.web.websocket.CommandQueueResource;
import org.yamcs.web.websocket.PacketResource;
import org.yamcs.xtce.BaseDataType;
import org.yamcs.xtce.Calibrator;
import org.yamcs.xtce.DataEncoding;
import org.yamcs.xtce.DatabaseLoadException;
import org.yamcs.xtce.EnumeratedParameterType;
import org.yamcs.xtce.FloatDataEncoding;
import org.yamcs.xtce.IntegerDataEncoding;
import org.yamcs.xtce.Parameter;
import org.yamcs.xtce.SequenceContainer;
import org.yamcs.xtce.XtceDb;
import org.yamcs.xtceproc.XtceDbFactory;
import org.yamcs.xtceproc.XtceTmProcessor;
import com.google.protobuf.ByteString;
import io.netty.handler.codec.http.HttpMethod;
public class PacketViewer extends JFrame implements ActionListener,
TreeSelectionListener, ParameterRequestManager, ConnectionListener {
private static final long serialVersionUID = 1L;
private static final Logger log=LoggerFactory.getLogger(PacketViewer.class);
static PacketViewer theApp;
static int maxLines = -1;
XtceDb xtcedb;
File lastFile;
JSplitPane hexSplit;
JTextPane hexText;
StyledDocument hexDoc;
Style fixedStyle, highlightedStyle;
JMenu fileMenu;
List<JMenuItem> miRecentFiles;
JMenuItem miAutoScroll, miAutoSelect;
JTextArea logText;
JScrollPane logScrollpane;
PacketsTable packetsTable;
ParametersTable parametersTable;
JTree structureTree;
DefaultMutableTreeNode structureRoot;
DefaultTreeModel structureModel;
JSplitPane mainsplit;
FindParameterBar findBar;
ListPacket currentPacket;
OpenFileDialog openFileDialog;
static Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
static final SimpleDateFormat dateTimeFormatFine = new SimpleDateFormat("yyyy.MM.dd/DDD HH:mm:ss.SSS");
YamcsConnector yconnector;
ConnectDialog connectDialog;
GoToPacketDialog goToPacketDialog;
Preferences uiPrefs;
YamcsConnectionProperties connectionParams;
//used for decoding full packets
XtceTmProcessor tmProcessor;
boolean authenticationEnabled = false;
String streamName;
private String defaultNamespace;
public PacketViewer(int maxLines) throws ConfigurationException {
setDefaultCloseOperation(EXIT_ON_CLOSE);
uiPrefs = Preferences.userNodeForPackage(PacketViewer.class);
YConfiguration config = YConfiguration.getConfiguration("yamcs-ui");
if(config.containsKey("authenticationEnabled")) {
authenticationEnabled = config.getBoolean("authenticationEnabled");
}
if(config.containsKey("defaultNamespace")) {
defaultNamespace = config.getString("defaultNamespace");
}
// table to the left which shows one row per packet
packetsTable = new PacketsTable(this);
packetsTable.setMaxLines(maxLines);
JScrollPane packetScrollpane = new JScrollPane(packetsTable);
// table to the right which shows one row per parameter in the selected packet
parametersTable = new ParametersTable(this);
JScrollPane tableScrollpane = new JScrollPane(parametersTable);
tableScrollpane.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
if (e.getComponent().getWidth() < parametersTable.getPreferredSize().getWidth())
parametersTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
else
parametersTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
}
});
// tree to the right which shows the container structure of the selected packet
structureRoot = new DefaultMutableTreeNode();
structureModel = new DefaultTreeModel(structureRoot);
structureTree = new JTree(structureModel);
structureTree.setEditable(false);
structureTree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
structureTree.addTreeSelectionListener(this);
JScrollPane treeScrollpane = new JScrollPane(structureTree);
Insets oldInsets = UIManager.getInsets("TabbedPane.contentBorderInsets");
UIManager.put("TabbedPane.contentBorderInsets", new Insets(0, 0, 0, 0));
JTabbedPane tabpane = new JTabbedPane();
UIManager.put("TabbedPane.contentBorderInsets", oldInsets);
tabpane.add("Parameters", tableScrollpane);
tabpane.add("Structure", treeScrollpane);
findBar = new FindParameterBar(parametersTable);
JPanel parameterPanel = new JPanel(new BorderLayout());
parameterPanel.add(tabpane, BorderLayout.CENTER);
parameterPanel.add(findBar, BorderLayout.SOUTH);
// hexdump panel
hexText = new JTextPane() {
private static final long serialVersionUID = 1L;
@Override
public boolean getScrollableTracksViewportWidth() {
return false; // disable line wrap
}
};
hexDoc = hexText.getStyledDocument();
final Style defStyle = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);
fixedStyle = hexDoc.addStyle("fixed", defStyle);
StyleConstants.setFontFamily(fixedStyle, Font.MONOSPACED);
highlightedStyle = hexDoc.addStyle("highlighted", fixedStyle);
StyleConstants.setBackground(highlightedStyle, parametersTable.getSelectionBackground());
StyleConstants.setForeground(highlightedStyle, parametersTable.getSelectionForeground());
hexText.setEditable(false);
JScrollPane hexScrollpane = new JScrollPane(hexText);
hexScrollpane.getViewport().setBackground(hexText.getBackground());
hexSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, parameterPanel, hexScrollpane);
removeBorders(hexSplit);
hexSplit.setResizeWeight(0.7);
mainsplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, packetScrollpane, hexSplit);
removeBorders(mainsplit);
mainsplit.setResizeWeight(0.0);
// log text
logText = new JTextArea(3, 20);
logText.setEditable(false);
logScrollpane = new JScrollPane(logText);
JSplitPane logsplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, mainsplit, logScrollpane);
removeBorders(logsplit);
logsplit.setResizeWeight(1.0);
logsplit.setContinuousLayout(true);
installMenubar();
getContentPane().add(logsplit, BorderLayout.CENTER);
clearWindow();
updateTitle();
pack();
setVisible(true);
}
private void installMenubar() {
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
fileMenu = new JMenu("File");
fileMenu.setMnemonic(KeyEvent.VK_F);
menuBar.add(fileMenu);
// Ctrl on win/linux, Command on mac
int menuKey=Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
JMenuItem menuitem = new JMenuItem("Open...", KeyEvent.VK_O);
menuitem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, menuKey));
menuitem.setActionCommand("open file");
menuitem.addActionListener(this);
fileMenu.add(menuitem);
menuitem = new JMenuItem("Connect to Yamcs...");
menuitem.setMnemonic(KeyEvent.VK_C);
menuitem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, menuKey));
menuitem.setActionCommand("connect-yamcs");
menuitem.addActionListener(this);
fileMenu.add(menuitem);
fileMenu.addSeparator();
miRecentFiles = new ArrayList<JMenuItem>();
for (int i = 0; i < 4; i++) {
menuitem = new JMenuItem();
menuitem.setMnemonic(KeyEvent.VK_1 + i);
menuitem.setActionCommand("recent-file-" + i);
menuitem.addActionListener(this);
fileMenu.add(menuitem);
miRecentFiles.add(menuitem);
}
updateMenuWithRecentFiles();
if (!getRecentFiles().isEmpty())
fileMenu.addSeparator();
/*menuitem = new JMenuItem("Preferences", KeyEvent.VK_COMMA);
menuitem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_COMMA, menuKey));
menu.add(menuitem);
menu.addSeparator();*/
menuitem = new JMenuItem("Quit", KeyEvent.VK_Q);
menuitem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, menuKey));
menuitem.setActionCommand("quit");
menuitem.addActionListener(this);
fileMenu.add(menuitem);
JMenu menu = new JMenu("Edit");
menu.setMnemonic(KeyEvent.VK_E);
menuBar.add(menu);
Action openFindBarAction = findBar.getActionMap().get(FindParameterBar.OPEN_ACTION);
menu.add(new JMenuItem(openFindBarAction));
menu.addSeparator();
Action toggleMarkAction = packetsTable.getActionMap().get(PacketsTable.TOGGLE_MARK_ACTION_KEY);
menu.add(new JMenuItem(toggleMarkAction));
menu = new JMenu("Navigate");
menu.setMnemonic(KeyEvent.VK_N);
menuBar.add(menu);
Action goToPacketAction = packetsTable.getActionMap().get(PacketsTable.GO_TO_PACKET_ACTION_KEY);
menu.add(new JMenuItem(goToPacketAction));
menu.addSeparator();
Action backAction = packetsTable.getActionMap().get(PacketsTable.BACK_ACTION_KEY);
menu.add(new JMenuItem(backAction));
Action forwardAction = packetsTable.getActionMap().get(PacketsTable.FORWARD_ACTION_KEY);
menu.add(new JMenuItem(forwardAction));
menu.addSeparator();
Action upAction = packetsTable.getActionMap().get(PacketsTable.UP_ACTION_KEY);
menu.add(new JMenuItem(upAction));
Action downAction = packetsTable.getActionMap().get(PacketsTable.DOWN_ACTION_KEY);
menu.add(new JMenuItem(downAction));
menu = new JMenu("View");
menu.setMnemonic(KeyEvent.VK_V);
menuBar.add(menu);
miAutoScroll = new JCheckBoxMenuItem("Auto-Scroll To Last Packet");
miAutoScroll.setSelected(true);
menu.add(miAutoScroll);
miAutoSelect = new JCheckBoxMenuItem("Auto-Select Last Packet");
miAutoSelect.setSelected(false);
menu.add(miAutoSelect);
menu.addSeparator();
menuitem = new JMenuItem("Clear", KeyEvent.VK_C);
menuitem.setActionCommand("clear");
menuitem.addActionListener(this);
menu.add(menuitem);
}
void updateTitle() {
SwingUtilities.invokeLater(() -> {
StringBuilder title = new StringBuilder("Yamcs Packet Viewer");
if (connectionParams != null) {
title.append(" [").append(connectionParams.getUrl()).append("]");
} else if (lastFile != null) {
title.append(" - ");
title.append(lastFile.getName());
} else {
title.append(" (no file loaded)");
}
setTitle(title.toString());
});
}
void updateMenuWithRecentFiles() {
List<String[]> recentFiles = getRecentFiles();
int i;
for (i = 0; i < recentFiles.size() && i < miRecentFiles.size(); i++) {
String fileRef = recentFiles.get(i)[0];
int maxChars = 30;
if (fileRef.length() > maxChars) {
// Search first slash from right to left
int slashIndex = fileRef.lastIndexOf(File.separatorChar);
if (fileRef.length() - slashIndex > maxChars - 3) {
// Chop off the end of the string of the last path segment
fileRef = "..." + fileRef.substring(slashIndex, slashIndex + maxChars - 2*3) + "...";
} else {
// Output the complete filename, and fill up with initial path segments
fileRef = fileRef.substring(0, maxChars - 3 - (fileRef.length() - slashIndex))
+ "..." + fileRef.substring(slashIndex);
}
}
JMenuItem mi = miRecentFiles.get(i);
mi.setVisible(true);
mi.setText((i+1) + " " + fileRef);
mi.setToolTipText(recentFiles.get(i)[0]);
}
for (; i < miRecentFiles.size(); i++)
miRecentFiles.get(i).setVisible(false);
}
static void debugLogComponent(String name, JComponent c) {
Insets in = c.getInsets();
System.out.println("component " + name + ": "
+ "min(" + c.getMinimumSize().width + "," + c.getMinimumSize().height + ") "
+ "pref(" + c.getPreferredSize().width + "," + c.getPreferredSize().height + ") "
+ "max(" + c.getMaximumSize().width + "," + c.getMaximumSize().height + ") "
+ "size(" + c.getSize().width + "," + c.getSize().height + ") "
+ "insets(" + in.top + "," + in.left + "," +in.bottom + "," +in.right + ")");
}
@Override
public void log(final String s) {
SwingUtilities.invokeLater(() -> {
if(logText!=null) {
logText.append(s + "\n");
logScrollpane.getVerticalScrollBar().setValue(logScrollpane.getVerticalScrollBar().getMaximum());
} else {
System.err.println(s);
}
});
}
void showMessage(String msg) {
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this, msg, getTitle(), JOptionPane.PLAIN_MESSAGE);
});
}
void showError(String msg) {
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this, msg, getTitle(), JOptionPane.ERROR_MESSAGE);
});
}
@Override
public void actionPerformed(ActionEvent ae) {
String cmd = ae.getActionCommand();
if (cmd.equals("quit")) {
processWindowEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
} else if (cmd.equals("clear")) {
clearWindow();
} else if (cmd.equals("open file")) {
if(openFileDialog==null) {
try {
openFileDialog = new OpenFileDialog();
} catch (ConfigurationException e) {
showError("Cannot load local mdb config: "+e.getMessage());
return;
}
}
int returnVal = openFileDialog.showDialog(this);
if (returnVal == OpenFileDialog.APPROVE_OPTION) {
openFile(openFileDialog.getSelectedFile(), openFileDialog.getSelectedDbConfig());
}
} else if (cmd.equals("connect-yamcs")) {
if(connectDialog==null) {
connectDialog = new ConnectDialog(this, authenticationEnabled, true, true, true);
}
int ret = connectDialog.showDialog();
if(ret==ConnectDialog.APPROVE_OPTION) {
streamName = connectDialog.getStreamName();
connectYamcs(connectDialog.getConnectData());
}
} else if (cmd.startsWith("recent-file-")) {
JMenuItem mi = (JMenuItem) ae.getSource();
for (String[] recentFile : getRecentFiles())
if (recentFile[0].equals(mi.getToolTipText()))
openFile(new File(recentFile[0]), recentFile[1]);
}
}
private void openFile(File file, String xtceDb) {
if (!file.exists() || !file.isFile()) {
JOptionPane.showMessageDialog(null, "File not found: " + file, "File not found", JOptionPane.ERROR_MESSAGE);
return;
}
disconnect();
lastFile = file;
if(loadLocalXtcedb(xtceDb))
loadFile();
updateRecentFiles(lastFile, xtceDb);
}
private static class ShortReadException extends Exception{
public ShortReadException(long needed, long read, long offset) {
super();
this.needed = needed;
this.offset = offset;
this.read = read;
}
long needed;
long read;
long offset;
@Override
public String toString() {
return String.format("short seek %d/%d at offset %d", read, needed, offset);
}
}
private boolean loadLocalXtcedb(String configName) {
if(tmProcessor!=null) {
tmProcessor.stopAsync();
}
log("Loading local XTCE db "+configName);
try {
xtcedb=XtceDbFactory.createInstanceByConfig(configName);
} catch (ConfigurationException|DatabaseLoadException e) {
log.error(e.toString(), e);
showError(e.getMessage());
return false;
}
tmProcessor=new XtceTmProcessor(xtcedb);
tmProcessor.setParameterListener(this);
tmProcessor.startProvidingAll();
tmProcessor.startAsync();
log(String.format("Loaded definition of %d sequence container%s and %d parameter%s"
, xtcedb.getSequenceContainers().size(), (xtcedb.getSequenceContainers().size() != 1 ? "s":"")
, xtcedb.getParameterNames().size(), (xtcedb.getParameterNames().size() != 1 ? "s":"")));
packetsTable.setupParameterColumns();
return true;
}
private boolean loadRemoteXtcedb(String configName) {
if(tmProcessor!=null) {
tmProcessor.stopAsync();
}
log("Loading remote XTCE db for yamcs instance "+connectionParams.getInstance());
RestClient restClient = new RestClient(connectionParams);
try {
restClient.setAcceptMediaType(MediaType.JAVA_SERIALIZED_OBJECT);
restClient.setMaxResponseLength(10*1024*1024);//TODO make this configurable
byte[] serializedMdb = restClient.doRequest("/mdb/"+connectionParams.getInstance(), HttpMethod.GET).get();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(serializedMdb));
Object o=ois.readObject();
xtcedb=(XtceDb) o;
ois.close();
} catch (Exception e) {
e.printStackTrace();
showError(e.getMessage());
return false;
}
tmProcessor = new XtceTmProcessor(xtcedb);
tmProcessor.setParameterListener(this);
tmProcessor.startProvidingAll();
tmProcessor.startAsync();
packetsTable.setupParameterColumns();
log("Loaded "+xtcedb.getSequenceContainers().size()+" sequence containers and "+xtcedb.getParameterNames().size()+" parameters");
return true;
}
void loadFile() {
new SwingWorker<Void, TmPacketData>() {
ProgressMonitor progress;
@Override
protected Void doInBackground() throws Exception {
boolean isPacts=false;
long r;
try {
FileInputStream reader = new FileInputStream(lastFile);
byte[] fourb = new byte[4];
TmPacketData packet;
ByteBuffer buf;
int res;
int len, offset = 0;
clearWindow();
int progressMax = (maxLines == -1) ? (int)(lastFile.length()>>10) : maxLines;
progress = new ProgressMonitor(theApp, String.format("Loading %s", lastFile.getName()), null, 0, progressMax);
int packetCount = 0;
while (!progress.isCanceled()) {
res = reader.read(fourb, 0, 4);
if (res != 4) {
break;
}
buf = ByteBuffer.allocate(16);
long gentime = TimeEncoding.INVALID_INSTANT;
int seqcount = -1;
if ((fourb[0] & 0xe8) == 0x08) {// CCSDS packet
buf.put(fourb, 0, 4);
if((r=reader.read(buf.array(), 4, 12))!=12) {
throw new ShortReadException(16, r, offset);
}
gentime = CcsdsPacket.getInstant(buf);
seqcount = CcsdsPacket.getSequenceCount(buf);
} else if((fourb[2]==0) && (fourb[3]==0)) { //hrdp packet - first 4 bytes are packet size in little endian
if((r=reader.skip(6))!=6) {
throw new ShortReadException(6, r, offset);
}
offset+=10;
if((r=reader.read(buf.array()))!=16) {
throw new ShortReadException(16, r, offset);
}
} else {//pacts packet
isPacts=true;
//System.out.println("pacts packet");
// read ASCII header up to the second blank
int i, j;
StringBuffer hdr = new StringBuffer();
j = 0;
for(i=0;i<4;i++) {
hdr.append((char)fourb[i]);
if ( fourb[i] == 32 ) {
++j;
}
}
offset+=4;
while((j < 2) && (i < 20)) {
int c = reader.read();
if(c==-1) {
throw new ShortReadException(1, 0, offset);
}
offset++;
hdr.append((char)c);
if ( c == 32 ) {
++j;
}
i++;
}
if((r=reader.read(buf.array()))!=16) {
throw new ShortReadException(16,r,offset);
}
}
len = CcsdsPacket.getCccsdsPacketLength(buf) + 7;
if(len<16) {
log("Short packet read: length: "+len);
break;
}
byte[] bufn = new byte[len];
System.arraycopy(buf.array(), 0, bufn, 0, 16);
r=reader.read(bufn, 16, len-16);
if (r != len - 16) {
throw new ShortReadException(len-16, r, offset);
}
TmPacketData.Builder packetb = TmPacketData.newBuilder().setPacket(ByteString.copyFrom(bufn))
.setReceptionTime(TimeEncoding.getWallclockTime());
if (gentime != TimeEncoding.INVALID_INSTANT) {
packetb.setGenerationTime(gentime);
}
if (seqcount >= 0) {
packetb.setSequenceNumber(seqcount);
}
packet = packetb.build();
offset += len;
if(isPacts) {
if(reader.skip(1)!=1) {
throw new ShortReadException(1, 0, offset);
}
offset+=1;
}
publish(packet);
packetCount++;
if (packetCount == maxLines) {
break;
}
progress.setProgress((maxLines == -1) ? (int)(offset>>10) : packetCount);
}
reader.close();
} catch (Exception x) {
x.printStackTrace();
final String msg = String.format("Error while loading %s: %s", lastFile.getName(), x.getMessage());
log(msg);
showError(msg);
clearWindow();
lastFile = null;
}
return null;
}
@Override
protected void process(final List<TmPacketData> chunks) {
for (TmPacketData packet:chunks) {
packetsTable.packetReceived(packet);
}
}
@Override
protected void done() {
if (progress != null) {
if (progress.isCanceled()) {
clearWindow();
log(String.format("Cancelled loading %s", lastFile.getName()));
} else {
log(String.format("Loaded %d packet%s from \"%s\"",
packetsTable.getRowCount(),
packetsTable.getRowCount()!=1?"s":"", lastFile.getPath()));
}
progress.close();
}
updateTitle();
}
}.execute();
}
void clearWindow() {
SwingUtilities.invokeLater(() -> {
packetsTable.clear();
parametersTable.clear();
hexText.setText(null);
packetsTable.revalidate();
parametersTable.revalidate();
structureRoot.removeAllChildren();
structureTree.setRootVisible(false);
});
}
void highlightBitRanges(Range[] highlightBits) {
final int linesize = 5 + 5 * 8 + 16 + 1;
int n, tmp, textoffset, binHighStart, binHighStop, ascHighStart, ascHighStop;
hexDoc.setCharacterAttributes(0, hexDoc.getLength(), fixedStyle, true); // reset styles throughout the document
// apply style for highlighted parts
for (Range bitRange:highlightBits) {
if (bitRange == null) {
continue;
}
final int highlightStartNibble = bitRange.offset / 4;
final int highlightStopNibble = (bitRange.offset + bitRange.size + 3) / 4;
for (n = highlightStartNibble / 32 * 32 ; n < highlightStopNibble; n += 32) {
binHighStart = 5;
ascHighStart = 5 + 5 * 8;
tmp = highlightStartNibble - n;
if (tmp > 0) {
binHighStart += tmp + (tmp / 4);
ascHighStart += tmp / 2;
}
binHighStop = 5 + 5 * 8 - 1;
ascHighStop = 5 + 5 * 8 + 16;
tmp = n + 32 - highlightStopNibble;
if (tmp > 0) {
binHighStop -= tmp + (tmp / 4);
ascHighStop -= tmp / 2;
}
textoffset = linesize * (n / 32);
//System.out.println(String.format("setCharacterAttributes %d/%d %d %d %d/%d %d/%d",
// highlightStartNibble, highlightStopNibble, n, textoffset, binHighStart, binHighStop, ascHighStart, ascHighStop));
hexDoc.setCharacterAttributes(textoffset + binHighStart, binHighStop - binHighStart, highlightedStyle, true);
hexDoc.setCharacterAttributes(textoffset + ascHighStart, ascHighStop - ascHighStart, highlightedStyle, true);
}
}
// put the caret into the position of the first item (caret makes itself visible by default)
final int hexScrollPos = (highlightBits.length == 0 || highlightBits[0] == null) ? 0 : (linesize * (highlightBits[0].offset / 128));
hexText.setCaretPosition(hexScrollPos);
}
void connectYamcs(YamcsConnectionProperties ycd) {
disconnect();
connectionParams = ycd;
yconnector = new YamcsConnector("PacketViewer");
yconnector.addConnectionListener(this);
yconnector.connect(ycd);
updateTitle();
}
void disconnect() {
if(yconnector!=null) {
yconnector.disconnect();
}
connectionParams = null;
updateTitle();
}
@Override
public void valueChanged(TreeSelectionEvent e) {
TreePath[] paths = structureTree.getSelectionPaths();
Range[] bits=null;
if(paths==null) {
bits=new Range[0];
} else {
bits = new Range[paths.length];
for (int i = 0; i < paths.length; ++i) {
Object last = paths[i].getLastPathComponent();
if (last instanceof TreeEntry) {
TreeEntry te = (TreeEntry)last;
bits[i] = new Range(te.bitOffset, te.bitSize);
} else {
bits[i] = null;
}
}
}
highlightBitRanges(bits);
}
@Override
public void update(final Collection<ParameterValue> params) {
SwingUtilities.invokeLater(new Runnable() {
Hashtable<String,TreeContainer> containers = new Hashtable<String,TreeContainer>();
DefaultMutableTreeNode getTreeNode(SequenceContainer sc) {
if (sc.getBaseContainer() == null) {
return structureRoot;
}
TreeContainer tc = containers.get(sc.getOpsName());
if (tc == null) {
tc = new TreeContainer(sc);
containers.put(sc.getOpsName(), tc);
}
getTreeNode(sc.getBaseContainer()).add(tc);
return tc;
}
@Override
public void run() {
Object[] vec = new Object[parametersTable.getColumnCount()];
DataEncoding encoding;
Calibrator calib;
Object paramtype;
String name;
parametersTable.clear();
structureRoot.removeAllChildren();
for (ParameterValue value:params) {
// add new leaf to the structure tree
// parameters become leaves, and sequence containers become nodes recursively
name = value.getParameter().getOpsName();
getTreeNode(value.getParameterEntry().getSequenceContainer()).add(new TreeEntry(value));
// add new row for parameter table
vec[0] = value.getParameter();
vec[1] = value.getEngValue().toString();
vec[2] = value.getRawValue().toString();
vec[3] = value.getWarningRange() == null ? "" : Double.toString(value.getWarningRange().getMinInclusive());
vec[4] = value.getWarningRange() == null ? "" : Double.toString(value.getWarningRange().getMaxInclusive());;
vec[5] = value.getCriticalRange() == null ? "" : Double.toString(value.getCriticalRange().getMinInclusive());
vec[6] = value.getCriticalRange() == null ? "" : Double.toString(value.getCriticalRange().getMaxInclusive());
vec[7] = String.valueOf(value.getAbsoluteBitOffset());
vec[8] = String.valueOf(value.getBitSize());
paramtype = value.getParameter().getParameterType();
if (paramtype instanceof EnumeratedParameterType) {
vec[9] = paramtype;
} else if (paramtype instanceof BaseDataType) {
encoding = ((BaseDataType)paramtype).getEncoding();
calib = null;
if (encoding instanceof IntegerDataEncoding) {
calib = ((IntegerDataEncoding) encoding).getDefaultCalibrator();
} else if (encoding instanceof FloatDataEncoding) {
calib = ((FloatDataEncoding) encoding).getDefaultCalibrator();
}
vec[9] = calib == null ? "" : calib.toString();
}
parametersTable.addRow(vec);
}
structureRoot.setUserObject(currentPacket);
structureModel.nodeStructureChanged(structureRoot);
structureTree.setRootVisible(true);
// expand all nodes
for (TreeContainer tc:containers.values()) {
structureTree.expandPath(new TreePath(tc.getPath()));
}
// build hexdump text
currentPacket.hexdump(hexDoc);
hexText.setCaretPosition(0);
// select first row
parametersTable.setRowSelectionInterval(0, 0);
}
});
}
public void setSelectedPacket(ListPacket listPacket) {
currentPacket = listPacket;
try {
currentPacket.load(lastFile);
byte[] b = currentPacket.getBuffer();
tmProcessor.processPacket(new PacketWithTime(TimeEncoding.currentInstant(), currentPacket.getGenerationTime(), b));
} catch (IOException x) {
final String msg = String.format("Error while loading %s: %s", lastFile.getName(), x.getMessage());
log(msg);
showError(msg);
}
}
class TreeContainer extends DefaultMutableTreeNode {
TreeContainer(SequenceContainer sc) {
super(sc.getOpsName(), true);
}
}
class TreeEntry extends DefaultMutableTreeNode {
int bitOffset, bitSize;
TreeEntry(ParameterValue value) {
super(String.format("%d/%d %s", value.getAbsoluteBitOffset(), value.getBitSize(), value.getParameter().getOpsName()), false);
bitOffset = value.getAbsoluteBitOffset();
bitSize = value.getBitSize();
}
}
protected class Range {
int offset, size;
Range(int offset, int size) {
this.offset = offset;
this.size = size;
}
}
@Override
public void connected(String url) {
connectionParams = yconnector.getConnectionParams();
try {
log("connected to "+url);
if(connectDialog!=null) {
if(connectDialog.getUseServerMdb()) {
if(!loadRemoteXtcedb(connectDialog.getServerMdbConfig())) {
return;
}
} else {
if(!loadLocalXtcedb(connectDialog.getLocalMdbConfig())) {
return;
}
}
} else {
RestClient restclient = new RestClient(connectionParams);
List<YamcsInstance> list = restclient.blockingGetYamcsInstances();
for(YamcsInstance yi:list) {
if(connectionParams.getInstance().equals(yi.getName())) {
String mdbConfig = yi.getMissionDatabase().getConfigName();
if(!loadRemoteXtcedb(mdbConfig)) {
return;
}
}
}
}
WebSocketRequest wsr = new WebSocketRequest(PacketResource.RESOURCE_NAME, CommandQueueResource.OP_subscribe+" "+streamName);
yconnector.performSubscription(wsr,
data -> {
if(data.hasTmPacket()) {
TmPacketData tm = data.getTmPacket();
packetsTable.packetReceived(tm);
}
}, e-> {
showError("Error subscribing to "+streamName+": "+e.getMessage());
});
} catch(Exception e) {
log(e.toString());
e.printStackTrace();
}
}
@Override
public void connecting(String url) {
log("connecting to "+url);
}
@Override
public void connectionFailed(String url, YamcsException exception) {
log("connection to "+url+" failed: "+exception);
}
@Override
public void disconnected() {
log("disconnected");
}
/**
* Returns the recently opened files from preferences
* Each entry is a String array with the filename on
* index 0, and the last used XTCE DB for that file on
* index 1.
*/
@SuppressWarnings("unchecked")
public List<String[]> getRecentFiles() {
List<String[]> recentFiles = null;
Object obj = PrefsObject.getObject(uiPrefs, "RecentlyOpened");
if(obj instanceof ArrayList)
recentFiles = (ArrayList<String[]>)obj;
return (recentFiles != null) ? recentFiles : new ArrayList<String[]>();
}
private void updateRecentFiles(File file, String xtceDb) {
String filename = file.getAbsolutePath();
List<String[]> recentFiles = getRecentFiles();
boolean exists = false;
for (int i = 0; i < recentFiles.size(); i++) {
String[] entry = recentFiles.get(i);
if (entry[0].equals(filename)) {
entry[1] = xtceDb;
recentFiles.add(0, recentFiles.remove(i));
exists = true;
}
}
if (!exists) {
recentFiles.add(0, new String[] { filename, xtceDb });
}
PrefsObject.putObject(uiPrefs, "RecentlyOpened", recentFiles);
// Also update JMenu accordingly
updateMenuWithRecentFiles();
}
private void removeBorders(JSplitPane splitPane) {
SplitPaneUI ui = splitPane.getUI();
if(ui instanceof BasicSplitPaneUI) { // We don't want to mess with other L&Fs
((BasicSplitPaneUI)ui).getDivider().setBorder(null);
splitPane.setBorder(BorderFactory.createEmptyBorder());
}
}
private static void printUsageAndExit(boolean full) {
System.err.println("usage: packetviewer.sh [-h] [-l n] [-x name] [-s name] [file|url]");
if (full) {
System.err.println();
System.err.println(" file The file to open at startup. Requires the use of -db");
System.err.println(" url Connect at startup to the given url");
System.err.println();
System.err.println("OPTIONS");
System.err.println(" -h Print a help message and exit");
System.err.println();
System.err.println(" -l n Limit the view to n packets only. If the Packet Viewer is");
System.err.println(" connected to a live instance, only the last n packets will");
System.err.println(" be visible. For offline file consulting, only the first n");
System.err.println(" packets of the file will be displayed.");
System.err.println(" Defaults to 1000 for realtime connections. There is no");
System.err.println(" default limitation for viewing offline files.");
System.err.println();
System.err.println(" -x name Name of the applicable XTCE DB as specified in the");
System.err.println(" mdb.yaml configuration file.");
System.err.println();
System.err.println(" -s name Name of the stream to connect to (if not specified, it connects to tm_realtime");
System.err.println("EXAMPLES");
System.err.println(" packetviewer.sh http://localhost:8090/yops");
System.err.println(" packetviewer.sh -l 50 -x my-db packet-file");
}
System.exit(1);
}
private static void printArgsError(String message) {
System.err.println(message);
printUsageAndExit(false);
}
private void setStreamName(String streamName) {
this.streamName = streamName;
}
public static void main(String[] args) throws ConfigurationException, URISyntaxException {
// Scan args
String fileOrUrl = null;
Map<String,String> options = new HashMap<String,String>();
for (int i = 0; i < args.length; i++) {
if ("-h".equals(args[i])) {
printUsageAndExit(true);
} else if ("-l".equals(args[i])) {
if (i+1 < args.length) {
options.put(args[i], args[++i]);
} else {
printArgsError("Number of lines not specified for -l option");
}
} else if ("-x".equals(args[i])) {
if (i+1 < args.length) {
options.put(args[i], args[++i]);
} else {
printArgsError("Name of XTCE DB not specified for -x option");
}
} else if ("-s".equals(args[i])) {
if (i+1 < args.length) {
options.put(args[i], args[++i]);
} else {
printArgsError("Name of stream not specified for -s option");
}
} else if (args[i].startsWith("-")) {
printArgsError("Unknown option: " + args[i]);
} else { // i should now be positioned at [file|url]
if (i == args.length - 1) {
fileOrUrl = args[i];
} else {
printArgsError("Too many arguments. Only one file or url can be opened at a time");
}
}
}
// Do some more preparatory stuff
if (options.containsKey("-l")) {
try {
maxLines = Integer.parseInt((String) options.get("-l"));
} catch (NumberFormatException e) {
printArgsError("-l argument must be integer. Got: " + options.get("-l"));
}
}
if (fileOrUrl != null && fileOrUrl.startsWith("http://")) {
if (!options.containsKey("-l")) {
maxLines = 1000; // Default for realtime connections
}
}
if (fileOrUrl != null && !fileOrUrl.startsWith("http://")) {
if (!options.containsKey("-x")) {
printArgsError("-x argument must be specified when opening a file");
}
}
if (fileOrUrl != null && !fileOrUrl.startsWith("http://")) {
if (options.containsKey("-s")) {
printArgsError("-s argument only valid for yamcs connections");
}
}
// Okay, launch the GUI now
YConfiguration.setup();
theApp = new PacketViewer(maxLines);
if (fileOrUrl != null) {
if (fileOrUrl.startsWith("http://")) {
YamcsConnectionProperties ycd = YamcsConnectionProperties.parse(fileOrUrl);
String streamName =options.get("-s");
if(streamName==null) {
streamName = "tm_realtime";
}
theApp.setStreamName(streamName);
theApp.connectYamcs(ycd);
} else {
File file = new File(fileOrUrl);
theApp.openFile(file, (String) options.get("-x"));
}
}
}
public void addParameterToTheLeftTable(Parameter selectedParameter) {
packetsTable.addParameterColumn(selectedParameter);
}
public String getDefaultNamespace() {
return defaultNamespace;
}
}