From 0e0b232907e144f55080e6fed617db620776f4b5 Mon Sep 17 00:00:00 2001 From: scipio Date: Fri, 13 Apr 2007 00:10:28 +0000 Subject: [PATCH] LQI version of MultihopOscilloscope. --- apps/MultihopOscilloscopeLqi/Makefile | 4 + .../MultihopOscilloscope.h | 36 +++ .../MultihopOscilloscopeAppC.nc | 68 ++++ .../MultihopOscilloscopeC.nc | 276 ++++++++++++++++ apps/MultihopOscilloscopeLqi/README.txt | 51 +++ .../java/ColorCellEditor.java | 55 ++++ apps/MultihopOscilloscopeLqi/java/Data.java | 74 +++++ apps/MultihopOscilloscopeLqi/java/Graph.java | 232 ++++++++++++++ apps/MultihopOscilloscopeLqi/java/Makefile | 21 ++ apps/MultihopOscilloscopeLqi/java/Node.java | 101 ++++++ .../java/Oscilloscope.java | 128 ++++++++ apps/MultihopOscilloscopeLqi/java/Window.java | 294 ++++++++++++++++++ .../java/oscilloscope.jar | Bin 0 -> 18901 bytes 13 files changed, 1340 insertions(+) create mode 100644 apps/MultihopOscilloscopeLqi/Makefile create mode 100644 apps/MultihopOscilloscopeLqi/MultihopOscilloscope.h create mode 100644 apps/MultihopOscilloscopeLqi/MultihopOscilloscopeAppC.nc create mode 100644 apps/MultihopOscilloscopeLqi/MultihopOscilloscopeC.nc create mode 100644 apps/MultihopOscilloscopeLqi/README.txt create mode 100644 apps/MultihopOscilloscopeLqi/java/ColorCellEditor.java create mode 100644 apps/MultihopOscilloscopeLqi/java/Data.java create mode 100644 apps/MultihopOscilloscopeLqi/java/Graph.java create mode 100644 apps/MultihopOscilloscopeLqi/java/Makefile create mode 100644 apps/MultihopOscilloscopeLqi/java/Node.java create mode 100644 apps/MultihopOscilloscopeLqi/java/Oscilloscope.java create mode 100644 apps/MultihopOscilloscopeLqi/java/Window.java create mode 100644 apps/MultihopOscilloscopeLqi/java/oscilloscope.jar diff --git a/apps/MultihopOscilloscopeLqi/Makefile b/apps/MultihopOscilloscopeLqi/Makefile new file mode 100644 index 00000000..64913b5b --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/Makefile @@ -0,0 +1,4 @@ +COMPONENT=MultihopOscilloscopeAppC +CFLAGS += -I$(TOSDIR)/lib/net/ -I$(TOSDIR)/lib/net/lqi + +include $(MAKERULES) diff --git a/apps/MultihopOscilloscopeLqi/MultihopOscilloscope.h b/apps/MultihopOscilloscopeLqi/MultihopOscilloscope.h new file mode 100644 index 00000000..5f040539 --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/MultihopOscilloscope.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2006 Intel Corporation + * All rights reserved. + * + * This file is distributed under the terms in the attached INTEL-LICENSE + * file. If you do not find these files, copies can be found by writing to + * Intel Research Berkeley, 2150 Shattuck Avenue, Suite 1300, Berkeley, CA, + * 94704. Attention: Intel License Inquiry. + */ + +/** + * @author David Gay + * @author Kyle Jamieson + */ + +#ifndef MULTIHOP_OSCILLOSCOPE_H +#define MULTIHOP_OSCILLOSCOPE_H + +enum { + /* Number of readings per message. If you increase this, you may have to + increase the message_t size. */ + NREADINGS = 5, + /* Default sampling period. */ + DEFAULT_INTERVAL = 1024, + AM_OSCILLOSCOPE = 0x93 +}; + +typedef nx_struct oscilloscope { + nx_uint16_t version; /* Version of the interval. */ + nx_uint16_t interval; /* Samping period. */ + nx_uint16_t id; /* Mote id of sending mote. */ + nx_uint16_t count; /* The readings are samples count * NREADINGS onwards */ + nx_uint16_t readings[NREADINGS]; +} oscilloscope_t; + +#endif diff --git a/apps/MultihopOscilloscopeLqi/MultihopOscilloscopeAppC.nc b/apps/MultihopOscilloscopeLqi/MultihopOscilloscopeAppC.nc new file mode 100644 index 00000000..cefe6747 --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/MultihopOscilloscopeAppC.nc @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2006 Intel Corporation + * All rights reserved. + * + * This file is distributed under the terms in the attached INTEL-LICENSE + * file. If you do not find these files, copies can be found by writing to + * Intel Research Berkeley, 2150 Shattuck Avenue, Suite 1300, Berkeley, CA, + * 94704. Attention: Intel License Inquiry. + */ + +/** + * MultihopOscilloscope demo application using the collection layer. + * See README.txt file in this directory and TEP 119: Collection. + * + * @author David Gay + * @author Kyle Jamieson + */ + +configuration MultihopOscilloscopeAppC { } +implementation { + components MainC, MultihopOscilloscopeC, LedsC, new TimerMilliC(), + new DemoSensorC() as Sensor; + + //MainC.SoftwareInit -> Sensor; + + MultihopOscilloscopeC.Boot -> MainC; + MultihopOscilloscopeC.Timer -> TimerMilliC; + MultihopOscilloscopeC.Read -> Sensor; + MultihopOscilloscopeC.Leds -> LedsC; + + // + // Communication components. These are documented in TEP 113: + // Serial Communication, and TEP 119: Collection. + // + components CollectionC as Collector, // Collection layer + ActiveMessageC, // AM layer + new CollectionSenderC(AM_OSCILLOSCOPE), // Sends multihop RF + SerialActiveMessageC, // Serial messaging + new SerialAMSenderC(AM_OSCILLOSCOPE); // Sends to the serial port + + MultihopOscilloscopeC.RadioControl -> ActiveMessageC; + MultihopOscilloscopeC.SerialControl -> SerialActiveMessageC; + MultihopOscilloscopeC.RoutingControl -> Collector; + + MultihopOscilloscopeC.Send -> CollectionSenderC; + MultihopOscilloscopeC.SerialSend -> SerialAMSenderC.AMSend; + MultihopOscilloscopeC.Snoop -> Collector.Snoop[AM_OSCILLOSCOPE]; + MultihopOscilloscopeC.Receive -> Collector.Receive[AM_OSCILLOSCOPE]; + MultihopOscilloscopeC.RootControl -> Collector; + + components new PoolC(message_t, 10) as UARTMessagePoolP, + new QueueC(message_t*, 10) as UARTQueueP; + + MultihopOscilloscopeC.UARTMessagePool -> UARTMessagePoolP; + MultihopOscilloscopeC.UARTQueue -> UARTQueueP; + + components new PoolC(message_t, 20) as DebugMessagePool, + new QueueC(message_t*, 20) as DebugSendQueue, + new SerialAMSenderC(AM_LQI_DEBUG) as DebugSerialSender, + UARTDebugSenderP as DebugSender; + + DebugSender.Boot -> MainC; + DebugSender.UARTSend -> DebugSerialSender; + DebugSender.MessagePool -> DebugMessagePool; + DebugSender.SendQueue -> DebugSendQueue; + Collector.CollectionDebug -> DebugSender; + +} diff --git a/apps/MultihopOscilloscopeLqi/MultihopOscilloscopeC.nc b/apps/MultihopOscilloscopeLqi/MultihopOscilloscopeC.nc new file mode 100644 index 00000000..fff54d7c --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/MultihopOscilloscopeC.nc @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2006 Intel Corporation + * All rights reserved. + * + * This file is distributed under the terms in the attached INTEL-LICENSE + * file. If you do not find these files, copies can be found by writing to + * Intel Research Berkeley, 2150 Shattuck Avenue, Suite 1300, Berkeley, CA, + * 94704. Attention: Intel License Inquiry. + */ + +/** + * MultihopOscilloscope demo application using the collection layer. + * See README.txt file in this directory and TEP 119: Collection. + * + * @author David Gay + * @author Kyle Jamieson + */ + +#include "Timer.h" +#include "MultihopOscilloscope.h" + +module MultihopOscilloscopeC { + uses { + // Interfaces for initialization: + interface Boot; + interface SplitControl as RadioControl; + interface SplitControl as SerialControl; + interface StdControl as RoutingControl; + + // Interfaces for communication, multihop and serial: + interface Send; + interface Receive as Snoop; + interface Receive; + interface AMSend as SerialSend; + interface CollectionPacket; + interface RootControl; + + interface Queue as UARTQueue; + interface Pool as UARTMessagePool; + + // Miscalleny: + interface Timer; + interface Read; + interface Leds; + } +} + +implementation { + task void uartSendTask(); + static void startTimer(); + static void fatal_problem(); + static void report_problem(); + static void report_sent(); + static void report_received(); + + uint8_t uartlen; + message_t sendbuf; + message_t uartbuf; + bool sendbusy=FALSE, uartbusy=FALSE; + + /* Current local state - interval, version and accumulated readings */ + oscilloscope_t local; + + uint8_t reading; /* 0 to NREADINGS */ + + /* When we head an Oscilloscope message, we check it's sample count. If + it's ahead of ours, we "jump" forwards (set our count to the received + count). However, we must then suppress our next count increment. This + is a very simple form of "time" synchronization (for an abstract + notion of time). */ + bool suppress_count_change; + + // + // On bootup, initialize radio and serial communications, and our + // own state variables. + // + event void Boot.booted() { + local.interval = DEFAULT_INTERVAL; + local.id = TOS_NODE_ID; + local.version = 0; + + // Beginning our initialization phases: + if (call RadioControl.start() != SUCCESS) + fatal_problem(); + + if (call RoutingControl.start() != SUCCESS) + fatal_problem(); + } + + event void RadioControl.startDone(error_t error) { + if (error != SUCCESS) + fatal_problem(); + + if (sizeof(local) > call Send.maxPayloadLength()) + fatal_problem(); + + if (call SerialControl.start() != SUCCESS) + fatal_problem(); + } + + event void SerialControl.startDone(error_t error) { + if (error != SUCCESS) + fatal_problem(); + + // This is how to set yourself as a root to the collection layer: + if (local.id % 500 == 0) + call RootControl.setRoot(); + + startTimer(); + } + + static void startTimer() { + if (call Timer.isRunning()) call Timer.stop(); + call Timer.startPeriodic(local.interval); + reading = 0; + } + + event void RadioControl.stopDone(error_t error) { } + event void SerialControl.stopDone(error_t error) { } + + // + // Only the root will receive messages from this interface; its job + // is to forward them to the serial uart for processing on the pc + // connected to the sensor network. + // + event message_t* + Receive.receive(message_t* msg, void *payload, uint8_t len) { + oscilloscope_t* in = (oscilloscope_t*)payload; + oscilloscope_t* out; + if (uartbusy == FALSE) { + out = (oscilloscope_t*)call SerialSend.getPayload(&uartbuf); + if (len != sizeof(oscilloscope_t)) { + return msg; + } + else { + memcpy(out, in, sizeof(oscilloscope_t)); + } + uartlen = sizeof(oscilloscope_t); + post uartSendTask(); + } else { + // The UART is busy; queue up messages and service them when the + // UART becomes free. + message_t *newmsg = call UARTMessagePool.get(); + if (newmsg == NULL) { + // drop the message on the floor if we run out of queue space. + report_problem(); + return msg; + } + + //Prepare message to be sent over the uart + out = (oscilloscope_t*)call SerialSend.getPayload(newmsg); + memcpy(out, in, sizeof(oscilloscope_t)); + + if (call UARTQueue.enqueue(newmsg) != SUCCESS) { + // drop the message on the floor and hang if we run out of + // queue space without running out of queue space first (this + // should not occur). + call UARTMessagePool.put(newmsg); + fatal_problem(); + return msg; + } + } + + return msg; + } + + task void uartSendTask() { + if (call SerialSend.send(0xffff, &uartbuf, uartlen) != SUCCESS) { + report_problem(); + } else { + uartbusy = TRUE; + } + } + + event void SerialSend.sendDone(message_t *msg, error_t error) { + uartbusy = FALSE; + if (call UARTQueue.empty() == FALSE) { + // We just finished a UART send, and the uart queue is + // non-empty. Let's start a new one. + message_t *queuemsg = call UARTQueue.dequeue(); + if (queuemsg == NULL) { + fatal_problem(); + return; + } + memcpy(&uartbuf, queuemsg, sizeof(message_t)); + if (call UARTMessagePool.put(queuemsg) != SUCCESS) { + fatal_problem(); + return; + } + post uartSendTask(); + } + } + + // + // Overhearing other traffic in the network. + // + event message_t* + Snoop.receive(message_t* msg, void* payload, uint8_t len) { + oscilloscope_t *omsg = payload; + + report_received(); + + // If we receive a newer version, update our interval. + if (omsg->version > local.version) { + local.version = omsg->version; + local.interval = omsg->interval; + startTimer(); + } + + // If we hear from a future count, jump ahead but suppress our own + // change. + if (omsg->count > local.count) { + local.count = omsg->count; + suppress_count_change = TRUE; + } + + return msg; + } + + /* At each sample period: + - if local sample buffer is full, send accumulated samples + - read next sample + */ + event void Timer.fired() { + if (reading == NREADINGS) { + if (!sendbusy) { + oscilloscope_t *o = (oscilloscope_t *)call Send.getPayload(&sendbuf); + memcpy(o, &local, sizeof(local)); + if (call Send.send(&sendbuf, sizeof(local)) == SUCCESS) + sendbusy = TRUE; + else + report_problem(); + } + + reading = 0; + /* Part 2 of cheap "time sync": increment our count if we didn't + jump ahead. */ + if (!suppress_count_change) + local.count++; + suppress_count_change = FALSE; + } + + if (call Read.read() != SUCCESS) + fatal_problem(); + } + + event void Send.sendDone(message_t* msg, error_t error) { + if (error == SUCCESS) + report_sent(); + else + report_problem(); + + sendbusy = FALSE; + } + + event void Read.readDone(error_t result, uint16_t data) { + if (result != SUCCESS) { + data = 0xffff; + report_problem(); + } + local.readings[reading++] = data; + } + + + // Use LEDs to report various status issues. + static void fatal_problem() { + call Leds.led0On(); + call Leds.led1On(); + call Leds.led2On(); + call Timer.stop(); + } + + static void report_problem() { call Leds.led0Toggle(); } + static void report_sent() { call Leds.led1Toggle(); } + static void report_received() { call Leds.led2Toggle(); } +} diff --git a/apps/MultihopOscilloscopeLqi/README.txt b/apps/MultihopOscilloscopeLqi/README.txt new file mode 100644 index 00000000..e186a96b --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/README.txt @@ -0,0 +1,51 @@ +README for MultihopOscilloscopeLqi +Author/Contact: tinyos-help@millennium.berkeley.edu + +Description: + +MultihopOscilloscope is a simple data-collection demo. This variant, +MultihopOscilloscopeLqi, works only on platforms that have the CC2420. +Rather than use CTP, it uses MultihopLqi (lib/net/lqi), which is +much lighter weight but not quite as efficient or reliable. + +The application periodically samples +the default sensor and broadcasts a message every few readings. These readings +can be displayed by the Java "Oscilloscope" application found in the +TOSROOT/apps/Oscilloscope/java subdirectory. The sampling rate starts at 4Hz, +but can be changed from the Java application. + +You can compile MultihopOscilloscope with a sensor board's default sensor by +compiling as follows: + + SENSORBOARD= make + +You can change the sensor used by editing MultihopOscilloscopeAppC.nc. + +Tools: + +The Java application displays readings it receives from motes running the +MultihopOscilloscope demo via a serial forwarder. To run it, change to the +TOSROOT/apps/Oscilloscope/java subdirectory and type: + + make + java net.tinyos.sf.SerialForwarder -comm serial@: + # e.g., java net.tinyps.sf.SerialForwarder -comm serial@/dev/ttyUSB0:mica2 + # or java net.tinyps.sf.SerialForwarder -comm serial@COM2:telosb + ./run + +The controls at the bootom of the screen allow yoy to zoom in or out the X +axis, change the range of the Y axis, and clear all received data. You can +change the color used to display a mote by clicking on its color in the +mote table. + +Known bugs/limitations: + +None. + +See also: +TEP 113: Serial Communications, TEP 119: Collection. + +Notes: + +MultihopOscilloscope configures a mote whose TOS_NODE_ID modulo 500 is zero +to be a collection root. diff --git a/apps/MultihopOscilloscopeLqi/java/ColorCellEditor.java b/apps/MultihopOscilloscopeLqi/java/ColorCellEditor.java new file mode 100644 index 00000000..f88b0b6f --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/java/ColorCellEditor.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2006 Intel Corporation + * All rights reserved. + * + * This file is distributed under the terms in the attached INTEL-LICENSE + * file. If you do not find these files, copies can be found by writing to + * Intel Research Berkeley, 2150 Shattuck Avenue, Suite 1300, Berkeley, CA, + * 94704. Attention: Intel License Inquiry. + */ + +import javax.swing.*; +import javax.swing.table.*; +import java.awt.*; +import java.awt.event.*; + +/* Editor for table cells representing colors. Popup a color chooser. */ +public class ColorCellEditor extends AbstractCellEditor + implements TableCellEditor { + private Color color; + private JButton button; + + public ColorCellEditor(String title) { + button = new JButton(); + final JColorChooser chooser = new JColorChooser(); + final JDialog dialog = JColorChooser.createDialog + (button, title, true, chooser, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + color = chooser.getColor(); + } }, + null); + + button.setBorderPainted(false); + button.addActionListener + (new ActionListener () { + public void actionPerformed(ActionEvent e) { + button.setBackground(color); + chooser.setColor(color); + dialog.setVisible(true); + fireEditingStopped(); + } } ); + + } + + public Object getCellEditorValue() { return color; } + public Component getTableCellEditorComponent(JTable table, + Object value, + boolean isSelected, + int row, + int column) { + color = (Color)value; + return button; + } +} + diff --git a/apps/MultihopOscilloscopeLqi/java/Data.java b/apps/MultihopOscilloscopeLqi/java/Data.java new file mode 100644 index 00000000..ac35aa72 --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/java/Data.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2006 Intel Corporation + * All rights reserved. + * + * This file is distributed under the terms in the attached INTEL-LICENSE + * file. If you do not find these files, copies can be found by writing to + * Intel Research Berkeley, 2150 Shattuck Avenue, Suite 1300, Berkeley, CA, + * 94704. Attention: Intel License Inquiry. + */ + +import java.util.*; + +/* Hold all data received from motes */ +class Data { + /* The mote data is stored in a flat array indexed by a mote's identifier. + A null value indicates no mote with that identifier. */ + private Node[] nodes = new Node[256]; + private Oscilloscope parent; + + Data(Oscilloscope parent) { + this.parent = parent; + } + + /* Data received from mote nodeId containing NREADINGS samples from + messageId * NREADINGS onwards. Tell parent if this is a new node. */ + void update(int nodeId, int messageId, int readings[]) { + if (nodeId >= nodes.length) { + int newLength = nodes.length * 2; + if (nodeId >= newLength) + newLength = nodeId + 1; + + Node newNodes[] = new Node[newLength]; + System.arraycopy(nodes, 0, newNodes, 0, nodes.length); + nodes = newNodes; + } + Node node = nodes[nodeId]; + if (node == null) { + nodes[nodeId] = node = new Node(nodeId); + parent.newNode(nodeId); + } + node.update(messageId, readings); + } + + /* Return value of sample x for mote nodeId, or -1 for missing data */ + int getData(int nodeId, int x) { + if (nodeId >= nodes.length || nodes[nodeId] == null) + return -1; + return nodes[nodeId].getData(x); + } + + /* Return number of last known sample on mote nodeId. Returns 0 for + unknown motes. */ + int maxX(int nodeId) { + if (nodeId >= nodes.length || nodes[nodeId] == null) + return 0; + return nodes[nodeId].maxX(); + } + + /* Return number of largest known sample on all motes (0 if there are no + motes) */ + int maxX() { + int max = 0; + + for (int i = 0; i < nodes.length; i++) + if (nodes[i] != null) { + int nmax = nodes[i].maxX(); + + if (nmax > max) + max = nmax; + } + + return max; + } +} diff --git a/apps/MultihopOscilloscopeLqi/java/Graph.java b/apps/MultihopOscilloscopeLqi/java/Graph.java new file mode 100644 index 00000000..9a42c1c8 --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/java/Graph.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2006 Intel Corporation + * All rights reserved. + * + * This file is distributed under the terms in the attached INTEL-LICENSE + * file. If you do not find these files, copies can be found by writing to + * Intel Research Berkeley, 2150 Shattuck Avenue, Suite 1300, Berkeley, CA, + * 94704. Attention: Intel License Inquiry. + */ + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.awt.font.*; +import java.awt.geom.*; +import java.util.*; + +/* Panel for drawing mote-data graphs */ +class Graph extends JPanel +{ + final static int BORDER_LEFT = 40; + final static int BORDER_RIGHT = 0; + final static int BORDER_TOP = 10; + final static int BORDER_BOTTOM = 10; + + final static int TICK_SPACING = 40; + final static int MAX_TICKS = 16; + final static int TICK_WIDTH = 10; + + final static int MIN_WIDTH = 50; + + int gx0, gx1, gy0, gy1; // graph bounds + int scale = 2; // gx1 - gx0 == MIN_WIDTH << scale + Window parent; + + /* Graph to screen coordinate conversion support */ + int height, width; + double xscale, yscale; + + void updateConversion() { + height = getHeight() - BORDER_TOP - BORDER_BOTTOM; + width = getWidth() - BORDER_LEFT - BORDER_RIGHT; + if (height < 1) + height = 1; + if (width < 1) + width = 1; + xscale = (double)width / (gx1 - gx0 + 1); + yscale = (double)height / (gy1 - gy0 + 1); + } + + Graphics makeClip(Graphics g) { + return g.create(BORDER_LEFT, BORDER_TOP, width, height); + } + + // Note that these do not include the border offset! + int screenX(int gx) { + return (int)(xscale * (gx - gx0) + 0.5); + } + + int screenY(int gy) { + return (int)(height - yscale * (gy - gy0)); + } + + int graphX(int sx) { + return (int)(sx / xscale + gx0 + 0.5); + } + + Graph(Window parent) { + this.parent = parent; + gy0 = 0; gy1 = 0xffff; + gx0 = 0; gx1 = MIN_WIDTH << scale; + } + + void rightDrawString(Graphics2D g, String s, int x, int y) { + TextLayout layout = + new TextLayout(s, parent.smallFont, g.getFontRenderContext()); + Rectangle2D bounds = layout.getBounds(); + layout.draw(g, x - (float)bounds.getWidth(), y + (float)bounds.getHeight() / 2); + } + + protected void paintComponent(Graphics g) { + Graphics2D g2d = (Graphics2D)g; + + /* Repaint. Synchronize on Oscilloscope to avoid data changing. + Simply clear panel, draw Y axis and all the mote graphs. */ + synchronized (parent.parent) { + updateConversion(); + g2d.setColor(Color.BLACK); + g2d.fillRect(0, 0, getWidth(), getHeight()); + drawYAxis(g2d); + + Graphics clipped = makeClip(g2d); + int count = parent.moteListModel.size(); + for (int i = 0; i < count; i++) { + clipped.setColor(parent.moteListModel.getColor(i)); + drawGraph(clipped, parent.moteListModel.get(i)); + } + } + } + + /* Draw the Y-axis */ + protected void drawYAxis(Graphics2D g) { + int axis_x = BORDER_LEFT - 1; + int height = getHeight() - BORDER_BOTTOM - BORDER_TOP; + + g.setColor(Color.WHITE); + g.drawLine(axis_x, BORDER_TOP, axis_x, BORDER_TOP + height - 1); + + /* Draw a reasonable set of tick marks */ + int nTicks = height / TICK_SPACING; + if (nTicks > MAX_TICKS) + nTicks = MAX_TICKS; + + int tickInterval = (gy1 - gy0 + 1) / nTicks; + if (tickInterval == 0) + tickInterval = 1; + + /* Tick interval should be of the family A * 10^B, + where A = 1, 2 * or 5. We tend more to rounding A up, to reduce + rather than increase the number of ticks. */ + int B = (int)(Math.log(tickInterval) / Math.log(10)); + int A = (int)(tickInterval / Math.pow(10, B) + 0.5); + if (A > 2) A = 5; + else if (A > 5) A = 10; + + tickInterval = A * (int)Math.pow(10, B); + + /* Ticks are printed at multiples of tickInterval */ + int tick = ((gy0 + tickInterval - 1) / tickInterval) * tickInterval; + while (tick <= gy1) { + int stick = screenY(tick) + BORDER_TOP; + rightDrawString(g, "" + tick, axis_x - TICK_WIDTH / 2 - 2, stick); + g.drawLine(axis_x - TICK_WIDTH / 2, stick, + axis_x - TICK_WIDTH / 2 + TICK_WIDTH, stick); + tick += tickInterval; + } + + } + + /* Draw graph for mote nodeId */ + protected void drawGraph(Graphics g, int nodeId) { + SingleGraph sg = new SingleGraph(g, nodeId); + + if (gx1 - gx0 >= width) // More points than pixels-iterate by pixel + for (int sx = 0; sx < width; sx++) + sg.nextPoint(g, graphX(sx), sx); + else // Less points than pixel-iterate by points + for (int gx = gx0; gx <= gx1; gx++) + sg.nextPoint(g, gx, screenX(gx)); + } + + /* Inner class to simplify drawing a graph. Simplify initialise it, then + feed it the X screen and graph coordinates, from left to right. */ + private class SingleGraph { + int lastsx, lastsy, nodeId; + + /* Start drawing the graph mote id */ + SingleGraph(Graphics g, int id) { + nodeId = id; + lastsx = -1; + lastsy = -1; + } + + /* Next point in mote's graph is at x value gx, screen coordinate sx */ + void nextPoint(Graphics g, int gx, int sx) { + int gy = parent.parent.data.getData(nodeId, gx); + int sy = -1; + + if (gy >= 0) { // Ignore missing values + double rsy = height - yscale * (gy - gy0); + + // Ignore problem values + if (rsy >= -1e6 && rsy <= 1e6) + sy = (int)(rsy + 0.5); + + if (lastsy >= 0 && sy >= 0) + g.drawLine(lastsx, lastsy, sx, sy); + } + lastsx = sx; + lastsy = sy; + } + } + + /* Update X-axis range in GUI */ + void updateXLabel() { + parent.xLabel.setText("X: " + gx0 + " - " + gx1); + } + + /* Ensure that graph is nicely positioned on screen. max is the largest + sample number received from any mote. */ + private void recenter(int max) { + // New data will show up at the 3/4 point + // The 2nd term ensures that gx1 will be >= max + int scrollby = ((gx1 - gx0) >> 2) + (max - gx1); + gx0 += scrollby; + gx1 += scrollby; + if (gx0 < 0) { // don't bother showing negative sample numbers + gx1 -= gx0; + gx0 = 0; + } + updateXLabel(); + } + + /* New data received. Redraw graph, scrolling if necessary */ + void newData() { + int max = parent.parent.data.maxX(); + + if (max > gx1 || max < gx0) // time to scroll + recenter(max); + repaint(); + } + + /* User set the X-axis scale to newScale */ + void setScale(int newScale) { + gx1 = gx0 + (MIN_WIDTH << newScale); + scale = newScale; + recenter(parent.parent.data.maxX()); + repaint(); + } + + /* User attempted to set Y-axis range to newy0..newy1. Refuse bogus + values (return false), or accept, redraw and return true. */ + boolean setYAxis(int newy0, int newy1) { + if (newy0 >= newy1 || newy0 < 0 || newy0 > 65535 || + newy1 < 0 || newy1 > 65535) + return false; + gy0 = newy0; + gy1 = newy1; + repaint(); + return true; + } +} diff --git a/apps/MultihopOscilloscopeLqi/java/Makefile b/apps/MultihopOscilloscopeLqi/java/Makefile new file mode 100644 index 00000000..55c2605b --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/java/Makefile @@ -0,0 +1,21 @@ +GEN=OscilloscopeMsg.java Constants.java + +all: oscilloscope.jar + +oscilloscope.jar: Oscilloscope.class + jar cf $@ *.class + +OscilloscopeMsg.java: ../MultihopOscilloscope.h + mig -target=null -java-classname=OscilloscopeMsg java ../MultihopOscilloscope.h oscilloscope -o $@ + +Constants.java: ../MultihopOscilloscope.h + ncg -target=null -java-classname=Constants java ../MultihopOscilloscope.h NREADINGS DEFAULT_INTERVAL -o $@ + +Oscilloscope.class: $(wildcard *.java) $(GEN) + javac *.java + +clean: + rm -f *.class $(GEN) + +veryclean: clean + rm oscilloscope.jar diff --git a/apps/MultihopOscilloscopeLqi/java/Node.java b/apps/MultihopOscilloscopeLqi/java/Node.java new file mode 100644 index 00000000..cfe8db9e --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/java/Node.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2006 Intel Corporation + * All rights reserved. + * + * This file is distributed under the terms in the attached INTEL-LICENSE + * file. If you do not find these files, copies can be found by writing to + * Intel Research Berkeley, 2150 Shattuck Avenue, Suite 1300, Berkeley, CA, + * 94704. Attention: Intel License Inquiry. + */ + +/** + * Class holding all data received from a mote. + */ +class Node { + /* Data is hold in an array whose size is a multiple of INCREMENT, and + INCREMENT itself must be a multiple of Constant.NREADINGS. This + simplifies handling the extension and clipping of old data + (see setEnd) */ + final static int INCREMENT = 100 * Constants.NREADINGS; + final static int MAX_SIZE = 100 * INCREMENT; // Must be multiple of INCREMENT + + /* The mote's identifier */ + int id; + + /* Data received from the mote. data[0] is the dataStart'th sample + Indexes 0 through dataEnd - dataStart - 1 hold data. + Samples are 16-bit unsigned numbers, -1 indicates missing data. */ + int[] data; + int dataStart, dataEnd; + + Node(int _id) { + id = _id; + } + + /* Update data to hold received samples newDataIndex .. newEnd. + If we receive data with a lower index, we discard newer data + (we assume the mote rebooted). */ + private void setEnd(int newDataIndex, int newEnd) { + if (newDataIndex < dataStart || data == null) { + /* New data is before the start of what we have. Just throw it + all away and start again */ + dataStart = newDataIndex; + data = new int[INCREMENT]; + } + if (newEnd > dataStart + data.length) { + /* Try extending first */ + if (data.length < MAX_SIZE) { + int newLength = (newEnd - dataStart + INCREMENT - 1) / INCREMENT * INCREMENT; + if (newLength >= MAX_SIZE) + newLength = MAX_SIZE; + + int[] newData = new int[newLength]; + System.arraycopy(data, 0, newData, 0, data.length); + data = newData; + + } + if (newEnd > dataStart + data.length) { + /* Still doesn't fit. Squish. + We assume INCREMENT >= (newEnd - newDataIndex), and ensure + that dataStart + data.length - INCREMENT = newDataIndex */ + int newStart = newDataIndex + INCREMENT - data.length; + + if (dataStart + data.length > newStart) + System.arraycopy(data, newStart - dataStart, data, 0, + data.length - (newStart - dataStart)); + dataStart = newStart; + } + } + /* Mark any missing data as invalid */ + for (int i = dataEnd < dataStart ? dataStart : dataEnd; + i < newDataIndex; i++) + data[i - dataStart] = -1; + + /* If we receive a count less than the old count, we assume the old + data is invalid */ + dataEnd = newEnd; + + } + + /* Data received containing NREADINGS samples from messageId * NREADINGS + onwards */ + void update(int messageId, int readings[]) { + int start = messageId * Constants.NREADINGS; + setEnd(start, start + Constants.NREADINGS); + for (int i = 0; i < readings.length; i++) + data[start - dataStart + i] = readings[i]; + } + + /* Return value of sample x, or -1 for missing data */ + int getData(int x) { + if (x < dataStart || x >= dataEnd) + return -1; + else + return data[x - dataStart]; + } + + /* Return number of last known sample */ + int maxX() { + return dataEnd - 1; + } +} diff --git a/apps/MultihopOscilloscopeLqi/java/Oscilloscope.java b/apps/MultihopOscilloscopeLqi/java/Oscilloscope.java new file mode 100644 index 00000000..3db3741f --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/java/Oscilloscope.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2006 Intel Corporation + * All rights reserved. + * + * This file is distributed under the terms in the attached INTEL-LICENSE + * file. If you do not find these files, copies can be found by writing to + * Intel Research Berkeley, 2150 Shattuck Avenue, Suite 1300, Berkeley, CA, + * 94704. Attention: Intel License Inquiry. + */ + +import net.tinyos.message.*; +import net.tinyos.util.*; +import java.io.*; + +/* The "Oscilloscope" demo app. Displays graphs showing data received from + the Oscilloscope mote application, and allows the user to: + - zoom in or out on the X axis + - set the scale on the Y axis + - change the sampling period + - change the color of each mote's graph + - clear all data + + This application is in three parts: + - the Node and Data objects store data received from the motes and support + simple queries + - the Window and Graph and miscellaneous support objects implement the + GUI and graph drawing + - the Oscilloscope object talks to the motes and coordinates the other + objects + + Synchronization is handled through the Oscilloscope object. Any operation + that reads or writes the mote data must be synchronized on Oscilloscope. + Note that the messageReceived method below is synchronized, so no further + synchronization is needed when updating state based on received messages. +*/ +public class Oscilloscope implements MessageListener +{ + MoteIF mote; + Data data; + Window window; + + /* The current sampling period. If we receive a message from a mote + with a newer version, we update our interval. If we receive a message + with an older version, we broadcast a message with the current interval + and version. If the user changes the interval, we increment the + version and broadcast the new interval and version. */ + int interval = Constants.DEFAULT_INTERVAL; + int version = -1; + + /* Main entry point */ + void run() { + data = new Data(this); + window = new Window(this); + window.setup(); + mote = new MoteIF(PrintStreamMessenger.err); + mote.registerListener(new OscilloscopeMsg(), this); + } + + /* The data object has informed us that nodeId is a previously unknown + mote. Update the GUI. */ + void newNode(int nodeId) { + window.newNode(nodeId); + } + + synchronized public void messageReceived(int dest_addr, Message msg) { + if (msg instanceof OscilloscopeMsg) { + OscilloscopeMsg omsg = (OscilloscopeMsg)msg; + + /* Update interval and mote data */ + periodUpdate(omsg.get_version(), omsg.get_interval()); + data.update(omsg.get_id(), omsg.get_count(), omsg.get_readings()); + + /* Inform the GUI that new data showed up */ + window.newData(); + } + } + + /* A potentially new version and interval has been received from the + mote */ + void periodUpdate(int moteVersion, int moteInterval) { + if (moteVersion > version) { + /* It's new. Update our vision of the interval. */ + version = moteVersion; + interval = moteInterval; + window.updateSamplePeriod(); + } + else if (moteVersion < version) { + /* It's old. Update the mote's vision of the interval. */ + sendInterval(); + } + } + + /* The user wants to set the interval to newPeriod. Refuse bogus values + and return false, or accept the change, broadcast it, and return + true */ + synchronized boolean setInterval(int newPeriod) { + if (newPeriod < 1 || newPeriod > 65535) + return false; + interval = newPeriod; + version++; + sendInterval(); + return true; + } + + /* Broadcast a version+interval message. */ + void sendInterval() { + OscilloscopeMsg omsg = new OscilloscopeMsg(); + + omsg.set_version(version); + omsg.set_interval(interval); + try { + mote.send(MoteIF.TOS_BCAST_ADDR, omsg); + } + catch (IOException e) { + window.error("Cannot send message to mote"); + } + } + + /* User wants to clear all data. */ + void clear() { + data = new Data(this); + } + + public static void main(String[] args) { + Oscilloscope me = new Oscilloscope(); + me.run(); + } +} diff --git a/apps/MultihopOscilloscopeLqi/java/Window.java b/apps/MultihopOscilloscopeLqi/java/Window.java new file mode 100644 index 00000000..d7979bf9 --- /dev/null +++ b/apps/MultihopOscilloscopeLqi/java/Window.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2006 Intel Corporation + * All rights reserved. + * + * This file is distributed under the terms in the attached INTEL-LICENSE + * file. If you do not find these files, copies can be found by writing to + * Intel Research Berkeley, 2150 Shattuck Avenue, Suite 1300, Berkeley, CA, + * 94704. Attention: Intel License Inquiry. + */ + +import javax.swing.*; +import javax.swing.table.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +/* The main GUI object. Build the GUI and coordinate all user activities */ +class Window +{ + Oscilloscope parent; + Graph graph; + + Font smallFont = new Font("Dialog", Font.PLAIN, 8); + Font boldFont = new Font("Dialog", Font.BOLD, 12); + Font normalFont = new Font("Dialog", Font.PLAIN, 12); + MoteTableModel moteListModel; // GUI view of mote list + JLabel xLabel; // Label displaying X axis range + JTextField sampleText, yText; // inputs for sample period and Y axis range + JFrame frame; + + Window(Oscilloscope parent) { + this.parent = parent; + } + + /* A model for the mote table, and general utility operations on the mote + list */ + class MoteTableModel extends AbstractTableModel { + private ArrayList motes = new ArrayList(); + private ArrayList colors = new ArrayList(); + + /* Initial mote colors cycle through this list. Add more colors if + you want. */ + private Color[] cycle = { + Color.RED, Color.WHITE, Color.GREEN, Color.MAGENTA, + Color.YELLOW, Color.GRAY, Color.YELLOW + }; + int cycleIndex; + + /* TableModel methods for achieving our table appearance */ + public String getColumnName(int col) { + if (col == 0) + return "Mote"; + else + return "Color"; + } + public int getColumnCount() { return 2; } + public synchronized int getRowCount() { return motes.size(); } + public synchronized Object getValueAt(int row, int col) { + if (col == 0) + return motes.get(row); + else + return colors.get(row); + } + public Class getColumnClass(int col) { + return getValueAt(0, col).getClass(); + } + public boolean isCellEditable(int row, int col) { return col == 1; } + public synchronized void setValueAt(Object value, int row, int col) { + colors.set(row, value); + fireTableCellUpdated(row, col); + graph.repaint(); + } + + /* Return mote id of i'th mote */ + int get(int i) { return ((Integer)motes.get(i)).intValue(); } + + /* Return color of i'th mote */ + Color getColor(int i) { return (Color)colors.get(i); } + + /* Return number of motes */ + int size() { return motes.size(); } + + /* Add a new mote */ + synchronized void newNode(int nodeId) { + /* Shock, horror. No binary search. */ + int i, len = motes.size(); + + for (i = 0; ; i++) + if (i == len || nodeId < get(i)) { + motes.add(i, new Integer(nodeId)); + // Cycle through a set of initial colors + colors.add(i, cycle[cycleIndex++ % cycle.length]); + break; + } + fireTableRowsInserted(i, i); + } + + /* Remove all motes */ + void clear() { + motes = new ArrayList(); + colors = new ArrayList(); + fireTableDataChanged(); + } + } + + /* A simple full-color cell */ + static class MoteColor extends JLabel implements TableCellRenderer { + public MoteColor() { setOpaque(true); } + public Component getTableCellRendererComponent + (JTable table, Object color, + boolean isSelected, boolean hasFocus, int row, int column) { + setBackground((Color)color); + return this; + } + } + + /* Convenience methods for making buttons, labels and textfields. + Simplifies code and ensures a consistent style. */ + + JButton makeButton(String label, ActionListener action) { + JButton button = new JButton(); + button.setText(label); + button.setFont(boldFont); + button.addActionListener(action); + return button; + } + + JLabel makeLabel(String txt, int alignment) { + JLabel label = new JLabel(txt, alignment); + label.setFont(boldFont); + return label; + } + + JLabel makeSmallLabel(String txt, int alignment) { + JLabel label = new JLabel(txt, alignment); + label.setFont(smallFont); + return label; + } + + JTextField makeTextField(int columns, ActionListener action) { + JTextField tf = new JTextField(columns); + tf.setFont(normalFont); + tf.setMaximumSize(tf.getPreferredSize()); + tf.addActionListener(action); + return tf; + } + + /* Build the GUI */ + void setup() { + JPanel main = new JPanel(new BorderLayout()); + + main.setMinimumSize(new Dimension(500, 250)); + main.setPreferredSize(new Dimension(800, 400)); + + // Three panels: mote list, graph, controls + moteListModel = new MoteTableModel(); + JTable moteList = new JTable(moteListModel); + moteList.setDefaultRenderer(Color.class, new MoteColor()); + moteList.setDefaultEditor(Color.class, new ColorCellEditor("Pick Mote Color")); + moteList.setPreferredScrollableViewportSize(new Dimension(100, 400)); + JScrollPane motePanel = new JScrollPane(); + motePanel.getViewport().add(moteList, null); + main.add(motePanel, BorderLayout.WEST); + + graph = new Graph(this); + main.add(graph, BorderLayout.CENTER); + + // Controls. Organised using box layouts. + + // Sample period. + JLabel sampleLabel = makeLabel("Sample period (ms):", JLabel.RIGHT); + sampleText = makeTextField(6, new ActionListener() { + public void actionPerformed(ActionEvent e) { setSamplePeriod(); } + } ); + updateSamplePeriod(); + + // Clear data. + JButton clearButton = makeButton("Clear data", new ActionListener() { + public void actionPerformed(ActionEvent e) { clearData(); } + } ); + + // Adjust X-axis zoom. + Box xControl = new Box(BoxLayout.Y_AXIS); + xLabel = makeLabel("", JLabel.CENTER); + final JSlider xSlider = new JSlider(JSlider.HORIZONTAL, 0, 8, graph.scale); + Hashtable xTable = new Hashtable(); + for (int i = 0; i <= 8; i += 2) + xTable.put(new Integer(i), + makeSmallLabel("" + (Graph.MIN_WIDTH << i), + JLabel.CENTER)); + xSlider.setLabelTable(xTable); + xSlider.setPaintLabels(true); + graph.updateXLabel(); + graph.setScale(graph.scale); + xSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + //if (!xSlider.getValueIsAdjusting()) + graph.setScale((int)xSlider.getValue()); + } + }); + xControl.add(xLabel); + xControl.add(xSlider); + + // Adjust Y-axis range. + JLabel yLabel = makeLabel("Y:", JLabel.RIGHT); + yText = makeTextField(12, new ActionListener() { + public void actionPerformed(ActionEvent e) { setYAxis(); } + } ); + yText.setText(graph.gy0 + " - " + graph.gy1); + + Box controls = new Box(BoxLayout.X_AXIS); + controls.add(clearButton); + controls.add(Box.createHorizontalGlue()); + controls.add(Box.createRigidArea(new Dimension(20, 0))); + controls.add(sampleLabel); + controls.add(sampleText); + controls.add(Box.createHorizontalGlue()); + controls.add(Box.createRigidArea(new Dimension(20, 0))); + controls.add(xControl); + controls.add(yLabel); + controls.add(yText); + main.add(controls, BorderLayout.SOUTH); + + // The frame part + frame = new JFrame("Oscilloscope"); + frame.setSize(main.getPreferredSize()); + frame.getContentPane().add(main); + frame.setVisible(true); + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { System.exit(0); } + }); + } + + /* User operation: clear data */ + void clearData() { + synchronized (parent) { + moteListModel.clear(); + parent.clear(); + graph.newData(); + } + } + + /* User operation: set Y-axis range. */ + void setYAxis() { + String val = yText.getText(); + + try { + int dash = val.indexOf('-'); + if (dash >= 0) { + String min = val.substring(0, dash).trim(); + String max = val.substring(dash + 1).trim(); + + if (!graph.setYAxis(Integer.parseInt(min), Integer.parseInt(max))) + error("Invalid range " + min + " - " + max + " (expected values between 0 and 65535)"); + return; + } + } + catch (NumberFormatException e) { } + error("Invalid range " + val + " (expected NN-MM)"); + } + + /* User operation: set sample period. */ + void setSamplePeriod() { + String periodS = sampleText.getText().trim(); + try { + int newPeriod = Integer.parseInt(periodS); + if (parent.setInterval(newPeriod)) + return; + } + catch (NumberFormatException e) { } + error("Invalid sample period " + periodS); + } + + /* Notification: sample period changed. */ + void updateSamplePeriod() { + sampleText.setText("" + parent.interval); + } + + /* Notification: new node. */ + void newNode(int nodeId) { + moteListModel.newNode(nodeId); + } + + /* Notification: new data. */ + void newData() { + graph.newData(); + } + + void error(String msg) { + JOptionPane.showMessageDialog(frame, msg, "Error", + JOptionPane.ERROR_MESSAGE); + } +} diff --git a/apps/MultihopOscilloscopeLqi/java/oscilloscope.jar b/apps/MultihopOscilloscopeLqi/java/oscilloscope.jar new file mode 100644 index 0000000000000000000000000000000000000000..28eb3b4e536ce46b85bf677e5f9d5f4ddb742a20 GIT binary patch literal 18901 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