Skip to content

Commit

Permalink
Merge pull request #7 from Arctos6135/develop
Browse files Browse the repository at this point in the history
Version 0.2.0 - For real this time!
  • Loading branch information
tylertian123 authored Oct 18, 2019
2 parents 68467c4 + 6c0ae3c commit d7504ab
Show file tree
Hide file tree
Showing 10 changed files with 612 additions and 8 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@

StdPlug is the Arctos 6135 "Standard" Shuffleboard Plugin. Currently, it offers these widgets:

| Name | Description | Accepted Data Types |
| ----------- | ------------------------------------------------- | -------------------------- |
| Image | Displays a static image when given its full path. | String |
| PIDVA Gains | Displays a set of PIDVA or PIDVA + DP gains. | PIDVA Gains, PIDVADP Gains |
| Name | Description | Accepted Data Types |
| ------------------- | ------------------------------------------------- | -------------------------- |
| Image | Displays a static image when given its full path. | String |
| PIDVA Gains | Displays a set of PIDVA or PIDVA + DP gains. | PIDVA Gains, PIDVADP Gains |
| MJPEG Stream Viewer | Displays an MJPEG video stream. | String |

And these data types:

Expand Down Expand Up @@ -41,7 +42,7 @@ dependencies {
compile wpi.deps.wpilib()
compile wpi.deps.vendor.java()
compile files('lib/StdPlug-API-0.1.0.jar')
compile files('lib/StdPlug-API-0.2.0.jar')
nativeZip wpi.deps.vendor.jni(wpi.platforms.roborio)
nativeDesktopZip wpi.deps.vendor.jni(wpi.platforms.desktop)
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ plugins {
id 'checkstyle'
}

project.version = "0.1.0"
project.version = "0.2.0"

// Set up JavaFX plugin
javafx {
Expand Down Expand Up @@ -67,6 +67,7 @@ tasks.withType(Javadoc) {
exclude 'com/arctos6135/stdplug/data/*'
exclude 'com/arctos6135/stdplug/datatypes/*'
exclude 'com/arctos6135/stdplug/widgets/*'
exclude 'com/arctos6135/stdplug/util/*'

doLast {
// Since the introduction of modules in Java 9, the Javadoc search functionality would break if the
Expand Down
2 changes: 2 additions & 0 deletions config/checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@
<suppress files="src[\\\/]main[\\\/]java[\\\/]com[\\\/]arctos6135[\\\/]stdplug[\\\/]data" checks="" />
<suppress files="src[\\\/]main[\\\/]java[\\\/]com[\\\/]arctos6135[\\\/]stdplug[\\\/]datatypes" checks="" />
<suppress files="src[\\\/]main[\\\/]java[\\\/]com[\\\/]arctos6135[\\\/]stdplug[\\\/]widgets" checks="" />
<suppress files="src[\\\/]main[\\\/]java[\\\/]com[\\\/]arctos6135[\\\/]stdplug[\\\/]util" checks="" />
<suppress files=".*\.gif" checks="" />
</suppressions>
6 changes: 4 additions & 2 deletions src/main/java/com/arctos6135/stdplug/StdPlug.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.arctos6135.stdplug.datatypes.PIDVADPDataType;
import com.arctos6135.stdplug.datatypes.PIDVADataType;
import com.arctos6135.stdplug.widgets.ImageWidget;
import com.arctos6135.stdplug.widgets.MJPEGStreamViewerWidget;
import com.arctos6135.stdplug.widgets.PIDVAGainsWidget;

import edu.wpi.first.shuffleboard.api.data.DataType;
Expand All @@ -14,7 +15,7 @@
import edu.wpi.first.shuffleboard.api.widget.ComponentType;
import edu.wpi.first.shuffleboard.api.widget.WidgetType;

@Description(group = "com.arctos6135", name = "StdPlug", version = "0.1.0", summary = "Arctos 6135 Standard Shuffleboard Plugin")
@Description(group = "com.arctos6135", name = "StdPlug", version = "0.2.0", summary = "Arctos 6135 Standard Shuffleboard Plugin")
public class StdPlug extends Plugin {

@Override
Expand All @@ -31,7 +32,8 @@ public List<DataType> getDataTypes() {
public List<ComponentType> getComponents() {
return List.of(
WidgetType.forAnnotatedWidget(ImageWidget.class),
WidgetType.forAnnotatedWidget(PIDVAGainsWidget.class)
WidgetType.forAnnotatedWidget(PIDVAGainsWidget.class),
WidgetType.forAnnotatedWidget(MJPEGStreamViewerWidget.class)
);
}

Expand Down
43 changes: 43 additions & 0 deletions src/main/java/com/arctos6135/stdplug/api/StdPlugWidgets.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,47 @@ private StdPlugWidgets() {
* @see StdPlugDataTypes#PIDVADP_GAINS
*/
public static final String PIDVA_GAINS = "PIDVA Gains";

/**
* Displays an MJPEG video stream.
* <p>
* The stream is specified by an URL.
* </p>
* <p>
* Supported types:
* <ul>
* <li>String</li>
* </ul>
* Custom properties:
* <table>
* <tr>
* <th>Name</th>
* <th>Type</th>
* <th>Default Value</th>
* <th>Notes</th>
* </tr>
* <tr>
* <td>Keep Aspect Ratio</td>
* <td>Boolean</td>
* <td>true</td>
* <td>If set, the aspect ratio of the stream will be kept</td>
* </tr>
* <tr>
* <td>Show Stats</td>
* <td>Boolean</td>
* <td>true</td>
* <td>If set, the FPS and bandwidth (Mbps) will be shown</td>
* </tr>
* <tr>
* <td>Start Stream</td>
* <td>Boolean</td>
* <td>true</td>
* <td>If set, the stream viewer will be started</td>
* </tr>
* </table>
* </p>
*
* @since 0.2.0
*/
public static final String MJPEG_STREAM_VIEWER = "MJPEG Stream Viewer";
}
271 changes: 271 additions & 0 deletions src/main/java/com/arctos6135/stdplug/util/MJPEGStreamViewerTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
package com.arctos6135.stdplug.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.atomic.AtomicBoolean;

import javafx.concurrent.Task;
import javafx.scene.image.Image;

/**
* A Task that receives image data from an MJPEG stream and never finishes unless cancelled.
*/
public class MJPEGStreamViewerTask extends Task<Image> {

public static final Image NO_CONNECTION_IMG = new Image(MJPEGStreamViewerTask.class.getResourceAsStream("/noconnection.gif"));
public static final String NO_CONNECTION_STR = "N/A,N/A";

// The bytes for the start image and end image tags in a JPEG image
// This is how we separate each image
private static final int[] START_IMAGE_BYTES = { 0xFF, 0xD8 };
private static final int[] END_IMAGE_BYTES = { 0xFF, 0xD9 };

// Conversion ratio from bytes per second to megabits per second
private static final double BPS_TO_MBPS = 8.0 / 1024.0 / 1024.0;

// The URL of the stream
volatile private String streamURL;

// Whether or not the URL has been updated
// If this is true then a new connection has to be opened
private AtomicBoolean streamURLUpdated = new AtomicBoolean(true);

// The stream of the connection that we can get image data from
private InputStream imgStream;

// The minimum time between two frames
volatile private long minRefreshInterval = 10;

/**
* Creates a new stream viewer thread with the specified URL.
* @param url The URL of the stream.
*/
public MJPEGStreamViewerTask(String url) {
if(url != null) {
streamURL = url.strip();
}
}

/**
* Updates the URL of the stream.
* @param streamURL The new URL of the stream.
*/
public void updateStreamURL(String streamURL) {
if(streamURL != null) {
this.streamURL = streamURL.strip();
}
else {
this.streamURL = null;
}
streamURLUpdated.set(true);
}

/**
* Sets the minimum time to wait between two frames, in milliseconds.
* @param minRefreshInterval The minimum time between frames
*/
public void setMinRefreshInterval(long minRefreshInterval) {
this.minRefreshInterval = minRefreshInterval;
}

/**
* Returns whether or not the URL has been updated.
*
* @return If the URL has been updated
*/
private boolean urlUpdated() {
if(streamURLUpdated.getAndSet(false)) {
return true;
}
return false;
}

/**
* Opens a connection to the stream URL and returns the stream.
*
* @return The stream
* @throws InterruptedException If the Task was cancelled
*/
private InputStream waitForImageStream() throws InterruptedException {
// Wait forever if not cancelled
while(!isCancelled()) {
if(streamURL == null || streamURL == "") {
updateValue(NO_CONNECTION_IMG);
updateMessage(NO_CONNECTION_STR);

if(!isCancelled()) {
// Wait a second and then retry
Thread.sleep(1000);
}
}
else {
try {
URL url = new URL(streamURL);
URLConnection conn = url.openConnection();

conn.setConnectTimeout(500);
conn.setReadTimeout(5000);

InputStream stream = conn.getInputStream();
System.out.println("Successfully connected to " + streamURL);
return stream;
}
// Catch any possible exceptions
// This thread shouldn't ever die by itself
catch(Exception e) {
System.err.println("Failed to connect to " + streamURL);
e.printStackTrace();

updateValue(NO_CONNECTION_IMG);
updateMessage(NO_CONNECTION_STR);

if(!isCancelled()) {
// Wait a second and then retry
Thread.sleep(1000);
}
}
}
}
return null;
}

private static long skipUntil(InputStream stream, int[] indicator) throws IOException {
long bytesRead = 0;
for(int i = 0; i < indicator.length; bytesRead++) {
int b = stream.read();
if(b == -1) {
throw new IOException("End of stream reached");
}
if(b == indicator[i]) {
i ++;
}
else {
i = 0;
}
}
return bytesRead;
}

private static long readUntil(InputStream stream, int[] indicator, ByteArrayOutputStream out) throws IOException {
long bytesRead = 0;
for(int i = 0; i < indicator.length; bytesRead ++) {
int b = stream.read();
if(b == -1) {
throw new IOException("End of stream reached");
}

if(out != null) {
out.write(b);
}

if(b == indicator[i]) {
i ++;
}
else {
i = 0;
}
}
return bytesRead;
}

@Override
protected Image call() throws Exception {
ByteArrayOutputStream imgBuf = new ByteArrayOutputStream();
long lastFrame = 0;
long lastStatCheck = 0;
int fpsCounter = 0;
long mbpsCounter = 0;

// Loop forever while not cancelled
while(!isCancelled()) {
// Try to wait for a connection
try {
imgStream = waitForImageStream();
}
catch(InterruptedException e) {
// Exit the loop and end this task if interrupted
break;
}

try {
while(!isCancelled() && !urlUpdated() && imgStream != null) {
// Make sure to respect the minimum refresh interval
if(System.currentTimeMillis() - lastFrame < minRefreshInterval) {
Thread.sleep(minRefreshInterval - (System.currentTimeMillis() - lastFrame));
}
// Clear the stream
// We don't want any old data
mbpsCounter += imgStream.available();
imgStream.skip(imgStream.available());

// Clear the image data
imgBuf.reset();
// Skip until the start of the actual image
mbpsCounter += skipUntil(imgStream, START_IMAGE_BYTES);
// Write the image start bytes into the image
for(int b : START_IMAGE_BYTES) {
imgBuf.write((byte) b);
}
// Read in the rest
mbpsCounter += readUntil(imgStream, END_IMAGE_BYTES, imgBuf);

// Calculate FPS and Mbps
fpsCounter ++;
if(System.currentTimeMillis() - lastStatCheck >= 1000) {
updateMessage(String.valueOf(fpsCounter) + ","
+ String.format("%.3f", mbpsCounter * BPS_TO_MBPS) + "Mbps");
fpsCounter = 0;
mbpsCounter = 0;
lastStatCheck = System.currentTimeMillis();
}

lastFrame = System.currentTimeMillis();
// Update the image
ByteArrayInputStream tmpStream = new ByteArrayInputStream(imgBuf.toByteArray());
updateValue(new Image(tmpStream));
}
}
catch(IOException e) {
updateValue(NO_CONNECTION_IMG);
updateMessage(NO_CONNECTION_STR);
System.err.println("Error while reading stream:");
e.printStackTrace();

try {
// This magical delay here fixes a magical bug where reading from the stream would give -1
Thread.sleep(100);
}
catch(InterruptedException e1) {
break;
}
}
catch(InterruptedException e) {
break;
}
catch(Exception e) {
// Something really bad just happened
// Recover anyways - it's critical that the stream never dies
System.err.println("Error while reading stream:");
e.printStackTrace();
}
finally {
// Clean up the old stream
if(imgStream != null) {
try {
imgStream.close();
}
catch(IOException e) {
e.printStackTrace();
}
}
}
}

return null;
}

}
Loading

0 comments on commit d7504ab

Please sign in to comment.