Changeset 346:d620b2af47e4 in finroc_tools_gui-java


Ignore:
Timestamp:
10.01.2019 17:09:25 (2 months ago)
Author:
Max Reichardt <max.reichardt@…>
Branch:
default
Phase:
public
Message:

Significantly revises Oscilloscope widget - with updates including:

  • Different timestamp modes - in particular support for using value-attached timestamps (for precise high-resolution plots - and for supporting distorted/scaled time possibly from simulation)
  • Support for sub-millisecond timing
  • Option to limit ramp/interpolation length (e.g. for values published on change only ramp should not be longer than cycle time - and events may have stairs-like representation)
  • Highly efficient lock-free implementation that no longer creates additional thread (locks are only used internal ring buffer size needs to be increased)
  • Display of status information
  • Cleaner rendering
File:
1 edited

Legend:

Unmodified
Added
Removed
  • widgets/Oscilloscope.java

    r344 r346  
    2727import java.awt.Graphics; 
    2828import java.awt.Graphics2D; 
     29import java.awt.event.ActionEvent; 
    2930import java.awt.event.ActionListener; 
    3031import java.awt.event.MouseEvent; 
    3132import java.awt.event.MouseListener; 
     33import java.awt.geom.Rectangle2D; 
    3234import java.io.Serializable; 
    3335import java.util.ArrayList; 
    3436import java.util.List; 
     37import java.util.concurrent.atomic.AtomicLong; 
    3538 
    3639import javax.swing.BorderFactory; 
     
    3841import javax.swing.JPanel; 
    3942import javax.swing.SwingConstants; 
    40  
     43import javax.swing.Timer; 
     44 
     45import org.finroc.core.FrameworkElementFlags; 
     46import org.finroc.core.port.AbstractPort; 
     47import org.finroc.core.port.PortCreationInfo; 
     48import org.finroc.core.port.std.PortBase; 
     49import org.finroc.core.port.std.PortDataManager; 
     50import org.finroc.tools.gui.GUIPanel; 
    4151import org.finroc.tools.gui.Widget; 
    4252import org.finroc.tools.gui.WidgetInput; 
     
    4959import org.finroc.tools.gui.util.gui.RulerOfTheForest; 
    5060import org.finroc.tools.gui.util.propertyeditor.PropertyList; 
    51 import org.rrlib.finroc_core_utils.jc.thread.LoopThread; 
    52 import org.rrlib.logging.Log; 
    53 import org.rrlib.logging.LogLevel; 
    54  
    55 import org.finroc.plugins.data_types.PartWiseLinearFunction; 
    5661import org.rrlib.serialization.NumericRepresentation; 
    5762import org.rrlib.serialization.rtti.DataType; 
    5863import org.rrlib.serialization.rtti.DataTypeBase; 
    59 import org.finroc.core.FrameworkElementFlags; 
    60 import org.finroc.core.datatype.CoreNumber; 
    61 import org.finroc.core.port.AbstractPort; 
    62 import org.finroc.core.port.PortCreationInfo; 
    63 import org.finroc.core.port.std.PortBase; 
    64 import org.finroc.core.port.std.PortDataManager; 
    6564 
    6665public class Oscilloscope extends Widget { 
     
    6968    private static final long serialVersionUID = 1892231028811778314L; 
    7069 
     70    public enum ClockAndTimestampMode { 
     71        AUTO_SELECT,                        // Auto-selects mode: currently ATTACHED_TIMESTAMPS if all inputs have timestamps attached, otherwise FINGUI_VALUE_RECEIVE_TIMESTAMPS 
     72        USE_ATTACHED_TIMESTAMPS,            // Uses timestamps attached to values only (remote clock). Inputs that do not have timestamps attached are not displayed. 
     73        USE_LOCAL_TIME_ON_VALUE_RECEIVE,    // Uses local (fingui) clock time when receiving values as their timestamps. Classic behavior - somewhat imprecise on weak WLAN connections 
     74    } 
     75 
     76    public ClockAndTimestampMode clockAndTimestampMode = ClockAndTimestampMode.AUTO_SELECT; 
    7177    public WidgetPorts<WidgetInput.Numeric> signals = new WidgetPorts<WidgetInput.Numeric>("signal", 1, WidgetInput.Numeric.class, this); 
    7278    public double timeScaleMaxInMs = 10000; 
     
    7783    public PropertyList<OscilloscopeSignal> channels = new PropertyList<OscilloscopeSignal>(OscilloscopeSignal.class, 25); 
    7884    public long timerIntervalInMs = 100; 
    79  
    80     private static final double EPSILON = 0.0000001; 
     85    public long maxRampLengthDefaultInMs = 40;          // Maximum length of interpolation (ramp) to new value; relevant when values are published on change only; e.g. for state changes, zero can make sense (-> stairs-like representation); can be overridden per signal 
     86 
     87    /** 
     88     * Maximum network latency to consider -> only relevant for distributing values in mode USE_LOCAL_TIME_ON_VALUE_RECEIVE 
     89     * TODO: observe network connection for more accurate distributions/estimations (that's why this is not a widget parameter) 
     90     */ 
     91    private static final transient long MAX_NETWORK_LATENCY_TO_CONSIDER = 500 * 1000000; // 
     92 
    8193 
    8294    public Oscilloscope() { 
     
    97109    protected PortCreationInfo getPortCreationInfo(PortCreationInfo suggestion, WidgetPort<?> forPort) { 
    98110        if (signals != null && signals.contains(forPort)) { 
     111            // Notably queues are required for signal ports so that observed parts buffer values 
    99112            PortCreationInfo info = suggestion.derive(suggestion.flags | FrameworkElementFlags.HAS_QUEUE | FrameworkElementFlags.USES_QUEUE); 
    100             info.maxQueueSize = -1; 
     113            info.maxQueueSize = 1000;  // In order to be 100% safe with respect to memory leaks (actually, port listener should empty queues immediately on receiving new values) 
    101114            return info; 
    102115        } 
    103116        return suggestion; 
     117    } 
     118 
     119    @Override 
     120    public void restore(GUIPanel parent) { 
     121        if (clockAndTimestampMode == null) { 
     122            clockAndTimestampMode = ClockAndTimestampMode.AUTO_SELECT; 
     123        } 
     124        super.restore(parent); 
    104125    } 
    105126 
     
    115136        public Scale useScale = Scale.left; 
    116137        public Color color = getDefaultColor(Theme.DefaultColor.OSCILLOSCOPE_FOREGROUND); 
     138        public String maxRampLengthInMs; 
    117139    } 
    118140 
    119     class OscilloscopeUI extends WidgetUI implements WidgetPortsListener, MouseListener { 
     141    class OscilloscopeUI extends WidgetUI implements WidgetPortsListener, MouseListener, ActionListener { 
    120142 
    121143        /** UID */ 
     
    129151        JLabel label = new JLabel(); 
    130152        boolean pauseMode; 
    131  
    132         OscilloscopeThread thread; 
     153        long pauseTimestamp; 
     154 
     155        Timer timer; 
     156        long firstDisplayedTimestamp = 0;                     // Oscilloscope started displaying at this timestamp (in ns) 
    133157 
    134158        public OscilloscopeUI() { 
     
    156180            add(main, BorderLayout.CENTER); 
    157181 
    158             // create thread 
    159             thread = new OscilloscopeThread(); 
    160             thread.start(); 
    161             Log.log(LogLevel.DEBUG, this, "Oscilloscope Thread started."); 
     182            timer = new Timer((int)timerIntervalInMs, this); 
     183            timer.setRepeats(true); 
     184            timer.start(); 
    162185 
    163186            signals.addChangeListener(this); 
     
    168191        @Override 
    169192        public void portChanged(WidgetPorts<?> origin, AbstractPort port, Object value) { 
    170             this.setChanged(); 
    171             repaint(); 
     193            //System.out.println("Port Changed: " + value); 
     194 
     195            try { 
     196                // Update functions concurrently 
     197                int index = signals.indexOf(port); 
     198                if (index >= 0) { 
     199                    if (!pauseMode) { 
     200                        OscilloscopeFunction function = functions.get(index); 
     201 
     202                        PortDataManager manager = PortDataManager.getManager(value); 
     203                        long timestamp = manager.getTimestamp().longValue(); 
     204                        double yValue = ((NumericRepresentation)value).getNumericRepresentation().doubleValue(); 
     205 
     206                        int result = function.addNewValue(yValue, timestamp); 
     207                        if (result != OscilloscopeFunction.ENQUEUED) { 
     208                            OscilloscopeFunction newFunction = result == OscilloscopeFunction.REALLOCATE_EMPTY ? new OscilloscopeFunction(manager) : new OscilloscopeFunction(function);  // reallocate 
     209                            newFunction.addNewValue(yValue, timestamp); 
     210                            functions.set(index, newFunction); 
     211                        } 
     212                    } 
     213 
     214                    // Empty queue - as values are no longer needed 
     215                    PortBase p = (PortBase)port; 
     216                    PortDataManager m = p.dequeueSingleUnsafeRaw(); 
     217                    while (m != null) { 
     218                        m.releaseLock(); 
     219                        m = p.dequeueSingleUnsafeRaw(); 
     220                    } 
     221                } 
     222            } catch (Exception e) { 
     223                e.printStackTrace(); 
     224            } 
     225        } 
     226 
     227        @Override 
     228        public void actionPerformed(ActionEvent e) { 
     229            if (isVisible()) { 
     230                main.repaint(); 
     231            } 
     232            for (OscilloscopeFunction function : functions) { // Discard old values - independent of drawing (e.g. for inactive tabs) 
     233                function.updateThreadLocalVariablesForDrawing(); 
     234            } 
     235 
     236            // TODO optimization: decrease timer rate for oscilloscopes on inactive tabs (for less events) 
     237        } 
     238 
     239        @Override 
     240        protected void dispose() { 
     241            timer.stop(); 
     242            super.dispose(); 
    172243        } 
    173244 
    174245        @Override 
    175246        protected void paintChildren(Graphics g) { 
     247            //System.out.println("Test"); 
    176248            skip = true; 
    177249            super.paintChildren(g); 
     
    187259            left.setMinAndMax(leftScaleMin, leftScaleMax); 
    188260            right.setMinAndMax(rightScaleMin, rightScaleMax); 
    189             thread.setCycleTime(timerIntervalInMs); 
     261            timer.setDelay((int)timerIntervalInMs); 
    190262            synchronized (functions) { 
    191263                signals.setSize(channels.size()); 
    192264                initPorts(); 
    193265                while (functions.size() < channels.size()) { 
    194                     functions.add(new OscilloscopeFunction()); 
     266                    functions.add(null); 
    195267                } 
    196268                while (functions.size() > channels.size()) { 
    197269                    functions.remove(functions.size() - 1); 
     270                } 
     271 
     272                // Reallocate all functions 
     273                for (int i = 0; i < functions.size(); i++) { 
     274                    PortBase port = (PortBase)signals.get(i).getPort(); 
     275                    PortDataManager buffer = port.getLockedUnsafeRaw(true); 
     276                    functions.set(i, new OscilloscopeFunction(buffer)); 
     277                    buffer.releaseLock(); 
    198278                } 
    199279            } 
     
    233313        public void mouseClicked(MouseEvent e) { 
    234314            this.pauseMode = !this.pauseMode; 
     315            if (this.pauseMode) { 
     316                pauseTimestamp = System.currentTimeMillis(); 
     317            } 
    235318        } 
    236319 
     
    247330        public void mouseExited(MouseEvent e) {} 
    248331 
    249         class OscilloscopeThread extends LoopThread { 
    250  
    251             long startTime, lastTime; 
    252             ArrayList<PortDataManager> dequeuedValues = new ArrayList<PortDataManager>(); 
    253  
    254             public OscilloscopeThread() { 
    255                 super(timerIntervalInMs, false); 
    256                 startTime = System.currentTimeMillis(); 
    257                 lastTime = startTime; 
    258             } 
    259  
    260             @Override 
    261             public void mainLoopCallback() throws Exception { 
    262                 if (!isVisible()) { 
    263                     stopLoop(); // disposed 
    264                     Log.log(LogLevel.DEBUG, this, "Oscilloscope Thread stopped."); 
    265                     return; 
    266                 } 
    267  
    268                 if (pauseMode) { 
    269                     for (int i = 0; i < functions.size(); i++) { 
    270                         ((PortBase)signals.get(i).getPort()).dequeueSingleAutoLockedRaw(); 
    271                     } 
    272                     releaseAllLocks(); 
    273                     return; 
    274                 } 
    275  
    276                 try { 
    277                     synchronized (functions) { 
    278                         long time = System.currentTimeMillis(); 
    279                         for (int i = 0; i < functions.size(); i++) { 
    280                             dequeuedValues.clear(); 
    281                             PortDataManager dequeued = null; 
    282                             while ((dequeued = ((PortBase)signals.get(i).getPort()).dequeueSingleAutoLockedRaw()) != null) { 
    283                                 dequeuedValues.add(dequeued); 
    284                             } 
    285                             if (dequeuedValues.size() > 0) { 
    286                                 // TODO: We could use timestamps attached to values if available - however, this would make things a lot more complex 
    287                                 double timestep = (time - lastTime) / ((double)dequeuedValues.size()); 
    288                                 //System.out.println("Received " + dequeuedValues.size() + " values - timestep " + timestep); 
    289                                 double lastValueTime = lastTime; 
    290                                 for (int j = 0; j < dequeuedValues.size(); j++) { 
    291                                     double valueTime = lastTime + (j + 1) * timestep; 
    292                                     // TODO: an implementation without EPSILON should be possible - and would be cleaner 
    293                                     functions.get(i).addNewValue((valueTime - startTime) % timeScaleMaxInMs, ((NumericRepresentation)dequeuedValues.get(j).getObject().getData()).getNumericRepresentation().doubleValue(), (lastValueTime - startTime + EPSILON) % timeScaleMaxInMs); 
    294                                     lastValueTime = valueTime; 
    295                                 } 
    296                             } else { 
    297                                 functions.get(i).addNewValue((time - startTime) % timeScaleMaxInMs, signals.get(i).getDouble(), (lastTime - startTime + EPSILON) % timeScaleMaxInMs); 
    298                             } 
    299                             releaseAllLocks(); 
    300                         } 
    301                         lastTime = time; 
    302                     } 
    303                     main.repaint(); 
    304                 } catch (Exception e) { 
    305                     Log.log(LogLevel.DEBUG_WARNING, this, "Oscilloscope Thread skipped loop, because of temporary exception"); 
    306                 } 
    307             } 
    308         } 
    309332 
    310333        class OscilloscopeMainPanel extends JPanel { 
     
    319342            @Override 
    320343            protected void paintComponent(Graphics g) { 
     344                //System.out.println(System.currentTimeMillis() + " " + this.isOpaque()); 
     345 
    321346                if (skip) { 
    322347                    return; 
     
    351376                } 
    352377 
     378                // Determine drawing mode 
     379                ClockAndTimestampMode currentTimestampMode = Oscilloscope.this.clockAndTimestampMode; 
     380                if (currentTimestampMode == ClockAndTimestampMode.AUTO_SELECT) { 
     381                    boolean allAttached = true; 
     382                    for (OscilloscopeFunction function : functions) { 
     383                        allAttached &= function.attachedTimestamps; 
     384                    } 
     385                    currentTimestampMode = allAttached ? ClockAndTimestampMode.USE_ATTACHED_TIMESTAMPS : ClockAndTimestampMode.USE_LOCAL_TIME_ON_VALUE_RECEIVE; 
     386                } 
     387                boolean useAttachedTimestamps = currentTimestampMode == ClockAndTimestampMode.USE_ATTACHED_TIMESTAMPS; 
     388 
     389                // Update/fix values for drawing 
     390                for (OscilloscopeFunction function : functions) { 
     391                    function.updateThreadLocalVariablesForDrawing(); 
     392                } 
     393 
     394                // find latest timestamp in functions 
     395                long latestTimestamp = 0; 
     396                long earliestTimestamp = Long.MAX_VALUE; 
     397                for (OscilloscopeFunction function : functions) { 
     398                    if (function.size() > 0) { 
     399                        long[] timestampArray = useAttachedTimestamps ? function.timestampsAttached : function.timestampsReceiveTime; 
     400                        latestTimestamp = Math.max(latestTimestamp, timestampArray[function.getArrayIndex(function.size() - 1)]); 
     401                        earliestTimestamp = Math.min(earliestTimestamp, timestampArray[function.getArrayIndex(0)]); 
     402                    } 
     403                } 
     404                long now = (pauseMode ? pauseTimestamp : System.currentTimeMillis()) * 1000000; 
     405                if (!useAttachedTimestamps) { 
     406                    latestTimestamp = Math.max(latestTimestamp, now - MAX_NETWORK_LATENCY_TO_CONSIDER); 
     407                } 
     408 
     409                if (earliestTimestamp < Long.MAX_VALUE) { 
     410                    firstDisplayedTimestamp = firstDisplayedTimestamp == 0 ? earliestTimestamp : Math.min(earliestTimestamp, firstDisplayedTimestamp); 
     411                } 
     412 
     413                // determine first timestamp to display 
     414                long timeScaleMaxInNs = ((long)(timeScaleMaxInMs)) * 1000000; 
     415                long minTimestampToDisplay = Math.max(firstDisplayedTimestamp, latestTimestamp - timeScaleMaxInNs); 
     416 
    353417                // draw functions 
    354                 synchronized (functions) { 
    355                     for (int i = 0; i < functions.size(); i++) { 
    356                         List<PartWiseLinearFunction.Node> nodes = functions.get(i).getNodeList(); 
    357                         g2d.setColor(channels.get(i).color); 
    358  
    359                         PartWiseLinearFunction.Node last = null; 
     418                for (int i = 0; i < functions.size(); i++) { 
     419                    OscilloscopeFunction function = functions.get(i); 
     420                    g2d.setColor(channels.get(i).color); 
     421 
     422                    // Determine max ramp length 
     423                    long maxRampLength = maxRampLengthDefaultInMs; 
     424                    String rampLengthString = channels.get(i).maxRampLengthInMs; 
     425                    if (rampLengthString != null && rampLengthString.length() > 0) { 
     426                        try { 
     427                            maxRampLength = Long.parseLong(rampLengthString); 
     428                        } catch (Exception e) {} 
     429                    } 
     430                    maxRampLength = Math.max(0, maxRampLength * 1000000); 
     431                    long maxWaitForValues = maxRampLength + MAX_NETWORK_LATENCY_TO_CONSIDER; 
     432 
     433                    if (function.size() >= 1) { 
     434                        int firstIndex = 0; 
     435                        long firstTimestamp = minTimestampToDisplay; // first displayed timestamp 
    360436                        boolean left = channels.get(i).useScale.equals(OscilloscopeSignal.Scale.left); 
    361                         for (PartWiseLinearFunction.Node node : nodes) { 
    362                             int xnow = (int)Math.round(getXCord(node.x)); 
    363                             int ynow = (int)Math.round(getYCord(node.y, left)); 
    364                             if (channels.get(i).drawMode.equals(OscilloscopeSignal.DrawMode.lines)) { 
    365                                 if (last != null) { 
    366                                     int xlast = (int)Math.round(getXCord(last.x)); 
    367                                     int ylast = (int)Math.round(getYCord(last.y, left)); 
    368                                     g2d.drawLine(xlast, ylast, xnow, ynow); 
     437                        boolean drawLines = channels.get(i).drawMode.equals(OscilloscopeSignal.DrawMode.lines); 
     438                        long[] timestampsNanoSeconds = useAttachedTimestamps ? function.timestampsAttached : function.timestampsReceiveTime; 
     439 
     440                        while (firstIndex < function.size() && timestampsNanoSeconds[function.getArrayIndex(firstIndex)] < firstTimestamp) { 
     441                            firstIndex++; 
     442                        } 
     443 
     444                        double lastValue = 0; 
     445                        if (firstIndex >= function.size()) { 
     446                            lastValue = function.values[function.getArrayIndex(function.size() - 1)]; 
     447                        } else if (timestampsNanoSeconds[function.getArrayIndex(firstIndex)] == firstTimestamp) { 
     448                            lastValue = function.values[function.getArrayIndex(firstIndex)]; 
     449                            firstIndex++; 
     450                        } else { 
     451                            lastValue = function.getExactValueAtTimestamp(firstTimestamp, useAttachedTimestamps, maxRampLength); 
     452                        } 
     453                        long lastTimestamp = firstTimestamp; 
     454                        int lastX = getXCord(lastTimestamp, timeScaleMaxInNs); 
     455                        int lastY = (int)Math.round(getYCord(lastValue, left)); 
     456 
     457                        if (!drawLines) { 
     458                            g2d.drawLine(lastX, lastY, lastX, lastY); 
     459                        } 
     460                        boolean extendLine = false; 
     461                        if (drawLines && (!useAttachedTimestamps)) { 
     462                            long timestamp = timestampsNanoSeconds[function.getArrayIndex(function.size() - 1)]; 
     463                            extendLine = timestamp < now - maxWaitForValues; 
     464                        } 
     465 
     466                        for (int index = firstIndex; index < function.size() + (extendLine ? 1 : 0); index++) { 
     467                            boolean lineExtension = index >= function.size(); 
     468                            double currentValue = lineExtension ? lastValue : function.values[function.getArrayIndex(index)]; 
     469                            long currentTimestamp = lineExtension ? (now - maxWaitForValues) : timestampsNanoSeconds[function.getArrayIndex(index)]; 
     470 
     471                            // Limit ramp length? (-> virtually insert point) 
     472                            if (drawLines && currentValue != lastValue && currentTimestamp - lastTimestamp > maxRampLength) { 
     473                                //System.out.println("Limit " + index + " " + lastValue); 
     474                                currentValue = lastValue; 
     475                                currentTimestamp = currentTimestamp - maxRampLength; 
     476                                index--; 
     477                            } 
     478 
     479                            int xnow = getXCord(currentTimestamp, timeScaleMaxInNs); 
     480                            int ynow = (int)Math.round(getYCord(currentValue, left)); 
     481 
     482                            if (!drawLines) { 
     483                                g2d.drawLine(xnow, ynow, xnow, ynow); 
     484                            } else { 
     485 
     486                                assert(xnow <= getWidth()); 
     487                                if (xnow >= lastX) { 
     488                                    g2d.drawLine(lastX, lastY, xnow, ynow); 
     489                                } else { 
     490                                    // wrap-around 
     491                                    int width = getWidth(); 
     492                                    int virtualXNow = xnow + width; 
     493                                    int xDiff = virtualXNow - lastX; 
     494                                    int yDiff = ynow - lastY; 
     495                                    if (width - lastX > 1) { // if last line end was not at edge anyway 
     496                                        double interpolator = ((double)(width - 1) - lastX) / xDiff; 
     497                                        double y = lastY + interpolator * yDiff; 
     498                                        //System.out.println("Draw Line 1: " + lastX + " " + (width - 1)); 
     499                                        g2d.drawLine(lastX, lastY, width - 1, (int)y); 
     500                                    } 
     501                                    if (xnow > 0) { // if next line begin is not at edge anyway 
     502                                        double interpolator = ((double)width - lastX) / xDiff; 
     503                                        double y = lastY + interpolator * yDiff; 
     504                                        //System.out.println("Draw Line 2: " + 0 + " " + xnow); 
     505                                        g2d.drawLine(0, (int)y, xnow, ynow); 
     506                                    } 
    369507                                } 
    370                                 last = node; 
    371                                 if (node.x == functions.get(i).curTime) { 
    372                                     last = null; 
    373                                 } 
    374                             } else { 
    375                                 g2d.drawLine(xnow, ynow, xnow, ynow); 
    376508                            } 
     509 
     510                            lastX = xnow; 
     511                            lastY = ynow; 
     512                            lastValue = currentValue; 
     513                            lastTimestamp = currentTimestamp; 
    377514                        } 
    378515                    } 
    379516                } 
     517 
     518                // Display status info (clock / timestamps / pause) 
     519                // Use color of first function 
     520                Color statusColor = channels.size() > 0 ? channels.get(0).color.darker() : Color.WHITE; 
     521                g2d.setColor(statusColor); 
     522                String statusString = "Status: " + (pauseMode ? "Paused" : "Active") + ", Clock: " + (useAttachedTimestamps ? "Remote" : "Local") + ", Timestamps: " + (useAttachedTimestamps ? "Value-Attached" : "Receive Time"); 
     523                Rectangle2D bounds = g2d.getFontMetrics().getStringBounds(statusString, g2d); 
     524                //g2d.drawString(statusString, getWidth() - ((int)bounds.getWidth() + 3), getHeight() - 4); 
     525                //g2d.drawString(statusString, getWidth() - ((int)bounds.getWidth() + 3), getHeight() - 4); 
     526                //g2d.drawString(statusString, (getWidth() - ((int)bounds.getWidth())) / 2, getHeight() - 4); 
     527                g2d.drawString(statusString, (getWidth() - ((int)bounds.getWidth())) / 2, 12); 
    380528            } 
    381529 
     
    387535                    yRel = (y - rightScaleMin) / (rightScaleMax - rightScaleMin); 
    388536                } 
    389                 return ((double)(getHeight() - 1)) * (1 - yRel); 
    390             } 
    391  
    392             public double getXCord(double x) { 
    393                 double xRel = x / timeScaleMaxInMs; 
    394                 return ((double)getWidth()) * xRel; 
    395             } 
    396         } 
    397  
    398         class OscilloscopeFunction extends PartWiseLinearFunction { 
    399  
    400             public double curTime = -100; 
    401  
    402             /** UID */ 
    403             private static final long serialVersionUID = -4657542107242253198L; 
    404  
    405             public void addNewValue(double time, double d, double lastTime) { 
    406  
    407                 if (time > lastTime) { 
    408                     // remove entries between lastTime and time 
    409                     int idx = getInsertIndex(lastTime); 
    410                     while (idx < nodes.size() && nodes.get(idx).x <= time) { 
    411                         //System.out.println("removing " + time + " " + lastTime); 
    412                         nodes.remove(idx); 
    413                     } 
    414  
    415                     // add new entry 
    416                     //System.out.println("adding " + nodes.size()); 
    417                     addEntry(time, d); 
    418                 } else { 
    419                     // remove entries between lastTime and time 
    420                     int idx = getInsertIndex(lastTime); 
    421                     while (nodes.size() > idx) { 
    422                         nodes.remove(nodes.size() - 1); 
    423                     } 
    424                     while (nodes.size() > 0 && nodes.get(0).x <= time) { 
    425                         nodes.remove(0); 
    426                     } 
    427  
    428                     // add new entries 
    429                     addEntry(time + timeScaleMaxInMs, d); 
    430                     addEntry(time, d); 
    431                     addEntry(lastTime - timeScaleMaxInMs, nodes.get(nodes.size() - 1).y); 
    432                 } 
    433  
    434                 curTime = time; 
    435             } 
    436  
    437             List<PartWiseLinearFunction.Node> getNodeList() { 
    438                 return nodes; 
     537                return (getHeight() - 1) * (1 - yRel); 
     538            } 
     539 
     540            public int getXCord(long timestamp, long timeScaleMaxInNs) { 
     541                long firstTimestamp = firstDisplayedTimestamp; 
     542                long timeDiff = timestamp - firstTimestamp; 
     543                long timeSinceWrapAround = timeDiff % timeScaleMaxInNs; 
     544                return (int)((getWidth() * timeSinceWrapAround) / timeScaleMaxInNs); 
     545                //double xRel = ((double)timeSinceWrapAround) / timeScaleMaxInMs; 
     546                //return (getWidth()) * xRel; 
     547            } 
     548        } 
     549 
     550        class OscilloscopeFunction { 
     551 
     552            final boolean attachedTimestamps; 
     553 
     554            // Basic ring buffer for function values (this is by far more efficient than using objects in a Collection class)) 
     555            final long timestampsAttached[]; 
     556            final long timestampsReceiveTime[]; 
     557            final double values[]; 
     558            final long validBytes;  // Value to and index with to realize wraparound 
     559 
     560            // Indices 
     561            final AtomicLong startIndex = new AtomicLong();     // Start index required for redrawing (updated by AWT thread only) 
     562            final AtomicLong endIndex = new AtomicLong();       // Index of past the last enqueued value (updated by NQ thread only) 
     563 
     564            long startIndexAWT;                                 // Start index (copy for AWT thread - updated in 
     565            long endIndexAWT;                                   // End index (copy for AWT thread - 
     566 
     567 
     568            // Constants returned by addNewValue 
     569            static final int ENQUEUED = 0; 
     570            static final int REALLOCATE_EMPTY = 1; 
     571            static final int REALLOCATE_BIGGER = 2; 
     572 
     573            public OscilloscopeFunction(PortDataManager currentValueBuffer) { 
     574                int size = 1024; 
     575                timestampsAttached = new long[size]; 
     576                timestampsReceiveTime = new long[size]; 
     577                values = new double[size]; 
     578                validBytes = (size - 1); 
     579 
     580                // Add port's current value 
     581                long timestamp = currentValueBuffer.getTimestamp().longValue(); 
     582                this.attachedTimestamps = timestamp != 0; 
     583                addNewValue(((NumericRepresentation)currentValueBuffer.getCurReference().getData().getData()).getNumericRepresentation().doubleValue(), timestamp); 
     584            } 
     585 
     586            public OscilloscopeFunction(OscilloscopeFunction functionToRellocate) { 
     587                synchronized (functionToRellocate) { 
     588                    functionToRellocate.updateThreadLocalVariablesForDrawing(); 
     589 
     590                    attachedTimestamps = functionToRellocate.attachedTimestamps; 
     591                    int size = functionToRellocate.values.length * 2; 
     592                    timestampsAttached = new long[size]; 
     593                    timestampsReceiveTime = new long[size]; 
     594                    values = new double[size]; 
     595                    validBytes = (size - 1); 
     596 
     597                    long oldStartIndex = functionToRellocate.startIndex.get(); 
     598                    long oldEndIndex = functionToRellocate.endIndex.get(); 
     599 
     600                    int newIndex = 0; 
     601                    for (long i = oldStartIndex; i < oldEndIndex; i++, newIndex++) { 
     602                        int index = (int)(i & functionToRellocate.validBytes); 
     603                        timestampsAttached[newIndex] = functionToRellocate.timestampsAttached[index]; 
     604                        timestampsReceiveTime[newIndex] = functionToRellocate.timestampsReceiveTime[index]; 
     605                        values[newIndex] = functionToRellocate.values[index]; 
     606                    } 
     607                    endIndex.set(newIndex); 
     608                    startIndexAWT = 0; 
     609                    endIndexAWT = newIndex; 
     610                    checkConsistency(startIndexAWT, endIndexAWT); 
     611                } 
     612            } 
     613 
     614            public void checkConsistency(long startIndex, long endIndex) { 
     615                for (long i = startIndex + 1; i < endIndex; i++) { 
     616                    int arrayIndex1 = (int)((i - 1) & validBytes); 
     617                    int arrayIndex2 = (int)(i & validBytes); 
     618                    if (timestampsReceiveTime[arrayIndex1] > timestampsReceiveTime[arrayIndex2]) { 
     619                        System.out.println("Inconsistency detected"); 
     620                    } 
     621                } 
     622            } 
     623 
     624            // ################################## 
     625            // functions called by NQ thread only 
     626            // ################################## 
     627            public int addNewValue(double value, long attachedTimestampNs) { 
     628 
     629                boolean attachedTimestamps = attachedTimestampNs != 0; 
     630                long receiveTime = System.currentTimeMillis() * 1000000; 
     631                long startIndexCopy = startIndex.get(); 
     632                long endIndexCopy = endIndex.get(); 
     633 
     634                // Ensure that timestamps increase monotonically (otherwise clear history) 
     635                if (endIndexCopy != startIndexCopy) { 
     636 
     637                    if (attachedTimestamps != this.attachedTimestamps) { 
     638                        return REALLOCATE_EMPTY; 
     639                    } 
     640 
     641                    // Compare with end timestamps 
     642                    int index = (int)((endIndexCopy - 1) & validBytes); 
     643                    if (attachedTimestampNs < timestampsAttached[index] || receiveTime < timestampsReceiveTime[index]) { 
     644                        return REALLOCATE_EMPTY; 
     645                    } 
     646                } 
     647 
     648                if (endIndexCopy - startIndexCopy >= values.length) { 
     649                    return REALLOCATE_BIGGER; 
     650                } 
     651 
     652                int index = (int)(endIndexCopy & validBytes); 
     653                timestampsAttached[index] = attachedTimestampNs; 
     654                timestampsReceiveTime[index] = receiveTime; 
     655                values[index] = value; 
     656                endIndex.set(endIndexCopy + 1); 
     657                //checkConsistency(startIndexCopy, endIndexCopy + 1); 
     658                return ENQUEUED; 
     659            } 
     660 
     661            // ##################################################### 
     662            // functions called by AWT thread only - or synchronized 
     663            // ##################################################### 
     664 
     665            /** 
     666             * Must be called by AWT thread before drawing - otherwise non-updated data is processed 
     667             */ 
     668            public synchronized void updateThreadLocalVariablesForDrawing() { 
     669 
     670                long lastEndIndex = endIndexAWT; 
     671                startIndexAWT = startIndex.get(); 
     672                endIndexAWT = endIndex.get(); 
     673 
     674                // Equally distribute incoming values from network connection across local time window (to somewhat address the problem that network data may come in bursts - in particular in WLANs - with respective effects on their timestamps; nonetheless, it is highly recommended to attach timestamps to data - in particular to obtain clean high-resolution plots) 
     675                long endIndexDiff = endIndexAWT - lastEndIndex; 
     676                if (endIndexDiff > 1) { 
     677                    int lastEndIndexArray = (int)((lastEndIndex - 1) & validBytes); 
     678                    int newEndIndexArray = (int)((endIndexAWT - 1) & validBytes); 
     679                    long lastEndTimestamp = timestampsReceiveTime[lastEndIndexArray]; 
     680                    long newEndTimestamp = timestampsReceiveTime[newEndIndexArray]; 
     681                    long timeDiff = newEndTimestamp - lastEndTimestamp; 
     682 
     683                    timeDiff = Math.min(timeDiff, MAX_NETWORK_LATENCY_TO_CONSIDER); 
     684 
     685                    for (int i = 1; i < endIndexDiff; i++) { 
     686                        double indexFactor = ((double)i) / endIndexDiff; 
     687                        long timeDiffFromEnd = (long)(timeDiff * (1 - indexFactor)); 
     688                        int indexArray = (int)((lastEndIndex + (i - 1)) & validBytes); 
     689                        timestampsReceiveTime[indexArray] = newEndTimestamp - timeDiffFromEnd; 
     690                        checkConsistency(startIndexAWT, (lastEndIndex + (i - 1))); 
     691                    } 
     692                } 
     693 
     694                // Trim ring buffer? 
     695                long periodNanoseconds = ((long)(timeScaleMaxInMs)) * 1000000; 
     696                boolean startIndexUpdated = false; 
     697                if (endIndexAWT - startIndexAWT > 3) { 
     698                    int endIndexArray = (int)((endIndexAWT - 1) & validBytes); 
     699                    long lastTimestampReceiveTime = timestampsReceiveTime[endIndexArray]; 
     700                    long lastTimestampAttached = timestampsAttached[endIndexArray]; 
     701                    while (endIndexAWT - startIndexAWT > 3) { 
     702                        int index = (int)((startIndexAWT + 1) & validBytes); 
     703                        if (lastTimestampReceiveTime - timestampsReceiveTime[index] > periodNanoseconds && ((!attachedTimestamps) || (lastTimestampAttached - timestampsAttached[index] > periodNanoseconds))) { 
     704                            startIndexAWT++; 
     705                            startIndexUpdated = true; 
     706                        } else { 
     707                            break; 
     708                        } 
     709                    } 
     710                } 
     711                if (startIndexUpdated) { 
     712                    startIndex.set(startIndexAWT); 
     713                } 
     714            } 
     715 
     716            public int size() { 
     717                return (int)(endIndexAWT - startIndexAWT); 
     718            } 
     719 
     720            public int getArrayIndex(long index) { 
     721                return (int)((index + startIndexAWT) & validBytes); 
     722            } 
     723 
     724            public double lastValue() { 
     725                if (size() <= 0) { 
     726                    return 0; 
     727                } 
     728                int index = (int)((endIndexAWT - 1) & validBytes); 
     729                return values[index]; 
     730            } 
     731 
     732            public double getExactValueAtTimestamp(long timestamp, boolean useAttachedTimestamps, long maxRampLength) { 
     733                long[] timestampsNanoSeconds = useAttachedTimestamps ? timestampsAttached : timestampsReceiveTime; 
     734 
     735                // Bound checks 
     736                if (size() == 0 || timestamp < timestampsNanoSeconds[getArrayIndex(0)]) { 
     737                    return 0; 
     738                } 
     739                if (timestamp == timestampsNanoSeconds[getArrayIndex(0)]) { 
     740                    return values[getArrayIndex(0)]; 
     741                } 
     742                if (timestamp >= timestampsNanoSeconds[getArrayIndex(size() - 1)]) { 
     743                    return values[getArrayIndex(size() - 1)]; 
     744                } 
     745 
     746                // Do binary search 
     747                long first = 0; 
     748                long last = size() - 1; 
     749                while (last - first > 1) { 
     750                    long mid = (first + last) / 2; 
     751                    long timestampMid = timestampsNanoSeconds[getArrayIndex(mid)]; 
     752                    if (timestampMid == timestamp) { 
     753                        return values[getArrayIndex(mid)]; 
     754                    } else if (timestamp < timestampMid) { 
     755                        last = mid; 
     756                    } else { 
     757                        first = mid; 
     758                    } 
     759                } 
     760 
     761                if (last == first) { // should not happen, but handle anyway 
     762                    return values[getArrayIndex(first)]; 
     763                } 
     764                long timeLast = timestampsNanoSeconds[getArrayIndex(last)]; 
     765                long timeFirst = Math.max(timestampsNanoSeconds[getArrayIndex(first)], timeLast - maxRampLength); 
     766                double valueFirst = values[getArrayIndex(first)]; 
     767                double valueLast = values[getArrayIndex(last)]; 
     768                if (timestamp <= timeFirst) { 
     769                    return valueFirst; 
     770                } 
     771 
     772                assert(timestamp > timeFirst && timestamp < timeLast); 
     773                long timeDiff = timeLast - timeFirst; 
     774                double valueDiff = valueLast - valueFirst; 
     775                double interpolator = (timestamp - timeFirst) / ((double)timeDiff); 
     776                return valueFirst + interpolator * valueDiff; 
    439777            } 
    440778        } 
     
    444782    public final static DataTypeBase TYPE = new DataType<OscilloscopeUI.OscilloscopeFunction>(OscilloscopeUI.OscilloscopeFunction.class); 
    445783} 
     784 
Note: See TracChangeset for help on using the changeset viewer.