diff --git a/examples/sky-shell/build.xml b/examples/sky-shell/build.xml new file mode 100644 index 000000000..27c83ab88 --- /dev/null +++ b/examples/sky-shell/build.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/sky-shell/collect-init.script b/examples/sky-shell/collect-init.script new file mode 100644 index 000000000..528dcd278 --- /dev/null +++ b/examples/sky-shell/collect-init.script @@ -0,0 +1,10 @@ +echo ~K +echo killall +sleep 2 +echo time %TIME% | null +sleep 2 +echo netcmd { repeat 0 20 { randwait 20 sky-alldata | blink | send } } +sleep 2 +echo mac 0 +sleep 2 +echo collect | timestamp | blink | binprint & diff --git a/examples/sky-shell/lib/jcommon-1.0.13.jar b/examples/sky-shell/lib/jcommon-1.0.13.jar new file mode 100644 index 000000000..634447d2f Binary files /dev/null and b/examples/sky-shell/lib/jcommon-1.0.13.jar differ diff --git a/examples/sky-shell/lib/jfreechart-1.0.10.jar b/examples/sky-shell/lib/jfreechart-1.0.10.jar new file mode 100644 index 000000000..f6f7bbf2d Binary files /dev/null and b/examples/sky-shell/lib/jfreechart-1.0.10.jar differ diff --git a/examples/sky-shell/src/se/sics/contiki/collect/CollectServer.java b/examples/sky-shell/src/se/sics/contiki/collect/CollectServer.java new file mode 100644 index 000000000..b90288029 --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/CollectServer.java @@ -0,0 +1,875 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: CollectServer.java,v 1.1 2008/07/09 23:18:06 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * CollectServer + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 3 jul 2008 + * Updated : $Date: 2008/07/09 23:18:06 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect; +import java.awt.BorderLayout; +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Hashtable; +import java.util.Properties; +import javax.swing.BorderFactory; +import javax.swing.DefaultListModel; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JList; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTabbedPane; +import javax.swing.SwingUtilities; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.axis.ValueAxis; +import se.sics.contiki.collect.gui.BarChartPanel; +import se.sics.contiki.collect.gui.MapPanel; +import se.sics.contiki.collect.gui.SerialConsole; +import se.sics.contiki.collect.gui.TimeChartPanel; + +/** + * + */ +public class CollectServer { + + public static final String WINDOW_TITLE = "Sensor Data Collect with Contiki"; + public static final double TICKS_PER_SECOND = 4096; /* TODO Convert from TimerB ticks to seconds */ + public static final double UPDATE_PERIOD = 1; /* TODO Set update period (1 second?) */ + + public static final String CONFIG_FILE = "collect.conf"; + public static final String SENSORDATA_FILE = "sensordata.log"; + public static final String CONFIG_DATA_FILE = "collect-data.conf"; + public static final String INIT_SCRIPT = "collect-init.script"; + + private Properties config = new Properties(); + + private String configFile; + private Properties configTable = new Properties(); + + private ArrayList sensorDataList = new ArrayList(); + private PrintWriter sensorDataOutput; + + private Hashtable nodeTable = new Hashtable(); + private Node[] nodeCache; + + private JFrame window; + private JTabbedPane mainPanel; + + private Visualizer[] visualizers; + private MapPanel mapPanel; + private SerialConsole serialConsole; + private JFileChooser fileChooser; + + private JList nodeList; + private DefaultListModel nodeModel; + private Node[] selectedNodes; + + private SerialConnection serialConnection; + + @SuppressWarnings("serial") + public CollectServer(String comPort) { + loadConfig(config, CONFIG_FILE); + + this.configFile = config.getProperty("config.datafile", CONFIG_DATA_FILE); + if (this.configFile != null) { + loadConfig(configTable, this.configFile); + } + if (comPort == null) { + comPort = configTable.getProperty("collect.serialport"); + } + + /* Make sure we have nice window decorations */ +// JFrame.setDefaultLookAndFeelDecorated(true); +// JDialog.setDefaultLookAndFeelDecorated(true); + Rectangle maxSize = GraphicsEnvironment.getLocalGraphicsEnvironment() + .getMaximumWindowBounds(); + + /* Create and set up the window */ + window = new JFrame(WINDOW_TITLE + " (not connected)"); + window.setLocationByPlatform(true); + if (maxSize != null) { + window.setMaximizedBounds(maxSize); + } + window.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + + window.addWindowListener(new WindowAdapter() { + + public void windowClosing(WindowEvent e) { + exit(); + } + }); + + nodeModel = new DefaultListModel(); + nodeList = new JList(nodeModel); + nodeList.setPrototypeCellValue("Node 88888"); +// DefaultListCellRenderer l = new DefaultListCellRenderer(); +// l.setHorizontalAlignment(DefaultListCellRenderer.RIGHT); +// nodeList.setCellRenderer(l); + nodeList.addListSelectionListener(new ListSelectionListener() { + + @Override + public void valueChanged(ListSelectionEvent e) { + if (!e.getValueIsAdjusting() && e.getSource() == nodeList) { + Node[] selected; + int iMin = nodeList.getMinSelectionIndex(); + int iMax = nodeList.getMaxSelectionIndex(); + if ((iMin < 0) || (iMax < 0)) { + selected = null; + } else { + Node[] tmp = new Node[1 + (iMax - iMin)]; + int n = 0; + for(int i = iMin; i <= iMax; i++) { + if (nodeList.isSelectedIndex(i)) { + tmp[n++] = (Node) nodeModel.getElementAt(i); + } + } + if (n != tmp.length) { + Node[] t = new Node[n]; + System.arraycopy(tmp, 0, t, 0, n); + tmp = t; + } + selected = tmp; + } + selectNodes(selected, false); + } + + }}); + nodeList.setBorder(BorderFactory.createTitledBorder("Nodes")); + window.getContentPane().add(new JScrollPane(nodeList), BorderLayout.WEST); + + mainPanel = new JTabbedPane(); + mainPanel.setBackground(nodeList.getBackground()); + mainPanel.setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT); + + serialConsole = new SerialConsole(this); + mapPanel = new MapPanel(this); + String image = getConfig("collect.mapimage"); + if (image != null) { + mapPanel.setMapBackground(image); + } + visualizers = new Visualizer[] { + mapPanel, + new BarChartPanel(this, "Instantaneous Power", "Instantaneous Power Consumption", null, "Power (mW)", + new String[] { "LPM", "CPU", "Radio listen", "Radio transmit" }) { + protected void addSensorData(SensorData data) { + Node node = data.getNode(); + String nodeName = node.getName(); + dataset.addValue(data.getLPMPower(), categories[0], nodeName); + dataset.addValue(data.getCPUPower(), categories[1], nodeName); + dataset.addValue(data.getListenPower(), categories[2], nodeName); + dataset.addValue(data.getTransmitPower(), categories[3], nodeName); + ValueAxis axis = chart.getCategoryPlot().getRangeAxis(); + axis.setLowerBound(0.0); + axis.setUpperBound(75.0); + } + }, + new TimeChartPanel(this, "Power History", "Historical Power Consumption", "Time", "mW") { + protected double getSensorDataValue(SensorData data) { + return data.getAveragePower(); + } + }, + new TimeChartPanel(this, "Temperature", "Temperature", "Time", "Celsius") { + { + chart.getXYPlot().getRangeAxis().setStandardTickUnits(NumberAxis.createIntegerTickUnits()); + setRangeTick(5); + setRangeMinimumSize(10.0); + setGlobalRange(true); + } + protected double getSensorDataValue(SensorData data) { + return data.getTemperature(); + } + }, + new TimeChartPanel(this, "Relative Humidity", "Humidity", "Time", "%") { + { + chart.getXYPlot().getRangeAxis().setRange(0.0, 100.0); + } + protected double getSensorDataValue(SensorData data) { + return data.getHumidity(); + } + }, + new TimeChartPanel(this, "Light 1", "Light 1", "Time", "-") { + protected double getSensorDataValue(SensorData data) { + return data.getLight1(); + } + }, + new TimeChartPanel(this, "Light 2", "Light 2", "Time", "-") { + protected double getSensorDataValue(SensorData data) { + return data.getLight2(); + } + }, + new TimeChartPanel(this, "Network Hops", "Network Hops", "Time", "Hops") { + { + ValueAxis axis = chart.getXYPlot().getRangeAxis(); + axis.setLowerBound(0.0); + axis.setUpperBound(4.0); + axis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); + } + protected double getSensorDataValue(SensorData data) { + return data.getValue(SensorData.HOPS); + } + }, + new BarChartPanel(this, "Network Hops", "Network Hops", null, "Hops", + new String[] { "Hops" }) { + { + chart.getCategoryPlot().getRangeAxis().setStandardTickUnits(NumberAxis.createIntegerTickUnits()); + } + protected void addSensorData(SensorData data) { + dataset.addValue(data.getValue(SensorData.HOPS), categories[0], data.getNode().getName()); + } + }, + new TimeChartPanel(this, "Latency", "Latency", "Time", "Milliseconds") { + protected double getSensorDataValue(SensorData data) { + return data.getLatency(); + } + }, + serialConsole + }; + for (int i = 0, n = visualizers.length; i < n; i++) { + mainPanel.add(visualizers[i].getTitle(), visualizers[i].getPanel()); + } + window.getContentPane().add(mainPanel, BorderLayout.CENTER); + + // Setup menu + JMenuBar menuBar = new JMenuBar(); + + JMenu fileMenu = new JMenu("File"); + fileMenu.setMnemonic(KeyEvent.VK_F); + menuBar.add(fileMenu); + + final JMenuItem clearMapItem = new JMenuItem("Remove Map Background"); + clearMapItem.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + mapPanel.setMapBackground(null); + clearMapItem.setEnabled(false); + configTable.remove("collect.mapimage"); + } + + }); + clearMapItem.setEnabled(mapPanel.getMapBackground() != null); + + JMenuItem item = new JMenuItem("Select Map Background..."); + item.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + if (fileChooser == null) { + fileChooser = new JFileChooser(); + int reply = fileChooser.showOpenDialog(window); + if (reply == JFileChooser.APPROVE_OPTION) { + File file = fileChooser.getSelectedFile(); + String name = file.getAbsolutePath(); + configTable.put("collect.mapimage", name); + if (!mapPanel.setMapBackground(file.getAbsolutePath())) { + JOptionPane.showMessageDialog(window, "Failed to set background image", "Error", JOptionPane.ERROR_MESSAGE); + } + clearMapItem.setEnabled(mapPanel.getMapBackground() != null); + saveConfig(configTable, configFile); + } + } + } + + }); + fileMenu.add(item); + fileMenu.add(clearMapItem); + + fileMenu.addSeparator(); + item = new JMenuItem("Clear Sensor Data..."); + item.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + int reply = JOptionPane.showConfirmDialog(window, "Also clear the sensor data log file?"); + if (reply == JOptionPane.YES_OPTION) { + // Clear data from both memory and sensor log file + clearSensorDataLog(); + clearSensorData(); + } else if (reply == JOptionPane.NO_OPTION) { + // Only clear data from memory + clearSensorData(); + } + } + + }); + fileMenu.add(item); + + fileMenu.addSeparator(); + item = new JMenuItem("Exit", KeyEvent.VK_X); + item.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + exit(); + } + + }); + fileMenu.add(item); + + JMenu toolsMenu = new JMenu("Tools"); + toolsMenu.setMnemonic(KeyEvent.VK_T); + menuBar.add(toolsMenu); + + final JCheckBoxMenuItem scrollItem = new JCheckBoxMenuItem("Scroll Layout"); + scrollItem.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + if (scrollItem.getState()) { + mainPanel.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); + } else { + mainPanel.setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT); + } + } + + }); + toolsMenu.add(scrollItem); + + window.setJMenuBar(menuBar); + window.pack(); + + String bounds = configTable.getProperty("collect.bounds"); + if (bounds != null) { + String[] b = bounds.split(","); + if (b.length == 4) { + window.setBounds(Integer.parseInt(b[0]), Integer.parseInt(b[1]), + Integer.parseInt(b[2]), Integer.parseInt(b[3])); + } + } + + for(Object key: configTable.keySet()) { + String property = key.toString(); + if (!property.startsWith("collect")) { + getNode(property, true); + } + } + initSensorData(); + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + window.setVisible(true); + } + }); + + if (comPort == null) { + comPort = MoteFinder.selectComPort(window); +// if (comPort == null) { +// exit(); +// } + } + serialConnection = new SerialConnection() { + + private boolean hasOpened; + private boolean hasSentInit; + + @Override + protected void serialOpened() { + serialConsole.addSerialData("*** Serial console listening on port: " + getComPort() + " ***"); + hasOpened = true; + // Remember the last selected serial port + configTable.put("collect.serialport", getComPort()); + setSystemMessage("connected to " + getComPort()); + + // Send any initial commands + if (!hasSentInit) { + hasSentInit = true; + + String initScript = config.getProperty("init.script", INIT_SCRIPT); + if (initScript != null) { + // Wait a short time before running the init script + sleep(3000); + runScript(initScript); + } + } + } + + @Override + protected void serialClosed() { + String comPort = getComPort(); + String prefix; + if (hasOpened) { + serialConsole.addSerialData("*** Serial connection terminated ***"); + prefix = "Serial connection terminated.\n"; + hasOpened = false; + setSystemMessage("not connected"); + } else { + prefix = "Failed to connect to " + getComPort() + '\n'; + } + String options[] = {"Retry", "Search for connected nodes", "Cancel"}; + int value = JOptionPane.showOptionDialog(window, + prefix + "Do you want to retry or search for connected nodes?", + "Reconnect to serial port?", + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, + null, options, options[0]); + if (value == JOptionPane.CLOSED_OPTION || value == 2) { +// exit(); + } else { + if (value == 1) { + // Select new serial port + comPort = MoteFinder.selectComPort(window); + if (comPort == null) { +// exit(); + } + } + // Try to open com port again + if (comPort != null) { + open(comPort); + } + } + } + + @Override + protected void serialData(String line) { + parseIncomingLine(line); + } + + }; + serialConnection.open(comPort); + } + + private void exit() { + /* TODO Clean up resources */ + if (configFile != null) { + configTable.setProperty("collect.bounds", "" + window.getX() + ',' + window.getY() + ',' + window.getWidth() + ',' + window.getHeight()); + saveConfig(configTable, configFile); + } + if (serialConnection != null) { + serialConnection.close(); + } + System.exit(0); + } + + private void sleep(long delay) { + try { + Thread.sleep(delay); + } catch (InterruptedException e1) { + // Ignore + } + } + + private boolean runScript(String script) { + try { + BufferedReader in = new BufferedReader(new FileReader(script)); + String line; + while ((line = in.readLine()) != null) { + if (line.length() == 0 || line.charAt(0) == '#') { + // Ignore empty lines and comments + } else if (line.startsWith("echo ")) { + line = line.substring(5).trim(); + if (line.indexOf('%') >= 0) { + line = line.replace("%TIME%", "" + (System.currentTimeMillis() / 1000)); + } + sendToNode(line); + } else if (line.startsWith("sleep ")) { + long delay = Integer.parseInt(line.substring(6).trim()); + Thread.sleep(delay * 1000); + } else { + System.err.println("Unknown script comand: " + line); + return false; + } + } + in.close(); + return true; + } catch (FileNotFoundException e) { + return false; + } catch (Exception e) { + System.err.println("Failed to run script: " + script); + e.printStackTrace(); + return false; + } + } + + public String getConfig(String property) { + return getConfig(property, null); + } + + public String getConfig(String property, String defaultValue) { + return configTable.getProperty(property, config.getProperty(property, defaultValue)); + } + + protected void setSystemMessage(final String message) { + SwingUtilities.invokeLater(new Runnable() { + + public void run() { + if (message == null) { + window.setTitle(WINDOW_TITLE); + } else { + window.setTitle(WINDOW_TITLE + " (" + message + ')'); + } + } + }); + } + + // ------------------------------------------------------------------- + // Node Handling + // ------------------------------------------------------------------- + + public synchronized Node[] getNodes() { + if (nodeCache == null) { + Node[] tmp = nodeTable.values().toArray(new Node[nodeTable.size()]); + Arrays.sort(tmp); + nodeCache = tmp; + } + return nodeCache; + } + + public Node addNode(String nodeID) { + return getNode(nodeID, true); + } + + private Node getNode(final String nodeID, boolean notify) { + Node node = nodeTable.get(nodeID); + if (node == null) { + node = new Node(nodeID); + nodeTable.put(nodeID, node); + updateNodeLocation(node); + + synchronized (this) { + nodeCache = null; + } + + if (notify) { + final Node newNode = node; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + // Insert the node sorted by name + String nodeName = newNode.getName(); + boolean added = false; + for (int i = 0, n = nodeModel.size(); i < n; i++) { + int cmp = nodeName.compareTo(((Node) nodeModel.get(i)).getName()); + if (cmp < 0) { + nodeModel.insertElementAt(newNode, i); + added = true; + break; + } else if (cmp == 0) { + // node already added + added = true; + break; + } + } + if (!added) { + nodeModel.addElement(newNode); + } + if (visualizers != null) { + for (int i = 0, n = visualizers.length; i < n; i++) { + visualizers[i].nodeAdded(newNode); + } + } + } + }); + } + } + return node; + } + + public void selectNodes(Node[] nodes) { + selectNodes(nodes, true); + } + + private void selectNodes(Node[] nodes, boolean updateList) { + if (nodes != selectedNodes) { + selectedNodes = nodes; + if (updateList) { + nodeList.clearSelection(); + if (selectedNodes != null) { + for (int i = 0, n = selectedNodes.length; i < n; i++) { + int index = nodeModel.indexOf(selectedNodes[i]); + if (index >= 0) { + nodeList.addSelectionInterval(index, index); + } + } + } + } + if (visualizers != null) { + for (int i = 0, n = visualizers.length; i < n; i++) { + visualizers[i].nodesSelected(nodes); + } + } + } + } + + + // ------------------------------------------------------------------- + // Node location handling + // ------------------------------------------------------------------- + + public boolean updateNodeLocation(Node node) { + String id = node.getID(); + if (node.hasLocation()) { + String location = "" + node.getX() + ',' + node.getY(); + if (!location.equals(configTable.get(id))) { + configTable.put(id, location); + } + return false; + } + + String location = configTable.getProperty(id); + if (location != null) { + try { + String[] pos = location.split(","); + node.setLocation(Integer.parseInt(pos[0].trim()), + Integer.parseInt(pos[1].trim())); + return true; + } catch (Exception e) { + System.err.println("could not parse node location: " + location); + e.printStackTrace(); + } + } + return false; + } + + private boolean loadConfig(Properties properties, String configFile) { + try { + BufferedInputStream input = + new BufferedInputStream(new FileInputStream(configFile)); + try { + properties.load(input); + } finally { + input.close(); + } + return true; + } catch (FileNotFoundException e) { + // No configuration file exists. + } catch (IOException e) { + System.err.println("Failed to read configuration file: " + configFile); + e.printStackTrace(); + } + return false; + } + + private void saveConfig(Properties properties, String configFile) { + try { + File fp = new File(configFile); + if (fp.exists()) { + File targetFp = new File(configFile + ".bak"); + if (targetFp.exists()) { + targetFp.delete(); + } + fp.renameTo(targetFp); + } + FileOutputStream output = new FileOutputStream(configFile); + try { + properties.store(output, "Configuration for Collect"); + } finally { + output.close(); + } + } catch (IOException e) { + System.err.println("failed to save configuration to " + configFile); + e.printStackTrace(); + } + } + + + // ------------------------------------------------------------------- + // Serial communication + // ------------------------------------------------------------------- + + public boolean sendToNode(String data) { + if (serialConnection != null && serialConnection.isOpen()) { + serialConsole.addSerialData("SEND: " + data); + serialConnection.writeSerialData(data); + return true; + } + return false; + } + + protected void parseIncomingLine(String line) { + SensorData sensorData = SensorData.parseSensorData(this, line); + if (sensorData != null) { + // Sensor data received + handleSensorData(sensorData); + return; + } + System.out.println("SERIAL: " + line); + serialConsole.addSerialData(line); + } + + + // ------------------------------------------------------------------- + // SensorData handling + // ------------------------------------------------------------------- + + public int getSensorDataCount() { + return sensorDataList.size(); + } + + public SensorData getSensorData(int i) { + return sensorDataList.get(i); + } + + private void handleSensorData(final SensorData sensorData) { + System.out.println("SENSOR DATA: " + sensorData); + if (sensorData.getNode().addSensorData(sensorData)) { + sensorDataList.add(sensorData); + saveSensorData(sensorData); + handleLinks(sensorData); + if (visualizers != null) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + for (int i = 0, n = visualizers.length; i < n; i++) { + visualizers[i].nodeDataReceived(sensorData); + } + } + }); + } + } + } + + private void handleLinks(SensorData sensorData) { + String nodeID = sensorData.getBestNeighborID(); + if (nodeID != null) { + Node neighbor = addNode(nodeID); + Node source = sensorData.getNode(); + Link link = source.getLink(neighbor); + link.setETX(sensorData.getBestNeighborETX()); + link.setLastActive(sensorData.getTime()); + } + } + + private void initSensorData() { + loadSensorData(SENSORDATA_FILE); + } + + private boolean loadSensorData(String filename) { + File fp = new File(filename); + if (fp.exists() && fp.canRead()) { + BufferedReader in = null; + try { + in = new BufferedReader(new FileReader(fp)); + String line; + int no = 0; + while ((line = in.readLine()) != null) { + no++; + if (line.length() == 0 || line.charAt(0) == '#') { + // Ignore empty lines and comments + } else { + SensorData data = SensorData.parseSensorData(this, line); + if (data != null) { + if (data.getNode().addSensorData(data)) { + sensorDataList.add(data); + handleLinks(data); + } + } else { + // TODO exit here? + System.err.println("Failed to parse sensor data from log line " + no + ": " + line); + } + } + } + in.close(); + } catch (IOException e) { + System.err.println("Failed to read sensor data log from " + fp.getAbsolutePath()); + e.printStackTrace(); + return false; + } + } + return true; + } + + private void saveSensorData(SensorData data) { + PrintWriter output = this.sensorDataOutput; + if (output == null) { + try { + output = sensorDataOutput = new PrintWriter(new FileWriter(SENSORDATA_FILE, true)); + } catch (IOException e) { + System.err.println("Failed to add sensor data to log '" + SENSORDATA_FILE + '\''); + e.printStackTrace(); + } + } + if (output != null) { + output.println(data.toString()); + } + } + + private void clearSensorData() { + sensorDataList.clear(); + Node[] nodes = getNodes(); + if (nodes != null) { + for(Node node : nodes) { + node.removeAllSensorData(); + } + } + if (visualizers != null) { + for(Visualizer v : visualizers) { + v.clearNodeData(); + } + } + } + + private void clearSensorDataLog() { + PrintWriter output = this.sensorDataOutput; + if (output != null) { + output.close(); + } + // Remove the sensor data log + new File(SENSORDATA_FILE).delete(); + this.sensorDataOutput = null; + } + + // ------------------------------------------------------------------- + // Main + // ------------------------------------------------------------------- + + public static void main(String[] args) { + String comPort = null; + if (args.length > 0) { + if (args.length > 1 || args[0].startsWith("-h")) { + System.err.println("Usage: java CollectServer COMPORT"); + System.exit(1); + } + comPort = args[0]; + } + new CollectServer(comPort); + } + +} diff --git a/examples/sky-shell/src/se/sics/contiki/collect/Link.java b/examples/sky-shell/src/se/sics/contiki/collect/Link.java new file mode 100644 index 000000000..585fc8be0 --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/Link.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: Link.java,v 1.1 2008/07/09 23:18:06 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * Link + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 3 jul 2008 + * Updated : $Date: 2008/07/09 23:18:06 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect; + +/** + * + */ +public class Link { + + public final Node node; + + private double etx; + private int quality = 100; + private long lastActive = 0L; + + public Link(Node node) { + this.node = node; + this.lastActive = System.currentTimeMillis(); + } + + public Node getNode() { + return node; + } + + public int getQuality() { + return quality; + } + + public void setQuality(int quality) { + this.quality = quality; + } + + public double getETX() { + return etx; + } + + public void setETX(double etx) { + this.etx = etx; + } + + public long getLastActive() { + return lastActive; + } + + public void setLastActive(long time) { + this.lastActive = time; + } + +} diff --git a/examples/sky-shell/src/se/sics/contiki/collect/MoteFinder.java b/examples/sky-shell/src/se/sics/contiki/collect/MoteFinder.java new file mode 100644 index 000000000..ed584166f --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/MoteFinder.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: MoteFinder.java,v 1.1 2008/07/09 23:18:06 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * Motelist + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 4 jul 2008 + * Updated : $Date: 2008/07/09 23:18:06 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect; +import java.awt.Component; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.swing.JOptionPane; + +/** + * + */ +public class MoteFinder { + + public static final String MOTELIST_WINDOWS = "./tools/motelist-windows.exe"; + public static final String MOTELIST_LINUX = "./tools/motelist-linux"; + + private final Pattern motePattern; + private Process moteListProcess; +// private boolean hasVerifiedProcess; + private ArrayList comList = new ArrayList(); + private int[] moteList = new int[10]; + private int moteCount = 0; + + public MoteFinder() { + motePattern = Pattern.compile("\\s(COM|/dev/[a-zA-Z]+)(\\d+)\\s"); + } + + public int[] getMotes() throws IOException { + searchForMotes(); + return getMoteList(); + } + + public String[] getComPorts() throws IOException { + searchForMotes(); + return getComList(); + } + + private void searchForMotes() throws IOException { + comList.clear(); + moteCount = 0; +// hasVerifiedProcess = false; + + /* Connect to COM using external serialdump application */ + String osName = System.getProperty("os.name").toLowerCase(); + String fullCommand; + if (osName.startsWith("win")) { + fullCommand = MOTELIST_WINDOWS; + } else { + fullCommand = MOTELIST_LINUX; + } + + try { + String[] cmd = new String[] { fullCommand }; + moteListProcess = Runtime.getRuntime().exec(cmd); + final BufferedReader input = new BufferedReader(new InputStreamReader(moteListProcess.getInputStream())); + final BufferedReader err = new BufferedReader(new InputStreamReader(moteListProcess.getErrorStream())); + + /* Start thread listening on stdout */ + Thread readInput = new Thread(new Runnable() { + public void run() { + String line; + try { + while ((line = input.readLine()) != null) { + parseIncomingLine(line); + } + input.close(); + } catch (IOException e) { + System.err.println("Exception when reading from motelist"); + e.printStackTrace(); + } + } + }, "read motelist thread"); + + /* Start thread listening on stderr */ + Thread readError = new Thread(new Runnable() { + public void run() { + String line; + try { + while ((line = err.readLine()) != null) { + System.err.println("Motelist error stream> " + line); + } + err.close(); + } catch (IOException e) { + System.err.println("Exception when reading from motelist"); + e.printStackTrace(); + } + } + }, "read motelist error stream thread"); + + readInput.start(); + readError.start(); + + // Wait for the motelist program to finish executing + readInput.join(); + } catch (Exception e) { + throw (IOException) new IOException("Failed to execute '" + fullCommand + "'").initCause(e); + } + } + + private String[] getComList() { + return comList.toArray(new String[comList.size()]); + } + + private int[] getMoteList() { + if (moteCount < moteList.length) { + int[] tmp = new int[moteCount]; + System.arraycopy(moteList, 0, tmp, 0, tmp.length); + moteList = tmp; + } + return moteList; + } + + public void close() { + if (moteListProcess != null) { + moteListProcess.destroy(); + moteListProcess = null; + } + } + + protected void parseIncomingLine(String line) { + if (line.contains("No devices found") || line.startsWith("Reference")) { + // No Sky connected or title before connected motes +// hasVerifiedProcess = true; + } else if (line.startsWith("-------")) { + // Separator + } else { + Matcher matcher = motePattern.matcher(line); + if (matcher.find()) { + if (moteCount == moteList.length) { + int[] tmp = new int[moteCount + 10]; + System.arraycopy(moteList, 0, tmp, 0, moteCount); + moteList = tmp; + } + comList.add(matcher.group(1) + matcher.group(2)); + moteList[moteCount++] = Integer.parseInt(matcher.group(2)); + } else { + System.err.println("Motelist> " + line); + } + } + } + + public static String selectComPort(Component parent) { + MoteFinder finder = new MoteFinder(); + try { + String[] motes = finder.getComPorts(); + if (motes == null || motes.length == 0) { + JOptionPane.showMessageDialog(parent, "Could not find any connected motes.", "No mote found", JOptionPane.ERROR_MESSAGE); + return null; + } else if (motes.length == 1) { + // Only one node found + return motes[0]; + } else { + // Several motes found + return (String) JOptionPane.showInputDialog( + parent, "Found multiple connected motes. Please select serial port:", + "Select serial port", JOptionPane.QUESTION_MESSAGE, null, motes, motes[0]); + } + } catch (IOException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(parent, "Failed to search for connected motes:\n" + e, "Error", JOptionPane.ERROR_MESSAGE); + return null; + } finally { + finder.close(); + } + } + + public static void main(String[] args) throws IOException { + MoteFinder finder = new MoteFinder(); + String[] motes = finder.getComPorts(); + finder.close(); + if (motes == null || motes.length == 0) { + System.out.println("No motes connected"); + } else { + for(String port: motes) { + System.out.println("Found Sky at " + port); + } + } + } + +} diff --git a/examples/sky-shell/src/se/sics/contiki/collect/Node.java b/examples/sky-shell/src/se/sics/contiki/collect/Node.java new file mode 100644 index 000000000..34254b112 --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/Node.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: Node.java,v 1.1 2008/07/09 23:18:06 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * Node + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 3 jul 2008 + * Updated : $Date: 2008/07/09 23:18:06 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect; +import java.util.ArrayList; +import java.util.Hashtable; + +/** + * + */ +public class Node implements Comparable { + + private ArrayList sensorDataList = new ArrayList(); + private ArrayList links = new ArrayList(); + + private final String id; + private final String name; + + private int x = -1, y = -1; + + private Hashtable objectTable; + + private long lastActive; + + public Node(String nodeID) { + this.id = nodeID; + this.name = "Node " + nodeID; + } + + public final String getID() { + return id; + } + + public final String getName() { + return name; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public void setLocation(int x, int y) { + this.x = x; + this.y = y; + } + + public boolean hasLocation() { + return x >= 0 && y >= 0; + } + + public long getLastActive() { + return lastActive; + } + + public void setLastActive(long lastActive) { + this.lastActive = lastActive; + } + + @Override + public int compareTo(Node o) { + return name.compareTo(o.name); + } + + public String toString() { + return name; + } + + + // ------------------------------------------------------------------- + // Attributes + // ------------------------------------------------------------------- + + public Object getAttribute(String key) { + return getAttribute(key, null); + } + + public Object getAttribute(String key, Object defaultValue) { + if (objectTable == null) { + return null; + } + Object val = objectTable.get(key); + return val == null ? defaultValue : val; + } + + public void setAttribute(String key, Object value) { + if (objectTable == null) { + objectTable = new Hashtable(); + } + objectTable.put(key, value); + } + + public void clearAttributes() { + if (objectTable != null) { + objectTable.clear(); + } + } + + + // ------------------------------------------------------------------- + // SensorData + // ------------------------------------------------------------------- + + public SensorData[] getAllSensorData() { + return sensorDataList.toArray(new SensorData[sensorDataList.size()]); + } + + + public void removeAllSensorData() { + sensorDataList.clear(); + } + + public SensorData getSensorData(int index) { + return sensorDataList.get(index); + } + + public int getSensorDataCount() { + return sensorDataList.size(); + } + + public boolean addSensorData(SensorData data) { + if (sensorDataList.size() > 0) { + SensorData last = sensorDataList.get(sensorDataList.size() - 1); + // TODO should check seqno! + if (data.getTime() <= last.getTime()) { + // Sensor data already added + System.out.println("SensorData: ignoring (time " + (data.getTime() - last.getTime()) + + "msec): " + data); + return false; + } + } + sensorDataList.add(data); + return true; + } + + + // ------------------------------------------------------------------- + // Links + // ------------------------------------------------------------------- + + public Link getLink(Node node) { + for(Link l: links) { + if (l.node == node) { + return l; + } + } + + // Add new link + Link l = new Link(node); + links.add(l); + return l; + } + + public Link getLink(int index) { + return links.get(index); + } + + public int getLinkCount() { + return links.size(); + } + + public void removeLink(Node node) { + for (int i = 0, n = links.size(); i < n; i++) { + Link l = links.get(i); + if (l.node == node) { + links.remove(i); + break; + } + } + } + + public void clearLinks() { + links.clear(); + } + +} diff --git a/examples/sky-shell/src/se/sics/contiki/collect/SensorData.java b/examples/sky-shell/src/se/sics/contiki/collect/SensorData.java new file mode 100644 index 000000000..6de2ff294 --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/SensorData.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: SensorData.java,v 1.1 2008/07/09 23:18:06 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * SensorData + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 3 jul 2008 + * Updated : $Date: 2008/07/09 23:18:06 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect; + + +/** + * + */ +public class SensorData { + + public static final int TICKS_PER_SECOND = 4096; + private static final double VOLTAGE = 3; + private static final double POWER_CPU = 1.800 * VOLTAGE; /* mW */ + private static final double POWER_LPM = 0.0545 * VOLTAGE; /* mW */ + private static final double POWER_TRANSMIT = 17.7 * VOLTAGE; /* mW */ + private static final double POWER_LISTEN = 20.0 * VOLTAGE; /* mW */ + + public static final int DATA_LEN = 0; + public static final int TIMESTAMP1 = 1; + public static final int TIMESTAMP2 = 2; + public static final int TIMESYNCTIMESTAMP = 3; + public static final int NODE_ID = 4; + public static final int SEQNO = 5; + public static final int HOPS = 6; + public static final int LATENCY = 7; + public static final int DATA_LEN2 = 8; + public static final int CLOCK = 9; + public static final int TIMESYNCHTIME = 10; + public static final int LIGHT1 = 11; + public static final int LIGHT2 = 12; + public static final int TEMPERATURE = 13; + public static final int HUMIDITY = 14; + public static final int RSSI = 15; + public static final int TIME_CPU = 16; + public static final int TIME_LPM = 17; + public static final int TIME_TRANSMIT = 18; + public static final int TIME_LISTEN = 19; + public static final int BEST_NEIGHBOR = 20; + public static final int BEST_NEIGHBOR_ETX = 21; + public static final int BEST_NEIGHBOR_RTMETRIC = 22; + + public static final int VALUES_COUNT = 23; + + private final Node node; + private final int[] values; + private final long time; + + public SensorData(Node node, int[] values) { + this.node = node; + this.values = values; + this.time = ((values[TIMESTAMP1] << 16) + values[TIMESTAMP2]) * 1000; + } + + public Node getNode() { + return node; + } + + public String getNodeID() { + return node.getID(); + } + + public int getValue(int index) { + return values[index]; + } + + public long getTime() { + return time; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0, n = values.length; i < n; i++) { + if (i > 0) sb.append(' '); + sb.append(values[i]); + } + return sb.toString(); + } + + public static SensorData parseSensorData(CollectServer server, String line) { + String[] components = line.split(" "); + if (components.length != SensorData.VALUES_COUNT) { + return null; + } + // Sensor data line (probably) + int[] data = parseToInt(components); + if (data == null || data[0] != VALUES_COUNT) { + System.err.println("Failed to parse data line: '" + line + "'"); + return null; + } + String nodeID = mapNodeID(data[NODE_ID]); + Node node = server.addNode(nodeID); + return new SensorData(node, data); + } + + public static String mapNodeID(int nodeID) { + return "" + (nodeID & 0xff) + '.' + ((nodeID >> 8) & 0xff); + } + + private static int[] parseToInt(String[] text) { + try { + int[] data = new int[text.length]; + for (int i = 0, n = data.length; i < n; i++) { + data[i] = Integer.parseInt(text[i]); + } + return data; + } catch (NumberFormatException e) { + return null; + } + } + + public double getCPUPower() { + return (values[TIME_CPU] * POWER_CPU) / (values[TIME_CPU] + values[TIME_LPM]); + } + + public double getLPMPower() { + return (values[TIME_LPM] * POWER_LPM) / (values[TIME_CPU] + values[TIME_LPM]); + } + + public double getListenPower() { + return (values[TIME_LISTEN] * POWER_LISTEN) / (values[TIME_CPU] + values[TIME_LPM]); + } + + public double getTransmitPower() { + return (values[TIME_TRANSMIT] * POWER_TRANSMIT) / (values[TIME_CPU] + values[TIME_LPM]); + } + + public double getAveragePower() { + return (values[TIME_CPU] * POWER_CPU + values[TIME_LPM] * POWER_LPM + + values[TIME_LISTEN] * POWER_LISTEN + values[TIME_TRANSMIT] * POWER_TRANSMIT) + / (values[TIME_CPU] + values[TIME_LPM]); + } + + public long getPowerMeasureTime() { + return (1000 * (values[TIME_CPU] + values[TIME_LPM])) / TICKS_PER_SECOND; + } + + public double getTemperature() { + return -39.6 + 0.01 * values[TEMPERATURE]; + } + + public double getLatency() { + return values[LATENCY] / 4096.0; + } + + public double getHumidity() { +// double v = values[HUMIDITY]; +// double humidity = -4.0 + 0.0405 * v + -0.0000028 * v * v; +// // Correct humidity using temperature compensation +// return (getTemperature() - 25) * (0.01 + 0.00008*v + humidity); + return -4.0 + 405.0 * values[HUMIDITY] / 10000.0; + } + + public double getLight1() { +// double v = (values[LIGHT1] * VOLTAGE) / 4096.0; +// return 0.625 * 1000000 * v * 10; + return 10.0 * values[LIGHT1] / 7.0; + } + + public double getLight2() { + return 46.0 * values[LIGHT2] / 10.0; + } + + public String getBestNeighborID() { + return values[BEST_NEIGHBOR] > 0 ? mapNodeID(values[BEST_NEIGHBOR]): null; + } + + public double getBestNeighborETX() { + return values[BEST_NEIGHBOR_ETX] / 16.0; + } + +} diff --git a/examples/sky-shell/src/se/sics/contiki/collect/SerialConnection.java b/examples/sky-shell/src/se/sics/contiki/collect/SerialConnection.java new file mode 100644 index 000000000..cceca4ef8 --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/SerialConnection.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: SerialConnection.java,v 1.1 2008/07/09 23:18:06 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * SerialConnection + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 5 jul 2008 + * Updated : $Date: 2008/07/09 23:18:06 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +/** + * + */ +public abstract class SerialConnection { + + public static final String SERIALDUMP_WINDOWS = "./tools/serialdump-windows.exe"; + public static final String SERIALDUMP_LINUX = "./tools/serialdump-linux"; + + private String comPort; + private Process serialDumpProcess; + private PrintWriter serialOutput; + private boolean isRunning; + private boolean isOpen; + private String lastError; + + public boolean isOpen() { + return isOpen; + } + + public String getComPort() { + return comPort; + } + + public String getLastError() { + return lastError; + } + + public void open(String comPort) { + close(); + + this.comPort = comPort; + /* Connect to COM using external serialdump application */ + String osName = System.getProperty("os.name").toLowerCase(); + String fullCommand; + if (osName.startsWith("win")) { + fullCommand = SERIALDUMP_WINDOWS + " " + "-b115200" + " " + getMappedComPortForWindows(comPort); + } else { + fullCommand = SERIALDUMP_LINUX + " " + "-b115200" + " " + comPort; + } + + isRunning = true; + try { + String[] cmd = fullCommand.split(" "); + + serialDumpProcess = Runtime.getRuntime().exec(cmd); + final BufferedReader input = new BufferedReader(new InputStreamReader(serialDumpProcess.getInputStream())); + final BufferedReader err = new BufferedReader(new InputStreamReader(serialDumpProcess.getErrorStream())); + serialOutput = new PrintWriter(new OutputStreamWriter(serialDumpProcess.getOutputStream())); + + /* Start thread listening on stdout */ + Thread readInput = new Thread(new Runnable() { + public void run() { + String line; + try { + while ((line = input.readLine()) != null) { + serialData(line); + } + input.close(); + System.out.println("Serialdump process terminated."); + closeConnection(); + } catch (IOException e) { + lastError = "Error when reading from serialdump process: " + e; + System.err.println(lastError); + e.printStackTrace(); + closeConnection(); + } + } + }, "read input stream thread"); + + /* Start thread listening on stderr */ + Thread readError = new Thread(new Runnable() { + public void run() { + String line; + try { + while ((line = err.readLine()) != null) { + if (!isOpen && line.startsWith("connecting") && line.endsWith("[OK]")) { + isOpen = true; + serialOpened(); + } else { + System.err.println("Serialdump error stream> " + line); + } + } + err.close(); + } catch (IOException e) { + System.err.println("Error when reading from serialdump process: " + e); + e.printStackTrace(); + } + } + }, "read error stream thread"); + + readInput.start(); + readError.start(); + } catch (Exception e) { + lastError = "Failed to execute '" + fullCommand + "': " + e; + System.err.println(lastError); + e.printStackTrace(); + closeConnection(); + } + } + + private String getMappedComPortForWindows(String comPort) { + if (comPort.startsWith("COM")) { + comPort = "/dev/com" + comPort.substring(3); + } + return comPort; + } + + public void writeSerialData(String data) { + PrintWriter serialOutput = this.serialOutput; + if (serialOutput != null) { + serialOutput.println(data); + serialOutput.flush(); + } + } + + public void close() { + isOpen = false; + isRunning = false; + lastError = null; + closeConnection(); + } + + protected void closeConnection() { + if (serialOutput != null) { + serialOutput.close(); + serialOutput = null; + } + if (serialDumpProcess != null) { + serialDumpProcess.destroy(); + serialDumpProcess = null; + } + if (isRunning) { + isRunning = false; + serialClosed(); + } + } + + protected abstract void serialData(String line); + + protected abstract void serialOpened(); + + protected abstract void serialClosed(); + +} diff --git a/examples/sky-shell/src/se/sics/contiki/collect/Visualizer.java b/examples/sky-shell/src/se/sics/contiki/collect/Visualizer.java new file mode 100644 index 000000000..69cdd68ab --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/Visualizer.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: Visualizer.java,v 1.1 2008/07/09 23:18:06 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * Visualizer + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 3 jul 2008 + * Updated : $Date: 2008/07/09 23:18:06 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect; + +import java.awt.Component; + +/** + * + */ +public interface Visualizer { + + public String getTitle(); + public Component getPanel(); + public void nodesSelected(Node[] node); + public void nodeAdded(Node node); + public void nodeDataReceived(SensorData sensorData); + public void clearNodeData(); + +} diff --git a/examples/sky-shell/src/se/sics/contiki/collect/gui/BarChartPanel.java b/examples/sky-shell/src/se/sics/contiki/collect/gui/BarChartPanel.java new file mode 100644 index 000000000..94306fd97 --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/gui/BarChartPanel.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: BarChartPanel.java,v 1.1 2008/07/09 23:18:06 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * PowerPanel + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 5 jul 2008 + * Updated : $Date: 2008/07/09 23:18:06 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect.gui; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.JPanel; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.data.category.DefaultCategoryDataset; +import se.sics.contiki.collect.CollectServer; +import se.sics.contiki.collect.Node; +import se.sics.contiki.collect.SensorData; +import se.sics.contiki.collect.Visualizer; + +/** + * + */ +public abstract class BarChartPanel extends JPanel implements Visualizer { + + private static final long serialVersionUID = 7664283678708048061L; + + protected final CollectServer server; + protected final String title; + protected final String[] categories; + protected final JFreeChart chart; + protected final ChartPanel chartPanel; + protected final DefaultCategoryDataset dataset; + private int categoryOrder = 0; + + protected BarChartPanel(CollectServer server, String title, String chartTitle, String domainAxisLabel, String valueAxisLabel, + String[] categories) { + super(new BorderLayout()); + this.server = server; + this.title = title; + this.categories = categories; + + /* Create chart with power of all nodes */ + dataset = new DefaultCategoryDataset(); + this.chart = ChartFactory.createStackedBarChart(chartTitle, domainAxisLabel, valueAxisLabel, dataset, PlotOrientation.VERTICAL, true, true, false); + this.chartPanel = new ChartPanel(chart); + this.chartPanel.setPreferredSize(new Dimension(500, 270)); + if (categories.length > 1) { + this.chartPanel.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + categoryOrder++; + updateCharts(); + } + }); + } + add(chartPanel, BorderLayout.CENTER); + } + + @Override + public String getTitle() { + return title; + } + + @Override + public Component getPanel() { + return this; + } + + @Override + public void nodeAdded(Node node) { + if (isVisible()) { + for (int j = 0, m = categories.length; j < m; j++) { + dataset.addValue(0, categories[(j + categoryOrder) % categories.length], node.getName()); + } + int count = node.getSensorDataCount(); + if (count > 0) { + addSensorData(node.getSensorData(count - 1)); + } + } + } + + @Override + public void nodesSelected(Node[] nodes) { + } + + @Override + public void nodeDataReceived(SensorData data) { + if (isVisible()) { + addSensorData(data); + } + } + + @Override + public void clearNodeData() { + if (isVisible()) { + updateCharts(); + } + } + + private void updateCharts() { + dataset.clear(); + Node[] nodes = server.getNodes(); + if (nodes != null) { + for (int i = 0, n = nodes.length; i < n; i++) { + for (int j = 0, m = categories.length; j < m; j++) { + dataset.addValue(0, categories[(j + categoryOrder) % categories.length], nodes[i].getName()); + } + int count = nodes[i].getSensorDataCount(); + if (count > 0) { + addSensorData(nodes[i].getSensorData(count - 1)); + } + } + } + } + + public void setVisible(boolean visible) { + if (visible) { + updateCharts(); + } else { + dataset.clear(); + } + super.setVisible(visible); + } + + protected abstract void addSensorData(SensorData data); + +} diff --git a/examples/sky-shell/src/se/sics/contiki/collect/gui/MapPanel.java b/examples/sky-shell/src/se/sics/contiki/collect/gui/MapPanel.java new file mode 100644 index 000000000..bb0dc0568 --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/gui/MapPanel.java @@ -0,0 +1,581 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: MapPanel.java,v 1.1 2008/07/09 23:18:07 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * MapPanel + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 3 jul 2008 + * Updated : $Date: 2008/07/09 23:18:07 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect.gui; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.geom.Line2D; +import java.util.Hashtable; +import java.util.logging.Logger; +import javax.swing.ImageIcon; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.Timer; +import javax.swing.plaf.basic.BasicGraphicsUtils; +import se.sics.contiki.collect.CollectServer; +import se.sics.contiki.collect.Link; +import se.sics.contiki.collect.Node; +import se.sics.contiki.collect.SensorData; +import se.sics.contiki.collect.Visualizer; + +/** + * + */ +public class MapPanel extends JPanel implements Visualizer, ActionListener, MouseListener, MouseMotionListener { + + private static final long serialVersionUID = -8256619482599309425L; + + private static final Logger log = + Logger.getLogger(MapPanel.class.getName()); + + private static final boolean VISUAL_DRAG = true; + + private static final int FADE_COUNT = 20; + private static final int AGE_COUNT = 200; + + private static final Color[] OTHER_COLOR = new Color[FADE_COUNT]; + private static final Color[] LINK_COLOR = new Color[AGE_COUNT]; + + static { + for (int i = 0; i < FADE_COUNT; i++) { + OTHER_COLOR[i] = new Color(0xe0,0xe0,0x00,0xFF + - ((i * 255) / (FADE_COUNT - 1))); + } + + for (int i = 0, n = AGE_COUNT; i < n; i++) { + LINK_COLOR[i] = new Color(0x40 + i / 2, 0x40 + i / 2, 0xf0, 0xff - i); + } + } + + private static final int delta = 7; + + public static final int SHOW_BLINK = 40; + public static final int TOTAL_SHOW = 600; + + private Timer timer = new Timer(200, this); + private boolean hasPendingEvents = false; + private int ticker = 0; + + private JPopupMenu popupMenu; +// private MapNode popupNode; + private JMenuItem hideItem; + private JMenuItem resetNetworkItem; + + private Hashtable nodeTable = new Hashtable(); + private MapNode[] nodeList; + + private MapNode selectedNode; + private MapNode[] selectedMapNodes; + private Node[] selectedNodes; + private MapNode draggedNode; + private long draggedTime; + + private ImageIcon mapImage; + private String mapName; + + private final CollectServer server; + + private boolean hideNetwork = false; + + + public MapPanel(CollectServer server) { + super(new BorderLayout()); + this.server = server; + setPreferredSize(new Dimension(300, 200)); + + popupMenu = new JPopupMenu(getTitle()); +// popupMenu.addSeparator(); + hideItem = createMenuItem(popupMenu, "Hide Network"); + resetNetworkItem = createMenuItem(popupMenu, "Reset Network"); + + addMouseListener(this); + if (VISUAL_DRAG) { + addMouseMotionListener(this); + } + setBackground(Color.white); + } + + public String getMapBackground() { + return mapName; + } + + public boolean setMapBackground(String image) { + if (image == null) { + mapImage = null; + mapName = null; + return true; + } + + ImageIcon ii = new ImageIcon(image); + if (ii.getIconWidth() <= 0 || ii.getIconHeight() <= 0) { + log.warning("could not find image '" + image + '\''); + return false; + } + mapImage = ii; + mapName = image; + setPreferredSize(new Dimension(ii.getIconWidth(), ii.getIconHeight())); + repaint(); + return true; + } + + private JMenuItem createMenuItem(JPopupMenu menu, String title) { + JMenuItem item = new JMenuItem(title); + item.addActionListener(this); + menu.add(item); + return item; + } + + public void setVisible(boolean visible) { + if (visible) { + clear(); + timer.start(); + } else { + timer.stop(); + } + super.setVisible(visible); + } + + public void clear() { + draggedNode = null; + hasPendingEvents = false; + } + + + // ------------------------------------------------------------------- + // Node handling + // ------------------------------------------------------------------- + + public Node getNode(String id) { + MapNode node = nodeTable.get(id); + return node != null ? node.node : null; + } + + public MapNode getMapNode(String id) { + return nodeTable.get(id); + } + + private MapNode addMapNode(Node nd) { + MapNode node = nodeTable.get(nd.getID()); + if (node == null) { + node = new MapNode(this, nd); + synchronized (this) { + nodeTable.put("" + nd.getID(), node); + nodeList = nodeTable.values().toArray(new MapNode[nodeTable.size()]); + } + } + return node; + } + + + // ------------------------------------------------------------------- + // Visualizer + // ------------------------------------------------------------------- + + @Override + public String getTitle() { + return "Sensor Map"; + } + + @Override + public Component getPanel() { + return this; + } + + @Override + public void nodesSelected(Node[] nodes) { + if (selectedNodes != nodes) { + if (selectedMapNodes != null) { + for(MapNode node : selectedMapNodes) { + node.isSelected = false; + } + } + selectedNodes = nodes; + if (nodes == null || nodes.length == 0) { + selectedNode = null; + selectedMapNodes = null; + } else { + selectedMapNodes = new MapNode[nodes.length]; + for (int i = 0, n = nodes.length; i < n; i++) { + selectedMapNodes[i] = addMapNode(nodes[i]); + selectedMapNodes[i].isSelected = true; + } + selectedNode = selectedMapNodes[0]; + } + repaint(); + } + } + + @Override + public void nodeAdded(Node nd) { + addMapNode(nd); + repaint(); + } + + @Override + public void nodeDataReceived(SensorData sensorData) { + repaint(); + } + + @Override + public void clearNodeData() { + // Ignore + } + + + // ------------------------------------------------------------------- + // Graphics + // ------------------------------------------------------------------- + + public void paint(Graphics g) { + Graphics2D g2d = (Graphics2D) g; + int lx = 10; + super.paint(g); + long time = System.currentTimeMillis(); + if (mapImage != null) { + mapImage.paintIcon(this, g, 0, 0); + } + MapNode[] nodes = nodeList; + if (nodes != null) { + Line2D line = new Line2D.Double(); + for (int i = 0, m = nodes.length; i < m; i++) { + MapNode n = nodes[i]; + int x, y; + if (n.node.hasLocation()) { + x = n.node.getX(); + y = n.node.getY(); + } else { + x = lx; + y = 10; + lx += 30; + } + + if (!hideNetwork) { + FontMetrics fm = g.getFontMetrics(); + int fnHeight = fm.getHeight(); + int fnDescent = fm.getDescent(); + for (int j = 0, mu = n.node.getLinkCount(); j < mu; j++) { + Link link = n.node.getLink(j); + if (link.node.hasLocation()) { + long age = (time - link.getLastActive()) / 100; + int x2 = link.node.getX(); + int y2 = link.node.getY(); + if (n.isSelected) { + if (age > LINK_COLOR.length) { + age = 100; + } else { + age -= 50; + } + } + line.setLine(x, y, x2, y2); + if (age < LINK_COLOR.length) { + g.setColor(age < 0 ? LINK_COLOR[0] : LINK_COLOR[(int) age]); + } else { + g.setColor(LINK_COLOR[LINK_COLOR.length - 1]); + } + g2d.draw(line); +// g.setColor(Color.lightGray); + int xn1, xn2, yn1, yn2; + if (x <= x2) { + xn1 = x; xn2 = x2; + yn1 = y; yn2 = y2; + } else { + xn1 = x2; xn2 = x; + yn1 = y2; yn2 = y; + } + int dx = xn1 + (xn2 - xn1) / 2 + 4; + int dy = yn1 + (yn2 - yn1) / 2 - fnDescent; + if (yn2 < yn1) { + dy += fnHeight - fnDescent; + } + g.drawString("ETX:" + (((int)(link.getETX() * 100 + 0.5)) / 100.0), dx, dy); + } + } + } + + n.paint(g, x, y); + + g.setColor(Color.black); + if (n.isSelected) { + BasicGraphicsUtils.drawDashedRect(g, x - delta, y - delta, 2 * delta, 2 * delta); + } + if (selectedNode != null && selectedNode.message != null) { + g.drawString(selectedNode.message, 10, 10); + } + } + } + } + + + // ------------------------------------------------------------------- + // ActionListener + // ------------------------------------------------------------------- + + public void actionPerformed(ActionEvent e) { + Object source = e.getSource(); + if (source == timer) { + ticker++; + if (hasPendingEvents) { + boolean repaint = false; + hasPendingEvents = false; + MapNode[] nodes = nodeList; + if (nodes != null) { + long time = System.currentTimeMillis(); + for (int i = 0, n = nodes.length; i < n; i++) { + if (nodes[i].tick(time)) { + repaint = true; + } + } + } + if (repaint) { + hasPendingEvents = true; + repaint(); + } + } else if ((ticker % 10) == 0) { + repaint(); + } + } else if (source == hideItem) { + hideNetwork = !hideNetwork; + if (!hideNetwork) hideItem.setText("Hide Network"); + else hideItem.setText("Show Network"); + repaint(); + + } else if (source == resetNetworkItem) { + MapNode[] nodes = nodeList; + if (nodes != null) { + for (int i = 0, m = nodes.length; i < m; i++) { + MapNode n = nodes[i]; + n.node.clearLinks(); + } + repaint(); + } + } + } + + + // ------------------------------------------------------------------- + // Mouselistener + // ------------------------------------------------------------------- + + private MapNode getNodeAt(int mx, int my) { + int lx = 10; + MapNode[] nodes = nodeList; + if (nodes != null) { + for (int i = 0, m = nodes.length; i < m; i++) { + MapNode n = nodes[i]; + int x, y; + if (n.node.hasLocation()) { + x = n.node.getX(); + y = n.node.getY(); + } else { + x = lx; + y = 10; + lx += 30; + } + if (mx >= (x - delta) + && mx <= (x + delta) + && my >= (y - delta) + && my <= (y + delta)) { + return n; + } + } + } + return null; + } + + public void mouseClicked(MouseEvent e) { + int mx = e.getX(); + int my = e.getY(); + if (e.getButton() == MouseEvent.BUTTON1) { + MapNode node = getNodeAt(mx, my); + if (node != selectedNode) { + server.selectNodes(node == null ? null : new Node[] { node.node }); + } + } + showPopup(e); + } + + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + MapNode aNode = getNodeAt(e.getX(), e.getY()); + if (aNode != selectedNode) { + server.selectNodes(aNode != null ? new Node[] { aNode.node } : null); + } + draggedNode = aNode; + draggedTime = System.currentTimeMillis(); + + } else if (selectedNode != null) { + selectedNode = draggedNode = null; + server.selectNodes(null); + } + showPopup(e); + } + + public void mouseReleased(MouseEvent e) { + if (draggedNode != null && e.getButton() == MouseEvent.BUTTON1) { + if ((!VISUAL_DRAG || (draggedTime > 0)) && + (System.currentTimeMillis() - draggedTime) < 300) { + // Do not drag if mouse is only moved during click + + } else { + draggedNode.node.setLocation(e.getX(), e.getY()); + server.updateNodeLocation(draggedNode.node); + draggedNode = null; + repaint(); + } + } + + showPopup(e); + } + + private void showPopup(MouseEvent e) { + if (e.isPopupTrigger() + && (e.getModifiers() & (MouseEvent.SHIFT_MASK|MouseEvent.CTRL_MASK)) == 0) { +// popupNode = getNodeAt(e.getX(), e.getY()); +// nodeItem.setEnabled(popupNode != null); + popupMenu.show(this, e.getX(), e.getY()); + } + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + + // ------------------------------------------------------------------- + // MouseMotion + // ------------------------------------------------------------------- + + public void mouseDragged(MouseEvent e) { + if (!VISUAL_DRAG || draggedNode == null) { + // Do nothing + + } else if (draggedTime > 0) { + if ((System.currentTimeMillis() - draggedTime) > 300) { + // No mouse click, time to drag the node + draggedTime = -1; + } + + } else { + draggedNode.node.setLocation(e.getX(), e.getY()); + repaint(); + } + } + + public void mouseMoved(MouseEvent e) { + } + + + // ------------------------------------------------------------------- + // MapNode + // ------------------------------------------------------------------- + + private static class MapNode { + + public final Node node; + public boolean isSelected; + public String message = null; + + private int tick = 0; + + MapNode(MapPanel panel, Node node) { + this.node = node; + } + + boolean tick(long time) { + boolean r = false; + if (tick > 0) { + tick--; + r = true; + } + + for (int i = 0, n = node.getLinkCount(); i < n; i++) { + Link link = node.getLink(i); + long age = (time - link.getLastActive()) / 100; + if (age < 200) { + r = true; + break; + } + } + return r; + } + + public void paint(Graphics g, int x, int y) { + if (tick > (TOTAL_SHOW - SHOW_BLINK)) { + if ((tick & 4) == 0) { + // Hide circle + } else { + int index = FADE_COUNT - tick - 1; + if (index < 0) { + index = 0; + } + final int d = 8; + g.setColor(OTHER_COLOR[index]); + g.fillOval(x - d, y - d, d * 2 + 1, d * 2 + 1); + } + } + + if (tick < (TOTAL_SHOW - SHOW_BLINK) && tick > 0) { + g.setColor(Color.red); + int height = 13 * tick / TOTAL_SHOW; + g.fillRect(x - 6, 5 + y - height, 2, height); + } + + g.setColor(Color.black); + final int od = 3; + g.drawString(node.getID(), x + od * 2 + 3, y + 4); + g.fillOval(x - od, y - od, od * 2 + 1, od * 2 + 1); + } + + } // end of inner class MapNode + +} diff --git a/examples/sky-shell/src/se/sics/contiki/collect/gui/SerialConsole.java b/examples/sky-shell/src/se/sics/contiki/collect/gui/SerialConsole.java new file mode 100644 index 000000000..57697d111 --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/gui/SerialConsole.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: SerialConsole.java,v 1.1 2008/07/09 23:18:07 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * SerialConsole + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 4 jul 2008 + * Updated : $Date: 2008/07/09 23:18:07 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect.gui; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import se.sics.contiki.collect.CollectServer; +import se.sics.contiki.collect.Node; +import se.sics.contiki.collect.SensorData; +import se.sics.contiki.collect.Visualizer; + +/** + * + */ +public class SerialConsole implements Visualizer { + + private final CollectServer server; + private JPanel panel; + private JTextArea logArea; + private JTextField commandField; + private String[] history = new String[50]; + private int historyPos = 0; + private int historyCount = 0; + + public SerialConsole(CollectServer server) { + this.server = server; + panel = new JPanel(new BorderLayout()); + logArea = new JTextArea(4, 30); + logArea.setEditable(false); + panel.add(new JScrollPane(logArea), BorderLayout.CENTER); + + JPopupMenu popupMenu = new JPopupMenu(); + JMenuItem clearItem = new JMenuItem("Clear"); + clearItem.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + logArea.setText(""); + } + + }); + popupMenu.add(clearItem); + logArea.setComponentPopupMenu(popupMenu); + + commandField = new JTextField(); + commandField.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + String command = trim(commandField.getText()); + if (command != null) { + try { + int previous = historyCount - 1; + if (previous < 0) previous += history.length; + if (!command.equals(history[previous])) { + history[historyCount] = command; + historyCount = (historyCount + 1) % history.length; + } + historyPos = historyCount; + SerialConsole.this.server.sendToNode(command); + commandField.setText(""); + } catch (Exception ex) { + System.err.println("could not send '" + command + "':"); + ex.printStackTrace(); + JOptionPane.showMessageDialog(panel, + "could not send '" + command + "':\n" + + ex, "ERROR", + JOptionPane.ERROR_MESSAGE); + } + } else { + commandField.getToolkit().beep(); + } + } + + }); + commandField.addKeyListener(new KeyAdapter() { + + @Override + public void keyPressed(KeyEvent e) { + switch (e.getKeyCode()) { + case KeyEvent.VK_UP: { + int nextPos = (historyPos + history.length - 1) % history.length; + if (nextPos == historyCount || history[nextPos] == null) { + commandField.getToolkit().beep(); + } else { + String cmd = trim(commandField.getText()); + if (cmd != null) { + history[historyPos] = cmd; + } + historyPos = nextPos; + commandField.setText(history[historyPos]); + } + break; + } + case KeyEvent.VK_DOWN: { + int nextPos = (historyPos + 1) % history.length; + if (nextPos == historyCount) { + historyPos = nextPos; + commandField.setText(""); + } else if (historyPos == historyCount || history[nextPos] == null) { + commandField.getToolkit().beep(); + } else { + String cmd = trim(commandField.getText()); + if (cmd != null) { + history[historyPos] = cmd; + } + historyPos = nextPos; + commandField.setText(history[historyPos]); + } + break; + } + } + } + + }); + panel.add(commandField, BorderLayout.SOUTH); + } + + @Override + public Component getPanel() { + return panel; + } + + @Override + public String getTitle() { + return "Serial Console"; + } + + @Override + public void nodeAdded(Node node) { + // Ignore + } + + @Override + public void nodeDataReceived(SensorData sensorData) { + // Ignore + } + + @Override + public void clearNodeData() { + // Ignore + } + + @Override + public void nodesSelected(Node[] node) { + // Ignore + } + + public void addSerialData(final String text) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + String current = logArea.getText(); + int len = current.length(); + if (len > 4096) { + current = current.substring(len - 4096); + } + current = len > 0 ? (current + '\n' + text) : text; + logArea.setText(current); + logArea.setCaretPosition(current.length()); + } + }); + } + + private String trim(String text) { + return (text != null) && ((text = text.trim()).length() > 0) ? text : null; + } + +} diff --git a/examples/sky-shell/src/se/sics/contiki/collect/gui/TimeChartPanel.java b/examples/sky-shell/src/se/sics/contiki/collect/gui/TimeChartPanel.java new file mode 100644 index 000000000..7616ab119 --- /dev/null +++ b/examples/sky-shell/src/se/sics/contiki/collect/gui/TimeChartPanel.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2008, Swedish Institute of Computer Science. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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. + * + * $Id: TimeChartPanel.java,v 1.1 2008/07/09 23:18:07 nifi Exp $ + * + * ----------------------------------------------------------------- + * + * PowerPanel + * + * Authors : Joakim Eriksson, Niclas Finne + * Created : 3 jul 2008 + * Updated : $Date: 2008/07/09 23:18:07 $ + * $Revision: 1.1 $ + */ + +package se.sics.contiki.collect.gui; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.util.Date; +import javax.swing.JPanel; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.data.time.Second; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import se.sics.contiki.collect.CollectServer; +import se.sics.contiki.collect.Node; +import se.sics.contiki.collect.SensorData; +import se.sics.contiki.collect.Visualizer; + +/** + * + */ +public abstract class TimeChartPanel extends JPanel implements Visualizer { + + private static final long serialVersionUID = -607864439709540641L; + + protected final CollectServer server; + protected final String title; + protected final TimeSeriesCollection timeSeries; + protected final JFreeChart chart; + protected final ChartPanel chartPanel; + + private Node[] selectedNodes; + + private double minValue; + private double maxValue; + private int rangeTick = 0; + private boolean hasGlobalRange; + + public TimeChartPanel(CollectServer server, String title, + String chartTitle, String timeAxisLabel, String valueAxisLabel) { + super(new BorderLayout()); + this.server = server; + this.title = title; + this.timeSeries = new TimeSeriesCollection(); + this.chart = ChartFactory.createTimeSeriesChart( + chartTitle, timeAxisLabel, valueAxisLabel, timeSeries, + true, true, false + ); + this.chartPanel = new ChartPanel(chart); + this.chartPanel.setPreferredSize(new Dimension(500, 270)); + add(chartPanel, BorderLayout.CENTER); + } + + @Override + public String getTitle() { + return title; + } + + @Override + public Component getPanel() { + return this; + } + + @Override + public void nodeAdded(Node node) { + // Ignore + } + + @Override + public void nodesSelected(Node[] nodes) { + if (this.selectedNodes != nodes) { + this.selectedNodes = nodes; + if (isVisible()) { + updateCharts(); + } + } + } + + @Override + public void nodeDataReceived(SensorData data) { + if (hasGlobalRange) { + boolean update = false; + if (minValue > maxValue) { + update = true; + } else { + double value = getSensorDataValue(data); + if (value < minValue) { + minValue = value; + update = true; + } + if (value > maxValue) { + maxValue = value; + update = true; + } + } + if (update && isVisible()) { + updateGlobalRange(); + } + } + if (isVisible() && selectedNodes != null && selectedNodes.length == timeSeries.getSeriesCount()) { + Node node = data.getNode(); + for (int i = 0, n = selectedNodes.length; i < n; i++) { + if (node == selectedNodes[i]) { + TimeSeries series = timeSeries.getSeries(i); + series.addOrUpdate(new Second(new Date(data.getTime())), getSensorDataValue(data)); + chartPanel.repaint(); + break; + } + } + } + } + + @Override + public void clearNodeData() { + if (isVisible()) { + updateCharts(); + } + } + + private void updateCharts() { + timeSeries.removeAllSeries(); + if (this.selectedNodes != null) { + for(Node node: this.selectedNodes) { + TimeSeries series = new TimeSeries(node.getName(), Second.class); + for (int i = 0, n = node.getSensorDataCount(); i < n; i++) { + SensorData data = node.getSensorData(i); + series.addOrUpdate(new Second(new Date(data.getTime())), getSensorDataValue(data)); + } + timeSeries.addSeries(series); + } + } + } + + public int getRangeTick() { + return rangeTick; + } + + public void setRangeTick(int rangeTick) { + this.rangeTick = rangeTick; + } + + public double getRangeMinimumSize() { + return chart.getXYPlot().getRangeAxis().getAutoRangeMinimumSize(); + } + + public void setRangeMinimumSize(double size) { + chart.getXYPlot().getRangeAxis().setAutoRangeMinimumSize(size); + } + + public boolean hasGlobalRange() { + return hasGlobalRange; + } + + public void setGlobalRange(boolean hasGlobalRange) { + if (this.hasGlobalRange != hasGlobalRange) { + this.hasGlobalRange = hasGlobalRange; + if (hasGlobalRange) { + minValue = Double.MAX_VALUE; + maxValue = Double.MIN_NORMAL; + if (isVisible()) { + updateGlobalRange(); + } + } else { + chart.getXYPlot().getRangeAxis().setAutoRange(true); + } + } + } + + private void updateGlobalRange() { + if (hasGlobalRange) { + if (minValue > maxValue) { + for (int i = 0, n = server.getSensorDataCount(); i < n; i++) { + double value = getSensorDataValue(server.getSensorData(i)); + if (value < minValue) minValue = value; + if (value > maxValue) maxValue = value; + } + } + if (minValue < maxValue) { + double minSize = getRangeMinimumSize(); + double min = minValue; + double max = maxValue; + if (max - min < minSize) { + double d = (minSize - (max - min)) / 2; + min -= d; + max += d; + } + if (rangeTick > 0) { + min = ((int) (min - rangeTick / 2) / rangeTick) * rangeTick; +// max = ((int) (max + rangeTick / 2) / rangeTick) * rangeTick; + } + chart.getXYPlot().getRangeAxis().setRange(min, max); + } + } + } + + protected abstract double getSensorDataValue(SensorData data); + + public void setVisible(boolean visible) { + if (visible) { + updateGlobalRange(); + updateCharts(); + } else { + timeSeries.removeAllSeries(); + } + super.setVisible(visible); + } + +}