Skip to content

Commit 273959e

Browse files
PatrickPenguinTurtleSamCarlberg
authored andcommitted
Code Generation (#597)
Add support for generating Java, C++, and Python code from a pipeline. Exported code uses the official OpenCV API for that language (so no JavaCV). The generated code is set up so that a user supplies an image, runs the pipeline, and gets the data outputs manually. There is no integration with image sources, such as webcams, to make the generated code as portable as possible Notable changes and additions: * Made file chooser default to Pipeline as name and made exporter name the class properly * Created an icon for code generation * Adds new switches for gradle: -Pgeneration to compile/run generation and non generation tests -Pgenonly to compile all tests but only run generation tests if neither is specified then generation tests will not be compiled and thus OpenCV is not required to compile tests other than generation * Always display the correct language if tests fail. * Added shortcut for code generation * Updated to use a new thread for export * Made code generation work from within an executable. Also changed the template to the code generation folder. This made it easier for templates to be found. * Don't try generating code for publishing operations * Make variable and method names fit standard language conventions. Variables and methods no longer have numbers if there's only one of that operation, or if an operation only has one output. Operations with multiple outputs have the outputs named the same as on the socket (instead of just 'output0', 'output1', ...) Rename `MutableOf` to `Ref` because it's really just holding a reference and it's used similarly to output parameters in C/C++ * Add alert when trying to generate code for a pipeline with non-exportable operations * Fixed Java and Python tests on Travis. C++ test are disabled. * Use OpenCV artifacts on FRC maven for code gen testing. Enable code gen tests on Travis' OSX platform
1 parent 1c1a5f6 commit 273959e

File tree

277 files changed

+10843
-7
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

277 files changed

+10843
-7
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ bower_components
8888
**/generated
8989
*/generated_tests
9090
/bin/
91+
ui/PipelineTest.java
92+
ui/CameraTest.java
9193

9294
### NetworkTables
9395
networktables.ini
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
3+
#pip3 install --upgrade pip
4+
#pip3 install numpy
5+
#pip3 install opencv-python
6+
#mkdir -p $HOME/opencv/jni
7+
#sudo apt-get install libdc1394-22-dev
8+
#sudo ln /dev/null /dev/raw1394

.travis-scripts/osx-opencv-install.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
3+
brew update
4+
brew install gcc
5+
brew upgrade gcc
6+
brew install cmake
7+
brew upgrade cmake
8+
brew install ant
9+
brew upgrade ant
10+
brew install python3
11+
brew linkapps python3
12+
pip3 install numpy
13+
pip3 install opencv-python
14+
mkdir -p $HOME/opencv/jni

.travis.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,26 @@ addons:
2121
- gcc-4.8
2222
- g++-4.8
2323
- oracle-java8-installer
24-
24+
- python3
2525
before_install:
2626
- .travis-scripts/install.sh
27-
27+
- |
28+
if [[ "$TRAVIS_OS_NAME" == "osx" ]];
29+
then .travis-scripts/osx-opencv-install.sh;
30+
else .travis-scripts/linux-opencv-install.sh;
31+
fi
2832
# Only do an assemble when we aren't building a pull request
2933
install:
3034
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && ./gradlew jfxNative --no-daemon --stacktrace || ./gradlew --stacktrace '
3135

3236
script:
33-
- ./gradlew check jacocoTestReport jacocoRootReport --stacktrace -Pheadless=true -PlogTests
37+
# Only run code generation tests on OSX -- linux requires sudo to install OpenCV dependencies, and that environment
38+
# will not be able to run MainWindowTest.testDragOperationFromPaletteToPipeline
39+
- |
40+
if [[ "$TRAVIS_OS_NAME" == "osx" ]];
41+
then ./gradlew check jacocoTestReport jacocoRootReport --stacktrace -Pheadless=true -PlogTests -Pgeneration -PjniLocation=$HOME/opencv/jni;
42+
else ./gradlew check jacocoTestReport jacocoRootReport --stacktrace -Pheadless=true -PlogTests;
43+
fi
3444
3545
after_success:
3646
- if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then codecov ; fi

build.gradle

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,22 @@ buildscript {
66
maven {
77
url "https://plugins.gradle.org/m2/"
88
}
9+
maven {
10+
url 'https://github.com/WPIRoboticsProjects/opencv-maven/raw/mvn-repo'
11+
}
912
}
1013

1114
dependencies {
1215
classpath group: 'de.dynamicfiles.projects.gradle.plugins', name: 'javafx-gradle-plugin', version: '8.5.2'
1316
classpath 'com.netflix.nebula:gradle-aggregate-javadocs-plugin:2.2.+'
1417
classpath 'net.ltgt.gradle:gradle-errorprone-plugin:0.0.8'
18+
classpath group: 'edu.wpi.first.wpilib.opencv', name: 'opencv-installer', version: '2.0.0'
1519
}
1620
}
21+
22+
import edu.wpi.first.wpilib.opencv.installer.Installer
23+
import edu.wpi.first.wpilib.opencv.installer.platform.Platform
24+
1725
plugins {
1826
id 'java'
1927
id 'idea'
@@ -26,9 +34,12 @@ plugins {
2634
apply plugin: 'nebula-aggregate-javadocs'
2735

2836
def getGitCommit = { ->
29-
String HEAD_REF = new File(rootDir, '.git/HEAD').text.replace("ref: ", "").replace("\n", "")
30-
String COMMIT_HASH = new File(rootDir, ".git/${HEAD_REF}").text.substring(0, 7)
31-
return COMMIT_HASH
37+
def stdout = new ByteArrayOutputStream()
38+
exec {
39+
commandLine 'git', 'rev-parse', '--short', 'HEAD'
40+
standardOutput = stdout
41+
}
42+
return stdout.toString().trim()
3243
}
3344

3445
idea.project {
@@ -374,21 +385,38 @@ project(":ui") {
374385
apply plugin: 'application'
375386
apply plugin: 'javafx-gradle-plugin'
376387

388+
repositories {
389+
maven {
390+
url = "http://first.wpi.edu/FRC/roborio/maven/development"
391+
}
392+
}
393+
377394
configurations {
378395
ideProvider
379396
}
380-
397+
if(!(project.hasProperty('generation') || project.hasProperty('genonly'))){
398+
sourceSets {
399+
test {
400+
java {
401+
exclude "**/ui/codegeneration"
402+
exclude "**/ui/codegeneration/**"
403+
}
404+
}
405+
}
406+
}
381407
dependencies {
382408
compile project(path: ':core', configuration: 'shadow')
383409
compile project(path: ':ui:preloader')
384410
ideProvider project(path: ':core', configuration: 'compile')
385411
compile group: 'org.controlsfx', name: 'controlsfx', version: '8.40.11'
386412
compile group: 'com.hierynomus', name: 'sshj', version: '0.16.0'
413+
compile group: 'org.apache.velocity', name: 'velocity', version: '1.7'
387414
testCompile files(project(':core').sourceSets.test.output.classesDir)
388415
testCompile files(project(':core').sourceSets.test.output.resourcesDir)
389416
testCompile group: 'org.testfx', name: 'testfx-core', version: '4.0.+'
390417
testCompile group: 'org.testfx', name: 'testfx-junit', version: '4.0.+'
391418
testRuntime group: 'org.testfx', name: 'openjfx-monocle', version: '1.8.0_20'
419+
testCompile group: 'org.opencv', name: 'opencv-java', version: '3.1.0'
392420
}
393421

394422
evaluationDependsOn(':core')
@@ -412,6 +440,91 @@ project(":ui") {
412440
}
413441
}
414442

443+
task testSharedLib() {
444+
description 'Compiles the shared library used by c++ generation testing.'
445+
doLast {
446+
def syst = osdetector.os
447+
if(syst == "windows") {
448+
exec {
449+
ignoreExitValue true//if clean hasn't been called, directory already exists and mkdir fails.
450+
workingDir 'build/classes/test'
451+
commandLine 'cmd', '/c', 'mkdir', 'pipelib'
452+
}
453+
exec {
454+
workingDir 'build/classes/test/pipelib'
455+
commandLine '"C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\vcvarsall.bat"', 'amd64', '&', 'cmake', '-G', 'Visual Studio 14 2015 Win64', '..\\..\\..\\..\\src\\test\\resources\\edu\\wpi\\grip\\ui\\codegeneration\\tools\\'
456+
}
457+
exec {
458+
workingDir 'build/classes/test/pipelib'
459+
commandLine '"C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\vcvarsall.bat"', 'amd64', '&', 'cmake', '--build', '.', '--target', 'ALL_BUILD', '--config', 'Release'
460+
}
461+
exec {
462+
workingDir 'build/classes/test'
463+
commandLine 'cmd', '/C', 'copy /Y pipelib\\Release\\* .'
464+
}
465+
exec {
466+
workingDir 'build/classes/test'
467+
commandLine 'cmd', '/C', 'copy /Y pipelib\\pipe\\Release\\* .'
468+
}
469+
exec {
470+
workingDir 'build/classes/test/pipelib'
471+
commandLine 'cmd', '/C', 'copy /Y ..\\..\\..\\..\\src\\test\\resources\\edu\\wpi\\grip\\ui\\codegeneration\\tools\\pipe\\AbsPipeline.h pipe\\'
472+
}
473+
exec {
474+
workingDir 'build/classes/test'
475+
commandLine 'cmd', '/C', 'copy /Y ..\\..\\..\\src\\test\\resources\\edu\\wpi\\grip\\ui\\codegeneration\\tools\\realpipe\\CMakeLists.txt .'
476+
}
477+
} else {
478+
exec {
479+
ignoreExitValue true//if clean hasn't been called, directory already exists and mkdir fails.
480+
workingDir 'build/classes/test'
481+
commandLine 'mkdir', 'pipelib'
482+
}
483+
exec {
484+
workingDir 'build/classes/test/pipelib'
485+
commandLine 'cmake' , '../../../../src/test/resources/edu/wpi/grip/ui/codegeneration/tools/'
486+
}
487+
exec {
488+
workingDir 'build/classes/test/pipelib'
489+
commandLine 'make'
490+
}
491+
exec {
492+
workingDir 'build/classes/test'
493+
if(syst == "osx"){
494+
commandLine 'cp', 'pipelib/libgenJNI.dylib', '.'
495+
}
496+
if(syst == "linux"){
497+
commandLine 'cp' , 'pipelib/libgenJNI.so', '.'
498+
}
499+
}
500+
exec {
501+
workingDir 'build/classes/test/pipelib'
502+
commandLine 'cp' , '../../../../src/test/resources/edu/wpi/grip/ui/codegeneration/tools/pipe/AbsPipeline.h', 'pipe/'
503+
}
504+
exec {
505+
workingDir 'build/classes/test'
506+
commandLine 'cp', '../../../src/test/resources/edu/wpi/grip/ui/codegeneration/tools/realpipe/CMakeLists.txt', '.'
507+
}
508+
}
509+
}
510+
}
511+
512+
if(project.hasProperty('generation') || project.hasProperty('genonly')){
513+
def syst = osdetector.os
514+
test {
515+
Platform platform = Installer.getPlatform()
516+
Installer.setOpenCvVersion('3.1.0')
517+
def jniLocation = project.getProperties().getOrDefault('jniLocation', platform.defaultJniLocation())
518+
Installer.installJni("${jniLocation}")
519+
def defaultLibPath = System.getProperty('java.library.path');
520+
jvmArgs = ["-Djava.library.path=${defaultLibPath}${System.getProperty('path.separator')}${jniLocation}"]
521+
if(project.hasProperty('genonly')){
522+
useJUnit {
523+
includeCategories 'edu.wpi.grip.ui.codegeneration.GenerationTesting'
524+
}
525+
}
526+
}
527+
}
415528

416529
jfx {
417530
mainClass = "edu.wpi.grip.ui.Main"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package edu.wpi.grip.core.operations;
2+
3+
import edu.wpi.grip.core.OperationMetaData;
4+
5+
import com.google.common.collect.ImmutableList;
6+
7+
import java.util.Locale;
8+
9+
import javax.inject.Inject;
10+
11+
import static org.junit.Assert.fail;
12+
13+
public class OperationsUtil {
14+
@Inject
15+
private CVOperations cvOperations;
16+
17+
public ImmutableList<OperationMetaData> operations() {
18+
return cvOperations.operations();
19+
}
20+
21+
public OperationMetaData getMetaData(String opName) {
22+
Locale locOpName = new Locale(opName);
23+
String newOpName = locOpName.toString().toLowerCase().replaceAll("[^a-zA-Z]", "");
24+
for (OperationMetaData data : operations()) {
25+
String dataName = data.getDescription().name().toLowerCase().replaceAll("[^a-zA-Z]", "");
26+
if (dataName.equals(newOpName)) {
27+
return data;
28+
}
29+
}
30+
fail("Given operation name " + newOpName + " does not match any CV operation");
31+
return null; //Never going to happen since line above throws error
32+
}
33+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package edu.wpi.grip.core.sources;
2+
3+
import edu.wpi.grip.core.Source;
4+
import edu.wpi.grip.core.sockets.OutputSocket;
5+
import edu.wpi.grip.core.sockets.SocketHint;
6+
import edu.wpi.grip.core.sockets.SocketHints;
7+
import edu.wpi.grip.core.util.ExceptionWitness.Factory;
8+
9+
import com.google.common.collect.ImmutableList;
10+
11+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
12+
13+
import java.io.IOException;
14+
import java.util.List;
15+
import java.util.Properties;
16+
17+
public class MockNumberSource extends Source {
18+
19+
private static int numberOf = 0;
20+
private final int id;
21+
private final OutputSocket<Number> outputSocket;
22+
private final SocketHint<Number> outputSocketHint =
23+
SocketHints.Outputs.createNumberSocketHint("Num", Math.PI);
24+
25+
@SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
26+
justification = "Do not need to synchronize inside of a constructor")
27+
public MockNumberSource(Factory exceptionWitnessFactory, double value, OutputSocket.Factory osf) {
28+
super(exceptionWitnessFactory);
29+
id = numberOf++;
30+
outputSocket = osf.create(outputSocketHint);
31+
outputSocket.setValue(new Double(value));
32+
}
33+
34+
@Override
35+
public String getName() {
36+
return "NumberSource" + id;
37+
}
38+
39+
@Override
40+
protected List<OutputSocket> createOutputSockets() {
41+
return ImmutableList.of(
42+
outputSocket
43+
);
44+
}
45+
46+
@Override
47+
protected boolean updateOutputSockets() {
48+
return false;
49+
}
50+
51+
@Override
52+
public Properties getProperties() {
53+
final Properties properties = new Properties();
54+
properties.setProperty("number", createOutputSockets().get(0)
55+
.getValue().orElseGet(() -> Math.PI).toString());
56+
return properties;
57+
}
58+
59+
@Override
60+
public void initialize() throws IOException {
61+
//Initialize doesn't need to do anything for a number source.
62+
}
63+
64+
}

grip.xml

Whitespace-only changes.

ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import edu.wpi.grip.core.settings.SettingsProvider;
1010
import edu.wpi.grip.core.util.SafeShutdown;
1111
import edu.wpi.grip.core.util.service.SingleActionListener;
12+
import edu.wpi.grip.ui.codegeneration.Exporter;
13+
import edu.wpi.grip.ui.codegeneration.Language;
1214
import edu.wpi.grip.ui.components.StartStoppableButton;
1315
import edu.wpi.grip.ui.util.DPIUtility;
1416

@@ -21,13 +23,16 @@
2123
import java.io.File;
2224
import java.io.IOException;
2325
import java.util.Optional;
26+
import java.util.Set;
2427
import java.util.logging.Level;
2528
import java.util.logging.Logger;
2629

2730
import javafx.application.Platform;
31+
import javafx.event.ActionEvent;
2832
import javafx.fxml.FXML;
2933
import javafx.scene.Parent;
3034
import javafx.scene.Scene;
35+
import javafx.scene.control.Alert;
3136
import javafx.scene.control.ButtonType;
3237
import javafx.scene.control.Dialog;
3338
import javafx.scene.control.SplitPane;
@@ -240,6 +245,38 @@ protected void quit() {
240245
SafeShutdown.exit(0);
241246
}
242247
}
248+
249+
/**
250+
* Controls the export button in the main menu. Opens a filechooser with language selection.
251+
* The user can select the language to export to, save location and file name.
252+
* @param actionEvent Unused event passed by the controller.
253+
*/
254+
public void generate(ActionEvent actionEvent) {
255+
final FileChooser fileChooser = new FileChooser();
256+
fileChooser.setTitle("Export to");
257+
fileChooser.getExtensionFilters().add(new ExtensionFilter(Language.JAVA.name, "*.java"));
258+
fileChooser.getExtensionFilters().add(new ExtensionFilter(Language.CPP.name, "*.cpp"));
259+
fileChooser.getExtensionFilters().add(new ExtensionFilter(Language.PYTHON.name, "*.py"));
260+
fileChooser.setInitialFileName("Pipeline.java");
261+
final File file = fileChooser.showSaveDialog(root.getScene().getWindow());
262+
if (file == null) {
263+
return;
264+
}
265+
Language lang = Language.get(fileChooser.getSelectedExtensionFilter().getDescription());
266+
Exporter exporter = new Exporter(pipeline.getSteps(), lang, file);
267+
final Set<String> nonExportableSteps = exporter.getNonExportableSteps();
268+
if (!nonExportableSteps.isEmpty()) {
269+
StringBuilder b = new StringBuilder("The following steps cannot be exported:\n");
270+
nonExportableSteps.forEach(n -> b.append(" ").append(n).append('\n'));
271+
Alert alert = new Alert(Alert.AlertType.WARNING);
272+
alert.setContentText(b.toString());
273+
alert.showAndWait();
274+
return;
275+
}
276+
Thread exportRunner = new Thread(exporter);
277+
exportRunner.setDaemon(true);
278+
exportRunner.start();
279+
}
243280

244281
@FXML
245282
protected void deploy() {

0 commit comments

Comments
 (0)