diff --git a/WilyCore/build.gradle b/WilyCore/build.gradle index d852a6b04d8f..d575434548bb 100644 --- a/WilyCore/build.gradle +++ b/WilyCore/build.gradle @@ -19,24 +19,60 @@ kotlin { sourceSets { main { java { - srcDirs = ['src/main/java', {System.getenv('SRC_ROOT')}] + srcDirs = ['src/main/java', '../Sidekick/src/main/java', {System.getenv('SRC_ROOT')}] } } } +// LWJGL for gamepad input, generated from https://www.lwjgl.org/customize for just the "GLFW" +// module for MacOS arm64 and Windows x64. +import org.gradle.internal.os.OperatingSystem + +project.ext.lwjglVersion = "3.3.6" + +switch (OperatingSystem.current()) { + case OperatingSystem.LINUX: + project.ext.lwjglNatives = "natives-linux" + def osArch = System.getProperty("os.arch") + if (osArch.startsWith("arm") || osArch.startsWith("aarch64")) { + project.ext.lwjglNatives += osArch.contains("64") || osArch.startsWith("armv8") ? "-arm64" : "-arm32" + } else if (osArch.startsWith("ppc")) { + project.ext.lwjglNatives += "-ppc64le" + } else if (osArch.startsWith("riscv")) { + project.ext.lwjglNatives += "-riscv64" + } + break + case OperatingSystem.MAC_OS: + project.ext.lwjglNatives = "natives-macos-arm64" + break + case OperatingSystem.WINDOWS: + project.ext.lwjglNatives = "natives-windows" + break +} + repositories { - jcenter() + mavenCentral() maven { url = 'https://maven.brott.dev/' } } dependencies { implementation project(path: ':WilyWorks') implementation "com.acmerobotics.roadrunner:core:1.0.0" // For Pose2d, PoseVelocity2d, etc. - // implementation "uk.co.electronstudio.sdl2gdx:sdl2gdx:1.0.+" // For gamepad control implementation "org.reflections:reflections:0.10.2" // For enumerating classes with annotations implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" // For enumerating all classes implementation "com.google.code.gson:gson:2.11.0" // For serializing Java objects to JSON + implementation 'net.bytebuddy:byte-buddy:1.15.10' // For proxying classes rather than interfaces + implementation 'org.objenesis:objenesis:3.2' // Instantiate class without invoking a constructor + implementation 'org.json:json:20250107' // Or any recent version + + // Lwjgl for gamepad input: + implementation platform("org.lwjgl:lwjgl-bom:$lwjglVersion") + implementation "org.lwjgl:lwjgl" + implementation "org.lwjgl:lwjgl-glfw" + runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives" } + compileKotlin { kotlinOptions { jvmTarget = "1.8" diff --git a/WilyCore/src/main/java/android/content/Context.java b/WilyCore/src/main/java/android/content/Context.java index 7abce111a1bf..51e337c42ec1 100644 --- a/WilyCore/src/main/java/android/content/Context.java +++ b/WilyCore/src/main/java/android/content/Context.java @@ -1,6 +1,8 @@ package android.content; import android.app.Activity; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; import java.io.File; @@ -13,4 +15,6 @@ public String getPackageName() { } public static final int MODE_PRIVATE = 0; public File getDir(String var1, int var2) { return null; } + public AssetManager getAssets() { return null; } + public PackageManager getPackageManager() { return new PackageManager(); } } diff --git a/WilyCore/src/main/java/android/content/pm/PackageInfo.java b/WilyCore/src/main/java/android/content/pm/PackageInfo.java new file mode 100644 index 000000000000..d378541d4859 --- /dev/null +++ b/WilyCore/src/main/java/android/content/pm/PackageInfo.java @@ -0,0 +1,10 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package android.content.pm; + +public class PackageInfo { + public String versionName = "9.0.1"; +} diff --git a/WilyCore/src/main/java/android/content/pm/PackageManager.java b/WilyCore/src/main/java/android/content/pm/PackageManager.java new file mode 100644 index 000000000000..d352cff8f199 --- /dev/null +++ b/WilyCore/src/main/java/android/content/pm/PackageManager.java @@ -0,0 +1,12 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package android.content.pm; + +import android.annotation.NonNull; + +public class PackageManager { + public PackageInfo getPackageInfo(@NonNull String var1, int var2) { return new PackageInfo(); } +} diff --git a/WilyCore/src/main/java/android/content/res/AssetManager.java b/WilyCore/src/main/java/android/content/res/AssetManager.java new file mode 100644 index 000000000000..620a5840785a --- /dev/null +++ b/WilyCore/src/main/java/android/content/res/AssetManager.java @@ -0,0 +1,13 @@ +package android.content.res; + +import android.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; + +public class AssetManager { + @NonNull + public InputStream open(@NonNull String fileName) throws IOException { + throw new RuntimeException("Wily Works Stub!"); + } +} diff --git a/WilyCore/src/main/java/android/graphics/Bitmap.java b/WilyCore/src/main/java/android/graphics/Bitmap.java index a6d567772595..6fe95dfc63a6 100644 --- a/WilyCore/src/main/java/android/graphics/Bitmap.java +++ b/WilyCore/src/main/java/android/graphics/Bitmap.java @@ -1,4 +1,239 @@ package android.graphics; +import android.annotation.NonNull; + +import java.io.OutputStream; +import java.nio.Buffer; + public class Bitmap { + @NonNull + public static final int DENSITY_NONE = 0; + + Bitmap() { + throw new RuntimeException("Stub!"); + } + + public int getDensity() { + throw new RuntimeException("Stub!"); + } + + public void setDensity(int density) { + throw new RuntimeException("Stub!"); + } + + public void reconfigure(int width, int height, Config config) { + throw new RuntimeException("Stub!"); + } + + public void setWidth(int width) { + throw new RuntimeException("Stub!"); + } + + public void setHeight(int height) { + throw new RuntimeException("Stub!"); + } + + public void setConfig(Config config) { + throw new RuntimeException("Stub!"); + } + + public void recycle() { + throw new RuntimeException("Stub!"); + } + + public boolean isRecycled() { + throw new RuntimeException("Stub!"); + } + + public int getGenerationId() { + throw new RuntimeException("Stub!"); + } + + public void copyPixelsToBuffer(Buffer dst) { + throw new RuntimeException("Stub!"); + } + + public void copyPixelsFromBuffer(Buffer src) { + throw new RuntimeException("Stub!"); + } + + public Bitmap copy(Config config, boolean isMutable) { + throw new RuntimeException("Stub!"); + } + + + public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight, boolean filter) { + throw new RuntimeException("Stub!"); + } + + public static Bitmap createBitmap(@NonNull Bitmap src) { + throw new RuntimeException("Stub!"); + } + + public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height) { + throw new RuntimeException("Stub!"); + } + + public static Bitmap createBitmap(int width, int height, @NonNull Config config) { + throw new RuntimeException("Stub!"); + } + + public static Bitmap createBitmap(int width, int height, @NonNull Config config, boolean hasAlpha) { + throw new RuntimeException("Stub!"); + } + + public static Bitmap createBitmap(@NonNull int[] colors, int offset, int stride, int width, int height, @NonNull Config config) { + throw new RuntimeException("Stub!"); + } + + public static Bitmap createBitmap(@NonNull int[] colors, int width, int height, Config config) { + throw new RuntimeException("Stub!"); + } + + public byte[] getNinePatchChunk() { + throw new RuntimeException("Stub!"); + } + + public boolean compress(CompressFormat format, int quality, OutputStream stream) { + throw new RuntimeException("Stub!"); + } + + public boolean isMutable() { + throw new RuntimeException("Stub!"); + } + + public boolean isPremultiplied() { + throw new RuntimeException("Stub!"); + } + + public void setPremultiplied(boolean premultiplied) { + throw new RuntimeException("Stub!"); + } + + public int getWidth() { + throw new RuntimeException("Stub!"); + } + + public int getHeight() { + throw new RuntimeException("Stub!"); + } + + public int getScaledWidth(Canvas canvas) { + throw new RuntimeException("Stub!"); + } + + public int getScaledHeight(Canvas canvas) { + throw new RuntimeException("Stub!"); + } + + public int getScaledWidth(int targetDensity) { + throw new RuntimeException("Stub!"); + } + + public int getScaledHeight(int targetDensity) { + throw new RuntimeException("Stub!"); + } + + public int getRowBytes() { + throw new RuntimeException("Stub!"); + } + + public int getByteCount() { + throw new RuntimeException("Stub!"); + } + + public int getAllocationByteCount() { + throw new RuntimeException("Stub!"); + } + + public Config getConfig() { + throw new RuntimeException("Stub!"); + } + + public boolean hasAlpha() { + throw new RuntimeException("Stub!"); + } + + public void setHasAlpha(boolean hasAlpha) { + throw new RuntimeException("Stub!"); + } + + public boolean hasMipMap() { + throw new RuntimeException("Stub!"); + } + + public void setHasMipMap(boolean hasMipMap) { + throw new RuntimeException("Stub!"); + } + + public void eraseColor(int c) { + throw new RuntimeException("Stub!"); + } + + public void eraseColor(long color) { + throw new RuntimeException("Stub!"); + } + + public int getPixel(int x, int y) { + throw new RuntimeException("Stub!"); + } + + @NonNull + public Color getColor(int x, int y) { + throw new RuntimeException("Stub!"); + } + + public void getPixels(int[] pixels, int offset, int stride, int x, int y, int width, int height) { + throw new RuntimeException("Stub!"); + } + + public void setPixel(int x, int y, int color) { + throw new RuntimeException("Stub!"); + } + + public void setPixels(int[] pixels, int offset, int stride, int x, int y, int width, int height) { + throw new RuntimeException("Stub!"); + } + + public int describeContents() { + throw new RuntimeException("Stub!"); + } + + public Bitmap extractAlpha() { + throw new RuntimeException("Stub!"); + } + + public Bitmap extractAlpha(Paint paint, int[] offsetXY) { + throw new RuntimeException("Stub!"); + } + + public boolean sameAs(Bitmap other) { + throw new RuntimeException("Stub!"); + } + + public void prepareToDraw() { + throw new RuntimeException("Stub!"); + } + + public static enum CompressFormat { + JPEG, + PNG, + /** @deprecated */ + @Deprecated + WEBP, + WEBP_LOSSY, + WEBP_LOSSLESS; + } + + public static enum Config { + ALPHA_8, + RGB_565, + /** @deprecated */ + @Deprecated + ARGB_4444, + ARGB_8888, + RGBA_F16, + HARDWARE; + } + } diff --git a/WilyCore/src/main/java/android/graphics/BitmapFactory.java b/WilyCore/src/main/java/android/graphics/BitmapFactory.java new file mode 100644 index 000000000000..043fb7891a33 --- /dev/null +++ b/WilyCore/src/main/java/android/graphics/BitmapFactory.java @@ -0,0 +1,9 @@ +package android.graphics; + +import java.io.InputStream; + +public class BitmapFactory { + public static Bitmap decodeStream(InputStream is) { + throw new RuntimeException("Not Wily Works implemented!"); + } +} diff --git a/WilyCore/src/main/java/android/graphics/Canvas.java b/WilyCore/src/main/java/android/graphics/Canvas.java index 006df0645204..880ac9d08639 100644 --- a/WilyCore/src/main/java/android/graphics/Canvas.java +++ b/WilyCore/src/main/java/android/graphics/Canvas.java @@ -1,4 +1,243 @@ package android.graphics; +import android.annotation.NonNull; +import android.annotation.Nullable; + public class Canvas { + public Canvas(@NonNull Bitmap bitmap) { + throw new RuntimeException("Wily Works Stub!"); + } + + public static final int ALL_SAVE_FLAG = 31; + + public Canvas() { + throw new RuntimeException("Stub!"); + } + + public boolean isHardwareAccelerated() { + throw new RuntimeException("Stub!"); + } + + public void setBitmap(@Nullable Bitmap bitmap) { + throw new RuntimeException("Stub!"); + } + + public void enableZ() { + throw new RuntimeException("Stub!"); + } + + public void disableZ() { + throw new RuntimeException("Stub!"); + } + + public boolean isOpaque() { + throw new RuntimeException("Stub!"); + } + + public int getWidth() { + throw new RuntimeException("Stub!"); + } + + public int getHeight() { + throw new RuntimeException("Stub!"); + } + + public int getDensity() { + throw new RuntimeException("Stub!"); + } + + public void setDensity(int density) { + throw new RuntimeException("Stub!"); + } + + public int getMaximumBitmapWidth() { + throw new RuntimeException("Stub!"); + } + + public int getMaximumBitmapHeight() { + throw new RuntimeException("Stub!"); + } + + public int save() { + throw new RuntimeException("Stub!"); + } + + /** @deprecated */ + @Deprecated + public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint, int saveFlags) { + throw new RuntimeException("Stub!"); + } + + public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint) { + throw new RuntimeException("Stub!"); + } + /** @deprecated */ + @Deprecated + public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, int saveFlags) { + throw new RuntimeException("Stub!"); + } + + public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha) { + throw new RuntimeException("Stub!"); + } + + public void restore() { + throw new RuntimeException("Stub!"); + } + + public int getSaveCount() { + throw new RuntimeException("Stub!"); + } + + public void restoreToCount(int saveCount) { + throw new RuntimeException("Stub!"); + } + + public void translate(float dx, float dy) { + throw new RuntimeException("Stub!"); + } + + public void scale(float sx, float sy) { + throw new RuntimeException("Stub!"); + } + + public final void scale(float sx, float sy, float px, float py) { + throw new RuntimeException("Stub!"); + } + + public void rotate(float degrees) { + throw new RuntimeException("Stub!"); + } + + public final void rotate(float degrees, float px, float py) { + throw new RuntimeException("Stub!"); + } + + public void skew(float sx, float sy) { + throw new RuntimeException("Stub!"); + } + + public boolean clipRect(float left, float top, float right, float bottom) { + throw new RuntimeException("Stub!"); + } + + public boolean clipOutRect(float left, float top, float right, float bottom) { + throw new RuntimeException("Stub!"); + } + + public boolean clipRect(int left, int top, int right, int bottom) { + throw new RuntimeException("Stub!"); + } + + public boolean clipOutRect(int left, int top, int right, int bottom) { + throw new RuntimeException("Stub!"); + } + + + /** @deprecated */ + @Deprecated + public void drawBitmap(@NonNull int[] colors, int offset, int stride, float x, float y, int width, int height, boolean hasAlpha, @Nullable Paint paint) { + throw new RuntimeException("Stub!"); + } + + /** @deprecated */ + @Deprecated + public void drawBitmap(@NonNull int[] colors, int offset, int stride, int x, int y, int width, int height, boolean hasAlpha, @Nullable Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight, @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset, @Nullable Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawColor(int color) { + throw new RuntimeException("Stub!"); + } + + public void drawColor(long color) { + throw new RuntimeException("Stub!"); + } + + public void drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawLines(@NonNull float[] pts, int offset, int count, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawLines(@NonNull float[] pts, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawPaint(@NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawPoint(float x, float y, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawPoints(float[] pts, int offset, int count, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawPoints(@NonNull float[] pts, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + /** @deprecated */ + @Deprecated + public void drawPosText(@NonNull char[] text, int index, int count, @NonNull float[] pos, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + /** @deprecated */ + @Deprecated + public void drawPosText(@NonNull String text, @NonNull float[] pos, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawRGB(int r, int g, int b) { + throw new RuntimeException("Stub!"); + } + public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawText(@NonNull char[] text, int index, int count, float x, float y, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawText(@NonNull String text, int start, int end, float x, float y, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawText(@NonNull CharSequence text, int start, int end, float x, float y, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawTextRun(@NonNull char[] text, int index, int count, int contextIndex, int contextCount, float x, float y, boolean isRtl, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void drawTextRun(@NonNull CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, @NonNull Paint paint) { + throw new RuntimeException("Stub!"); + } + } diff --git a/WilyCore/src/main/java/android/graphics/Color.java b/WilyCore/src/main/java/android/graphics/Color.java index 6eb7ef13a2e1..5ba1ada90ac4 100644 --- a/WilyCore/src/main/java/android/graphics/Color.java +++ b/WilyCore/src/main/java/android/graphics/Color.java @@ -1,5 +1,7 @@ package android.graphics; +import android.annotation.NonNull; + public class Color { public static final int BLACK = -16777216; public static final int BLUE = -16776961; @@ -18,6 +20,10 @@ public static int argb(int alpha, int red, int green, int blue) { return (alpha << 24) | (red << 16) | (green << 8) | blue; } + public static int rgb(int red, int green, int blue) { + return (red << 16) | (green << 8) | blue; + } + public static void colorToHSV(int color, float[] hsv) { float rNorm = ((color >> 16) & 0xff) / 255.0f; float gNorm = ((color >> 8) & 0xff) / 255.0f; @@ -108,4 +114,104 @@ public static int parseColor(String colorString) { value |= 0xff000000; return value; } + + public static float red(long color) { + throw new RuntimeException("Stub!"); + } + + public static float green(long color) { + throw new RuntimeException("Stub!"); + } + + public static float blue(long color) { + throw new RuntimeException("Stub!"); + } + + public static float alpha(long color) { + throw new RuntimeException("Stub!"); + } + + public static boolean isSrgb(long color) { + throw new RuntimeException("Stub!"); + } + + public static boolean isWideGamut(long color) { + throw new RuntimeException("Stub!"); + } + + public static int toArgb(long color) { + throw new RuntimeException("Stub!"); + } + + @NonNull + public static Color valueOf(int color) { + throw new RuntimeException("Stub!"); + } + + @NonNull + public static Color valueOf(long color) { + throw new RuntimeException("Stub!"); + } + + @NonNull + public static Color valueOf(float r, float g, float b) { + throw new RuntimeException("Stub!"); + } + + @NonNull + public static Color valueOf(float r, float g, float b, float a) { + throw new RuntimeException("Stub!"); + } + + public static long pack(int color) { + throw new RuntimeException("Stub!"); + } + + public static long pack(float red, float green, float blue) { + throw new RuntimeException("Stub!"); + } + + public static long pack(float red, float green, float blue, float alpha) { + throw new RuntimeException("Stub!"); + } + + public static float luminance(long color) { + throw new RuntimeException("Stub!"); + } + + public static int alpha(int color) { + throw new RuntimeException("Stub!"); + } + + public static int red(int color) { + return (color >> 16) & 0xff; + } + + public static int green(int color) { + return (color >> 8) & 0xff; + } + + public static int blue(int color) { + return color & 0xff; + } + + public static int rgb(float red, float green, float blue) { + return rgb((int) red, (int) green, (int) blue); + } + + public static int argb(float alpha, float red, float green, float blue) { + return argb((int) alpha, (int) red, (int) green, (int) blue); + } + + public static float luminance(int color) { + throw new RuntimeException("Stub!"); + } + + public static void RGBToHSV(int red, int green, int blue, float[] hsv) { + throw new RuntimeException("Stub!"); + } + + public static int HSVToColor(int alpha, float[] hsv) { + throw new RuntimeException("Stub!"); + } } diff --git a/WilyCore/src/main/java/android/graphics/Paint.java b/WilyCore/src/main/java/android/graphics/Paint.java new file mode 100644 index 000000000000..6268ac2ab7ef --- /dev/null +++ b/WilyCore/src/main/java/android/graphics/Paint.java @@ -0,0 +1,509 @@ +package android.graphics; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.util.Locale; + +public class Paint { + public static final int ANTI_ALIAS_FLAG = 1; + public static final int CURSOR_AFTER = 0; + public static final int CURSOR_AT = 4; + public static final int CURSOR_AT_OR_AFTER = 1; + public static final int CURSOR_AT_OR_BEFORE = 3; + public static final int CURSOR_BEFORE = 2; + public static final int DEV_KERN_TEXT_FLAG = 256; + public static final int DITHER_FLAG = 4; + public static final int EMBEDDED_BITMAP_TEXT_FLAG = 1024; + public static final int END_HYPHEN_EDIT_INSERT_ARMENIAN_HYPHEN = 3; + public static final int END_HYPHEN_EDIT_INSERT_HYPHEN = 2; + public static final int END_HYPHEN_EDIT_INSERT_MAQAF = 4; + public static final int END_HYPHEN_EDIT_INSERT_UCAS_HYPHEN = 5; + public static final int END_HYPHEN_EDIT_INSERT_ZWJ_AND_HYPHEN = 6; + public static final int END_HYPHEN_EDIT_NO_EDIT = 0; + public static final int END_HYPHEN_EDIT_REPLACE_WITH_HYPHEN = 1; + public static final int FAKE_BOLD_TEXT_FLAG = 32; + public static final int FILTER_BITMAP_FLAG = 2; + public static final int HINTING_OFF = 0; + public static final int HINTING_ON = 1; + public static final int LINEAR_TEXT_FLAG = 64; + public static final int START_HYPHEN_EDIT_INSERT_HYPHEN = 1; + public static final int START_HYPHEN_EDIT_INSERT_ZWJ = 2; + public static final int START_HYPHEN_EDIT_NO_EDIT = 0; + public static final int STRIKE_THRU_TEXT_FLAG = 16; + public static final int SUBPIXEL_TEXT_FLAG = 128; + public static final int UNDERLINE_TEXT_FLAG = 8; + + public Paint() { + throw new RuntimeException("Stub!"); + } + + public Paint(int flags) { + throw new RuntimeException("Stub!"); + } + + public Paint(Paint paint) { + throw new RuntimeException("Stub!"); + } + + public void reset() { + throw new RuntimeException("Stub!"); + } + + public void set(Paint src) { + throw new RuntimeException("Stub!"); + } + + public int getFlags() { + throw new RuntimeException("Stub!"); + } + + public void setFlags(int flags) { + throw new RuntimeException("Stub!"); + } + + public int getHinting() { + throw new RuntimeException("Stub!"); + } + + public void setHinting(int mode) { + throw new RuntimeException("Stub!"); + } + + public final boolean isAntiAlias() { + throw new RuntimeException("Stub!"); + } + + public void setAntiAlias(boolean aa) { + throw new RuntimeException("Stub!"); + } + + public final boolean isDither() { + throw new RuntimeException("Stub!"); + } + + public void setDither(boolean dither) { + throw new RuntimeException("Stub!"); + } + + public final boolean isLinearText() { + throw new RuntimeException("Stub!"); + } + + public void setLinearText(boolean linearText) { + throw new RuntimeException("Stub!"); + } + + public final boolean isSubpixelText() { + throw new RuntimeException("Stub!"); + } + + public void setSubpixelText(boolean subpixelText) { + throw new RuntimeException("Stub!"); + } + + public final boolean isUnderlineText() { + throw new RuntimeException("Stub!"); + } + + public float getUnderlinePosition() { + throw new RuntimeException("Stub!"); + } + + public float getUnderlineThickness() { + throw new RuntimeException("Stub!"); + } + + public void setUnderlineText(boolean underlineText) { + throw new RuntimeException("Stub!"); + } + + public final boolean isStrikeThruText() { + throw new RuntimeException("Stub!"); + } + + public float getStrikeThruPosition() { + throw new RuntimeException("Stub!"); + } + + public float getStrikeThruThickness() { + throw new RuntimeException("Stub!"); + } + + public void setStrikeThruText(boolean strikeThruText) { + throw new RuntimeException("Stub!"); + } + + public final boolean isFakeBoldText() { + throw new RuntimeException("Stub!"); + } + + public void setFakeBoldText(boolean fakeBoldText) { + throw new RuntimeException("Stub!"); + } + + public final boolean isFilterBitmap() { + throw new RuntimeException("Stub!"); + } + + public void setFilterBitmap(boolean filter) { + throw new RuntimeException("Stub!"); + } + + public Style getStyle() { + throw new RuntimeException("Stub!"); + } + + public void setStyle(Style style) { + throw new RuntimeException("Stub!"); + } + + public int getColor() { + throw new RuntimeException("Stub!"); + } + + public long getColorLong() { + throw new RuntimeException("Stub!"); + } + + public void setColor(int color) { + throw new RuntimeException("Stub!"); + } + + public void setColor(long color) { + throw new RuntimeException("Stub!"); + } + + public int getAlpha() { + throw new RuntimeException("Stub!"); + } + + public void setAlpha(int a) { + throw new RuntimeException("Stub!"); + } + + public void setARGB(int a, int r, int g, int b) { + throw new RuntimeException("Stub!"); + } + + public float getStrokeWidth() { + throw new RuntimeException("Stub!"); + } + + public void setStrokeWidth(float width) { + throw new RuntimeException("Stub!"); + } + + public float getStrokeMiter() { + throw new RuntimeException("Stub!"); + } + + public void setStrokeMiter(float miter) { + throw new RuntimeException("Stub!"); + } + + public Cap getStrokeCap() { + throw new RuntimeException("Stub!"); + } + + public void setStrokeCap(Cap cap) { + throw new RuntimeException("Stub!"); + } + + public Join getStrokeJoin() { + throw new RuntimeException("Stub!"); + } + + public void setStrokeJoin(Join join) { + throw new RuntimeException("Stub!"); + } + + public void setShadowLayer(float radius, float dx, float dy, int shadowColor) { + throw new RuntimeException("Stub!"); + } + + public void setShadowLayer(float radius, float dx, float dy, long shadowColor) { + throw new RuntimeException("Stub!"); + } + + public void clearShadowLayer() { + throw new RuntimeException("Stub!"); + } + + public float getShadowLayerRadius() { + throw new RuntimeException("Stub!"); + } + + public float getShadowLayerDx() { + throw new RuntimeException("Stub!"); + } + + public float getShadowLayerDy() { + throw new RuntimeException("Stub!"); + } + + public int getShadowLayerColor() { + throw new RuntimeException("Stub!"); + } + + public long getShadowLayerColorLong() { + throw new RuntimeException("Stub!"); + } + + public Align getTextAlign() { + throw new RuntimeException("Stub!"); + } + + public void setTextAlign(Align align) { + throw new RuntimeException("Stub!"); + } + + @NonNull + public Locale getTextLocale() { + throw new RuntimeException("Stub!"); + } + + public void setTextLocale(@NonNull Locale locale) { + throw new RuntimeException("Stub!"); + } + + public boolean isElegantTextHeight() { + throw new RuntimeException("Stub!"); + } + + public void setElegantTextHeight(boolean elegant) { + throw new RuntimeException("Stub!"); + } + + public float getTextSize() { + throw new RuntimeException("Stub!"); + } + + public void setTextSize(float textSize) { + throw new RuntimeException("Stub!"); + } + + public float getTextScaleX() { + throw new RuntimeException("Stub!"); + } + + public void setTextScaleX(float scaleX) { + throw new RuntimeException("Stub!"); + } + + public float getTextSkewX() { + throw new RuntimeException("Stub!"); + } + + public void setTextSkewX(float skewX) { + throw new RuntimeException("Stub!"); + } + + public float getLetterSpacing() { + throw new RuntimeException("Stub!"); + } + + public void setLetterSpacing(float letterSpacing) { + throw new RuntimeException("Stub!"); + } + + public float getWordSpacing() { + throw new RuntimeException("Stub!"); + } + + public void setWordSpacing(float wordSpacing) { + throw new RuntimeException("Stub!"); + } + + public String getFontFeatureSettings() { + throw new RuntimeException("Stub!"); + } + + public void setFontFeatureSettings(String settings) { + throw new RuntimeException("Stub!"); + } + + public String getFontVariationSettings() { + throw new RuntimeException("Stub!"); + } + + public boolean setFontVariationSettings(String fontVariationSettings) { + throw new RuntimeException("Stub!"); + } + + public int getStartHyphenEdit() { + throw new RuntimeException("Stub!"); + } + + public int getEndHyphenEdit() { + throw new RuntimeException("Stub!"); + } + + public void setStartHyphenEdit(int startHyphen) { + throw new RuntimeException("Stub!"); + } + + public void setEndHyphenEdit(int endHyphen) { + throw new RuntimeException("Stub!"); + } + + public float ascent() { + throw new RuntimeException("Stub!"); + } + + public float descent() { + throw new RuntimeException("Stub!"); + } + + public float getFontMetrics(FontMetrics metrics) { + throw new RuntimeException("Stub!"); + } + + public FontMetrics getFontMetrics() { + throw new RuntimeException("Stub!"); + } + + public int getFontMetricsInt(FontMetricsInt fmi) { + throw new RuntimeException("Stub!"); + } + + public FontMetricsInt getFontMetricsInt() { + throw new RuntimeException("Stub!"); + } + + public float getFontSpacing() { + throw new RuntimeException("Stub!"); + } + + public float measureText(char[] text, int index, int count) { + throw new RuntimeException("Stub!"); + } + + public float measureText(String text, int start, int end) { + throw new RuntimeException("Stub!"); + } + + public float measureText(String text) { + throw new RuntimeException("Stub!"); + } + + public float measureText(CharSequence text, int start, int end) { + throw new RuntimeException("Stub!"); + } + + public int breakText(char[] text, int index, int count, float maxWidth, float[] measuredWidth) { + throw new RuntimeException("Stub!"); + } + + public int breakText(CharSequence text, int start, int end, boolean measureForwards, float maxWidth, float[] measuredWidth) { + throw new RuntimeException("Stub!"); + } + + public int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth) { + throw new RuntimeException("Stub!"); + } + + public int getTextWidths(char[] text, int index, int count, float[] widths) { + throw new RuntimeException("Stub!"); + } + + public int getTextWidths(CharSequence text, int start, int end, float[] widths) { + throw new RuntimeException("Stub!"); + } + + public int getTextWidths(String text, int start, int end, float[] widths) { + throw new RuntimeException("Stub!"); + } + + public int getTextWidths(String text, float[] widths) { + throw new RuntimeException("Stub!"); + } + + public float getTextRunAdvances(@NonNull char[] chars, int index, int count, int contextIndex, int contextCount, boolean isRtl, @Nullable float[] advances, int advancesIndex) { + throw new RuntimeException("Stub!"); + } + + public int getTextRunCursor(@NonNull char[] text, int contextStart, int contextLength, boolean isRtl, int offset, int cursorOpt) { + throw new RuntimeException("Stub!"); + } + + public int getTextRunCursor(@NonNull CharSequence text, int contextStart, int contextEnd, boolean isRtl, int offset, int cursorOpt) { + throw new RuntimeException("Stub!"); + } + + public boolean hasGlyph(String string) { + throw new RuntimeException("Stub!"); + } + + public float getRunAdvance(char[] text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset) { + throw new RuntimeException("Stub!"); + } + + public float getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset) { + throw new RuntimeException("Stub!"); + } + + public int getOffsetForAdvance(char[] text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance) { + throw new RuntimeException("Stub!"); + } + + public int getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance) { + throw new RuntimeException("Stub!"); + } + + public boolean equalsForTextMeasurement(@NonNull Paint other) { + throw new RuntimeException("Stub!"); + } + + public static enum Align { + LEFT, + CENTER, + RIGHT; + } + + public static enum Cap { + BUTT, + ROUND, + SQUARE; + } + + public static class FontMetrics { + public float ascent; + public float bottom; + public float descent; + public float leading; + public float top; + + public FontMetrics() { + throw new RuntimeException("Stub!"); + } + } + + public static class FontMetricsInt { + public int ascent; + public int bottom; + public int descent; + public int leading; + public int top; + + public FontMetricsInt() { + throw new RuntimeException("Stub!"); + } + + public String toString() { + throw new RuntimeException("Stub!"); + } + } + + public static enum Join { + MITER, + ROUND, + BEVEL; + } + + public static enum Style { + FILL, + STROKE, + FILL_AND_STROKE; + } +} diff --git a/WilyCore/src/main/java/androidx/annotation/ColorInt.java b/WilyCore/src/main/java/androidx/annotation/ColorInt.java new file mode 100644 index 000000000000..db72a7bfcf78 --- /dev/null +++ b/WilyCore/src/main/java/androidx/annotation/ColorInt.java @@ -0,0 +1,27 @@ +package androidx.annotation; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Denotes that the annotated element represents a packed color + * int, {@code AARRGGBB}. If applied to an int array, every element + * in the array represents a color integer. + *

+ * Example: + *

{@code
+ *  public abstract void setTextColor(@ColorInt int color);
+ * }
+ */ +@Documented +@Retention(CLASS) +@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) +public @interface ColorInt { +} diff --git a/WilyCore/src/main/java/com/qualcomm/ftccommon/FtcEventLoop.java b/WilyCore/src/main/java/com/qualcomm/ftccommon/FtcEventLoop.java index ec625044b355..1f0ba0255db0 100644 --- a/WilyCore/src/main/java/com/qualcomm/ftccommon/FtcEventLoop.java +++ b/WilyCore/src/main/java/com/qualcomm/ftccommon/FtcEventLoop.java @@ -4,6 +4,6 @@ public class FtcEventLoop { public OpModeManagerImpl getOpModeManager() { - return null; + return new OpModeManagerImpl(); } } diff --git a/WilyCore/src/main/java/org/swerverobotics/ftc/GoBildaPinpointDriver.java b/WilyCore/src/main/java/com/qualcomm/hardware/gobilda/GoBildaPinpointDriver.java similarity index 68% rename from WilyCore/src/main/java/org/swerverobotics/ftc/GoBildaPinpointDriver.java rename to WilyCore/src/main/java/com/qualcomm/hardware/gobilda/GoBildaPinpointDriver.java index f37c1c38c76d..bf02a2732133 100644 --- a/WilyCore/src/main/java/org/swerverobotics/ftc/GoBildaPinpointDriver.java +++ b/WilyCore/src/main/java/com/qualcomm/hardware/gobilda/GoBildaPinpointDriver.java @@ -1,28 +1,24 @@ -package org.swerverobotics.ftc; - -import static com.qualcomm.robotcore.util.TypeConversion.byteArrayToInt; +package com.qualcomm.hardware.gobilda; import com.acmerobotics.roadrunner.Pose2d; import com.acmerobotics.roadrunner.PoseVelocity2d; -import com.qualcomm.robotcore.hardware.I2cAddr; import com.qualcomm.robotcore.hardware.I2cDeviceSynchDevice; import com.qualcomm.robotcore.hardware.I2cDeviceSynchSimple; import com.qualcomm.robotcore.hardware.configuration.annotations.DeviceProperties; import com.qualcomm.robotcore.hardware.configuration.annotations.I2cDeviceType; -import com.qualcomm.robotcore.util.TypeConversion; import com.wilyworks.simulator.WilyCore; import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; import org.firstinspires.ftc.robotcore.external.navigation.Pose2D; +import org.firstinspires.ftc.robotcore.external.navigation.UnnormalizedAngleUnit; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.Arrays; @I2cDeviceType @DeviceProperties( - name = "goBILDA® Pinpoint Odometry Computer", + name = "Home-compiled goBILDA® Pinpoint Odometry Computer", xmlTag = "goBILDAPinpoint", description ="goBILDA® Pinpoint Odometry Computer (IMU Sensor Fusion for 2 Wheel Odometry)" ) @@ -79,7 +75,7 @@ private enum Register { X_VELOCITY (11), Y_VELOCITY (12), H_VELOCITY (13), - MM_PER_TICK (14), + TICKS_PER_MM (14), X_POD_OFFSET (15), Y_POD_OFFSET (16), YAW_SCALAR (17), @@ -94,13 +90,14 @@ private enum Register { //Device Status enum that captures the current fault condition of the device public enum DeviceStatus{ - NOT_READY (0), - READY (1), - CALIBRATING (1 << 1), - FAULT_X_POD_NOT_DETECTED (1 << 2), - FAULT_Y_POD_NOT_DETECTED (1 << 3), - FAULT_NO_PODS_DETECTED (1 << 2 | 1 << 3), - FAULT_IMU_RUNAWAY (1 << 4); + NOT_READY(0), + READY(1), + CALIBRATING(1 << 1), + FAULT_X_POD_NOT_DETECTED(1 << 2), + FAULT_Y_POD_NOT_DETECTED(1 << 3), + FAULT_NO_PODS_DETECTED(1 << 2 | 1 << 3), + FAULT_IMU_RUNAWAY(1 << 4), + FAULT_BAD_READ(1 << 5); private final int status; @@ -121,7 +118,7 @@ public enum GoBildaOdometryPods { goBILDA_4_BAR_POD; } //enum that captures a limited scope of read data. More options may be added in future update - public enum readData { + public enum ReadData { ONLY_UPDATE_HEADING, } @@ -130,7 +127,7 @@ public enum readData { @param reg the register to write the int to @param i the integer to write to the register */ - private void writeInt(final Register reg, int i){ + private void writeInt(final GoBildaPinpointDriver.Register reg, int i){ } /** @@ -138,7 +135,7 @@ private void writeInt(final Register reg, int i){ * @param reg the register to read from * @return returns an int that contains the value stored in the read register */ - private int readInt(Register reg){ + private int readInt(GoBildaPinpointDriver.Register reg){ return 0; } @@ -157,7 +154,7 @@ private float byteArrayToFloat(byte[] byteArray, ByteOrder byteOrder){ * @return the float value stored in that register */ - private float readFloat(Register reg){ + private float readFloat(GoBildaPinpointDriver.Register reg){ return 0; } @@ -176,7 +173,7 @@ private float readFloat(Register reg){ * @param reg the register to write to * @param bytes the byte array to write */ - private void writeByteArray (Register reg, byte[] bytes){ + private void writeByteArray (GoBildaPinpointDriver.Register reg, byte[] bytes){ } @@ -185,7 +182,7 @@ private void writeByteArray (Register reg, byte[] bytes){ * @param reg the register to write to * @param f the float to write */ - private void writeFloat (Register reg, float f){ + private void writeFloat (GoBildaPinpointDriver.Register reg, float f){ } /** @@ -193,30 +190,30 @@ private void writeFloat (Register reg, float f){ * @param s int to lookup * @return the Odometry Computer state */ - private DeviceStatus lookupStatus (int s){ - if ((s & DeviceStatus.CALIBRATING.status) != 0){ - return DeviceStatus.CALIBRATING; + private GoBildaPinpointDriver.DeviceStatus lookupStatus (int s){ + if ((s & GoBildaPinpointDriver.DeviceStatus.CALIBRATING.status) != 0){ + return GoBildaPinpointDriver.DeviceStatus.CALIBRATING; } - boolean xPodDetected = (s & DeviceStatus.FAULT_X_POD_NOT_DETECTED.status) == 0; - boolean yPodDetected = (s & DeviceStatus.FAULT_Y_POD_NOT_DETECTED.status) == 0; + boolean xPodDetected = (s & GoBildaPinpointDriver.DeviceStatus.FAULT_X_POD_NOT_DETECTED.status) == 0; + boolean yPodDetected = (s & GoBildaPinpointDriver.DeviceStatus.FAULT_Y_POD_NOT_DETECTED.status) == 0; if(!xPodDetected && !yPodDetected){ - return DeviceStatus.FAULT_NO_PODS_DETECTED; + return GoBildaPinpointDriver.DeviceStatus.FAULT_NO_PODS_DETECTED; } if (!xPodDetected){ - return DeviceStatus.FAULT_X_POD_NOT_DETECTED; + return GoBildaPinpointDriver.DeviceStatus.FAULT_X_POD_NOT_DETECTED; } if (!yPodDetected){ - return DeviceStatus.FAULT_Y_POD_NOT_DETECTED; + return GoBildaPinpointDriver.DeviceStatus.FAULT_Y_POD_NOT_DETECTED; } - if ((s & DeviceStatus.FAULT_IMU_RUNAWAY.status) != 0){ - return DeviceStatus.FAULT_IMU_RUNAWAY; + if ((s & GoBildaPinpointDriver.DeviceStatus.FAULT_IMU_RUNAWAY.status) != 0){ + return GoBildaPinpointDriver.DeviceStatus.FAULT_IMU_RUNAWAY; } - if ((s & DeviceStatus.READY.status) != 0){ - return DeviceStatus.READY; + if ((s & GoBildaPinpointDriver.DeviceStatus.READY.status) != 0){ + return GoBildaPinpointDriver.DeviceStatus.READY; } else { - return DeviceStatus.NOT_READY; + return GoBildaPinpointDriver.DeviceStatus.NOT_READY; } } @@ -232,8 +229,8 @@ public void update(){ * is supported. * @param data GoBildaPinpointDriver.readData.ONLY_UPDATE_HEADING */ - public void update(readData data) { - if (data == readData.ONLY_UPDATE_HEADING) { + public void update(ReadData data) { + if (data == ReadData.ONLY_UPDATE_HEADING) { hOrientation = 0; } } @@ -241,14 +238,16 @@ public void update(readData data) { /** * Sets the odometry pod positions relative to the point that the odometry computer tracks around.

* The most common tracking position is the center of the robot.

- * The X pod offset refers to how far sideways (in mm) from the tracking point the X (forward) odometry pod is. Left of the center is a positive number, right of center is a negative number.
- * the Y pod offset refers to how far forwards (in mm) from the tracking point the Y (strafe) odometry pod is. forward of center is a positive number, backwards is a negative number.
- * @param xOffset how sideways from the center of the robot is the X (forward) pod? Left increases - * @param yOffset how far forward from the center of the robot is the Y (Strafe) pod? forward increases - */ - public void setOffsets(double xOffset, double yOffset){ - writeFloat(Register.X_POD_OFFSET, (float) xOffset); - writeFloat(Register.Y_POD_OFFSET, (float) yOffset); + * The X pod offset refers to how far sideways from the tracking point the X (forward) odometry pod is. Left of the center is a positive number, right of center is a negative number.
+ * the Y pod offset refers to how far forwards from the tracking point the Y (strafe) odometry pod is. forward of center is a positive number, backwards is a negative number.
+ * + * @param xOffset how sideways from the center of the robot is the X (forward) pod? Left increases + * @param yOffset how far forward from the center of the robot is the Y (Strafe) pod? forward increases + * @param distanceUnit the unit of distance used for offsets. + */ + public void setOffsets(double xOffset, double yOffset, DistanceUnit distanceUnit) { + writeFloat(Register.X_POD_OFFSET, (float) distanceUnit.toMm(xOffset)); + writeFloat(Register.Y_POD_OFFSET, (float) distanceUnit.toMm(yOffset)); } /** @@ -256,56 +255,62 @@ public void setOffsets(double xOffset, double yOffset){ * Robot MUST be stationary

* Device takes a large number of samples, and uses those as the gyroscope zero-offset. This takes approximately 0.25 seconds. */ - public void recalibrateIMU(){writeInt(Register.DEVICE_CONTROL,1<<0);} + public void recalibrateIMU(){writeInt(GoBildaPinpointDriver.Register.DEVICE_CONTROL,1<<0);} /** * Resets the current position to 0,0,0 and recalibrates the Odometry Computer's internal IMU.

* Robot MUST be stationary

* Device takes a large number of samples, and uses those as the gyroscope zero-offset. This takes approximately 0.25 seconds. */ - public void resetPosAndIMU(){writeInt(Register.DEVICE_CONTROL,1<<1);} + public void resetPosAndIMU(){writeInt(GoBildaPinpointDriver.Register.DEVICE_CONTROL,1<<1);} /** * Can reverse the direction of each encoder. * @param xEncoder FORWARD or REVERSED, X (forward) pod should increase when the robot is moving forward * @param yEncoder FORWARD or REVERSED, Y (strafe) pod should increase when the robot is moving left */ - public void setEncoderDirections(EncoderDirection xEncoder, EncoderDirection yEncoder){ - if (xEncoder == EncoderDirection.FORWARD){ - writeInt(Register.DEVICE_CONTROL,1<<5); + public void setEncoderDirections(GoBildaPinpointDriver.EncoderDirection xEncoder, GoBildaPinpointDriver.EncoderDirection yEncoder){ + if (xEncoder == GoBildaPinpointDriver.EncoderDirection.FORWARD){ + writeInt(GoBildaPinpointDriver.Register.DEVICE_CONTROL,1<<5); } - if (xEncoder == EncoderDirection.REVERSED) { - writeInt(Register.DEVICE_CONTROL,1<<4); + if (xEncoder == GoBildaPinpointDriver.EncoderDirection.REVERSED) { + writeInt(GoBildaPinpointDriver.Register.DEVICE_CONTROL,1<<4); } - if (yEncoder == EncoderDirection.FORWARD){ - writeInt(Register.DEVICE_CONTROL,1<<3); + if (yEncoder == GoBildaPinpointDriver.EncoderDirection.FORWARD){ + writeInt(GoBildaPinpointDriver.Register.DEVICE_CONTROL,1<<3); } - if (yEncoder == EncoderDirection.REVERSED){ - writeInt(Register.DEVICE_CONTROL,1<<2); + if (yEncoder == GoBildaPinpointDriver.EncoderDirection.REVERSED){ + writeInt(GoBildaPinpointDriver.Register.DEVICE_CONTROL,1<<2); } } + /** - * If you're using goBILDA odometry pods, the ticks-per-mm values are stored here for easy access.

+ * This allows you to set the encoder resolution by the type of GoBilda pod you are using. + * If you aren't using a GoBilda pod, use setEncoderResolution(double) instead.

+ * * @param pods goBILDA_SWINGARM_POD or goBILDA_4_BAR_POD */ - public void setEncoderResolution(GoBildaOdometryPods pods){ + public void setEncoderResolution(GoBildaOdometryPods pods) { if (pods == GoBildaOdometryPods.goBILDA_SWINGARM_POD) { - writeByteArray(Register.MM_PER_TICK, (floatToByteArray(goBILDA_SWINGARM_POD, ByteOrder.LITTLE_ENDIAN))); + setEncoderResolution(goBILDA_SWINGARM_POD, DistanceUnit.MM); } - if (pods == GoBildaOdometryPods.goBILDA_4_BAR_POD){ - writeByteArray(Register.MM_PER_TICK,(floatToByteArray(goBILDA_4_BAR_POD, ByteOrder.LITTLE_ENDIAN))); + if (pods == GoBildaOdometryPods.goBILDA_4_BAR_POD) { + setEncoderResolution(goBILDA_4_BAR_POD, DistanceUnit.MM); } } /** * Sets the encoder resolution in ticks per mm of the odometry pods.
* You can find this number by dividing the counts-per-revolution of your encoder by the circumference of the wheel. - * @param ticks_per_mm should be somewhere between 10 ticks/mm and 100 ticks/mm a goBILDA Swingarm pod is ~13.26291192 + * + * @param ticksPerUnit should be somewhere between 10 ticks/mm and 100 ticks/mm a goBILDA Swingarm pod is ~13.26291192 + * @param distanceUnit unit used for distance */ - public void setEncoderResolution(double ticks_per_mm){ - writeByteArray(Register.MM_PER_TICK,(floatToByteArray((float) ticks_per_mm,ByteOrder.LITTLE_ENDIAN))); + public void setEncoderResolution(double ticksPerUnit, DistanceUnit distanceUnit) { + double resolution = distanceUnit.toMm(ticksPerUnit); + writeByteArray(Register.TICKS_PER_MM, (floatToByteArray((float) resolution, ByteOrder.LITTLE_ENDIAN))); } /** @@ -318,7 +323,7 @@ public void setEncoderResolution(double ticks_per_mm){ * @param yawOffset A scalar for the robot's heading. */ public void setYawScalar(double yawOffset){ - writeByteArray(Register.YAW_SCALAR,(floatToByteArray((float) yawOffset, ByteOrder.LITTLE_ENDIAN))); + writeByteArray(GoBildaPinpointDriver.Register.YAW_SCALAR,(floatToByteArray((float) yawOffset, ByteOrder.LITTLE_ENDIAN))); } /** @@ -344,9 +349,9 @@ public void setYawScalar(double yawOffset){ * @param pos a Pose2D describing the robot's new position. */ public Pose2D setPosition(Pose2D pos){ - writeByteArray(Register.X_POSITION,(floatToByteArray((float) pos.getX(DistanceUnit.MM), ByteOrder.LITTLE_ENDIAN))); - writeByteArray(Register.Y_POSITION,(floatToByteArray((float) pos.getY(DistanceUnit.MM),ByteOrder.LITTLE_ENDIAN))); - writeByteArray(Register.H_ORIENTATION,(floatToByteArray((float) pos.getHeading(AngleUnit.RADIANS),ByteOrder.LITTLE_ENDIAN))); + writeByteArray(GoBildaPinpointDriver.Register.X_POSITION,(floatToByteArray((float) pos.getX(DistanceUnit.MM), ByteOrder.LITTLE_ENDIAN))); + writeByteArray(GoBildaPinpointDriver.Register.Y_POSITION,(floatToByteArray((float) pos.getY(DistanceUnit.MM),ByteOrder.LITTLE_ENDIAN))); + writeByteArray(GoBildaPinpointDriver.Register.H_ORIENTATION,(floatToByteArray((float) pos.getHeading(AngleUnit.RADIANS),ByteOrder.LITTLE_ENDIAN))); return pos; } @@ -354,14 +359,14 @@ public Pose2D setPosition(Pose2D pos){ * Checks the deviceID of the Odometry Computer. Should return 1. * @return 1 if device is functional. */ - public int getDeviceID(){return readInt(Register.DEVICE_ID);} + public int getDeviceID(){return readInt(GoBildaPinpointDriver.Register.DEVICE_ID);} /** * @return the firmware version of the Odometry Computer */ - public int getDeviceVersion(){return readInt(Register.DEVICE_VERSION); } + public int getDeviceVersion(){return readInt(GoBildaPinpointDriver.Register.DEVICE_VERSION); } - public float getYawScalar(){return readFloat(Register.YAW_SCALAR); } + public float getYawScalar(){return readFloat(GoBildaPinpointDriver.Register.YAW_SCALAR); } /** * Device Status stores any faults the Odometry Computer may be experiencing. These faults include: @@ -373,7 +378,7 @@ public Pose2D setPosition(Pose2D pos){ * FAULT_X_POD_NOT_DETECTED - The device does not detect an X pod plugged in. BLUE LED
* FAULT_Y_POD_NOT_DETECTED - The device does not detect a Y pod plugged in. ORANGE LED
*/ - public DeviceStatus getDeviceStatus(){return DeviceStatus.READY; } + public GoBildaPinpointDriver.DeviceStatus getDeviceStatus(){return GoBildaPinpointDriver.DeviceStatus.READY; } /** * Checks the Odometry Computer's most recent loop time.

@@ -419,34 +424,43 @@ public double getFrequency(){ /** * @return the estimated H (heading) position of the robot in Radians */ - public double getHeading(){return hOrientation;} + public double getHeading(AngleUnit angleUnit) { + return angleUnit.fromRadians(hOrientation); + } + /** - * @return the estimated X (forward) velocity of the robot in mm/sec + * @return the estimated X (forward) velocity of the robot in specified unit/sec */ - public double getVelX(){return xVelocity; } + public double getVelX(DistanceUnit distanceUnit) { + return distanceUnit.fromMm(xVelocity); + } /** - * @return the estimated Y (strafe) velocity of the robot in mm/sec + * @return the estimated Y (strafe) velocity of the robot in specified unit/sec */ - public double getVelY(){return yVelocity; } + public double getVelY(DistanceUnit distanceUnit) { + return distanceUnit.fromMm(yVelocity); + } /** - * @return the estimated H (heading) velocity of the robot in radians/sec + * @return the estimated H (heading) velocity of the robot in specified unit/sec */ - public double getHeadingVelocity(){return hVelocity; } + public double getHeadingVelocity(UnnormalizedAngleUnit unnormalizedAngleUnit) { + return unnormalizedAngleUnit.fromRadians(hVelocity); + } /** * This uses its own I2C read, avoid calling this every loop. * @return the user-set offset for the X (forward) pod */ - public float getXOffset(){return readFloat(Register.X_POD_OFFSET);} + public float getXOffset(){return readFloat(GoBildaPinpointDriver.Register.X_POD_OFFSET);} /** * This uses its own I2C read, avoid calling this every loop. * @return the user-set offset for the Y (strafe) pod */ - public float getYOffset(){return readFloat(Register.Y_POD_OFFSET);} + public float getYOffset(){return readFloat(GoBildaPinpointDriver.Register.Y_POD_OFFSET);} /** * @return a Pose2D containing the estimated position of the robot diff --git a/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLFieldMap.java b/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLFieldMap.java new file mode 100644 index 000000000000..6fc9dc380ad6 --- /dev/null +++ b/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLFieldMap.java @@ -0,0 +1,251 @@ +package com.qualcomm.hardware.limelightvision; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a field map containing fiducial markers / AprilTags. + */ +public class LLFieldMap { + + /** + * Represents a fiducial marker / AprilTag in the field map. + */ + public static class Fiducial { + private int id; + private double size; + private String family; + private List transform; + private boolean isUnique; + + /** + * Constructs a Fiducial with default values. + */ + public Fiducial() { + this(-1, 165.1, "apriltag3_36h11_classic", new ArrayList<>(16), true); + } + + /** + * Constructs a Fiducial with specified values. + * + * @param id The ID of the fiducial. + * @param size The size of the fiducial. + * @param family The family of the fiducial. + * @param transform The transformation of the fiducial. + * @param isUnique Whether the fiducial is unique. + */ + public Fiducial(int id, double size, String family, List transform, boolean isUnique) { + this.id = id; + this.size = size; + this.family = family; + this.transform = new ArrayList<>(transform); + this.isUnique = isUnique; + } + + /** + * Constructs a Fiducial from JSON data. Returns a default Fidcial if JSON is null or malformed + * + * @param json The JSON object containing fiducial data. + */ + protected Fiducial(JSONObject json) { + this(); //empy Fiducial + if(json != null) + { + try{ + this.id = json.optInt("id", -1); + this.size = json.optDouble("size", 165.1); + this.family = json.optString("family", "apriltag3_36h11_classic"); + this.transform = new ArrayList<>(); + JSONArray transformArray = json.optJSONArray("transform"); + if (transformArray != null) { + for (int i = 0; i < transformArray.length(); i++) { + this.transform.add(transformArray.optDouble(i, 0.0)); + } + } + this.isUnique = json.optBoolean("unique", true); + } catch (Exception e) { + + } + } + } + + /** + * Gets the ID / index of the fiducial. + * + * @return The fiducial ID. + */ + public int getId() { + return id; + } + + /** + * Gets the size of the fiducial in millimeters + * + * @return The fiducial size. + */ + public double getSize() { + return size; + } + + /** + * Gets the family of the fiducial. eg "apriltag3_36h11_classic" + * + * @return The fiducial family. + */ + public String getFamily() { + return family; + } + + /** + * Gets the 4x4 transforms matrix of the fiducial. + * + * @return A copy of the fiducial's transform. + */ + public List getTransform() { + return new ArrayList<>(transform); + } + + /** + * Checks if the fiducial is marked as unique. + * + * @return true if the fiducial is unique, false otherwise. + */ + public boolean isUnique() { + return isUnique; + } + + /** + * Converts the Fiducial to a JSONObject. + * + * @return A JSONObject representation of the Fiducial. + */ + protected JSONObject toJson() { + JSONObject json = new JSONObject(); + try { + json.put("id", id); + json.put("size", size); + json.put("family", family); + json.put("transform", new JSONArray(transform)); + json.put("unique", isUnique); + } catch (Exception e){ + + } + return json; + } + } + + private List fiducials; + private String type; + + /** + * Constructs an empty LLFieldMap. + */ + public LLFieldMap() { + this(new ArrayList<>(), ""); + } + + /** + * Constructs an LLFieldMap with specified fiducials and type. + * + * @param fiducials The list of fiducials in the field map. + * @param type The type of the field map. + */ + public LLFieldMap(List fiducials, String type) { + this.fiducials = new ArrayList<>(fiducials); + this.type = type; + } + + /** + * Constructs an LLFieldMap from JSON data. Returns an empty map if json is null or malformed + * + * @param json The JSON object containing field map data. + */ + protected LLFieldMap(JSONObject json) { + this(); + if (json != null) { + try{ + this.type = json.optString("type", ""); + JSONArray fiducialsArray = json.optJSONArray("fiducials"); + if (fiducialsArray != null) { + for (int i = 0; i < fiducialsArray.length(); i++) { + JSONObject fiducialJson = fiducialsArray.optJSONObject(i); + if (fiducialJson != null) { + this.fiducials.add(new Fiducial(fiducialJson)); + } + } + } + } + catch (Exception e){ + } + } + } + + /** + * Gets the list of fiducials in the field map. + * + * @return A copy of the list of fiducials. + */ + public List getFiducials() { + return new ArrayList<>(fiducials); + } + + /** + * Gets the type of the field map. (eg "ftc" or "frc") + * + * @return The field map type. + */ + public String getType() { + return type; + } + + /** + * Gets the number of tags (fiducials) in the field map. + * + * @return The number of tags. + */ + public int getNumberOfTags() { + return fiducials.size(); + } + + /** + * Get validity of map. Maps are valid if they have more than zero tags and have a specified type + * + * @return True if valid. False otherwise + */ + public boolean isValid() + { + if(getNumberOfTags() == 0) + { + //Likely user error. + return false; + } + if((getType() != "ftc") && (getType() != "frc")) + { + //Likely user error. + return false; + } + return true; + } + + /** + * Converts the LLFieldMap to a JSONObject. + * + * @return A JSONObject representation of the LLFieldMap. + */ + protected JSONObject toJson() { + JSONObject json = new JSONObject(); + try { + JSONArray fiducialsArray = new JSONArray(); + for (Fiducial fiducial : fiducials) { + fiducialsArray.put(fiducial.toJson()); + } + json.put("fiducials", fiducialsArray); + json.put("type", type); + } catch (Exception e) { + } + return json; + } +} \ No newline at end of file diff --git a/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLResult.java b/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLResult.java new file mode 100644 index 000000000000..d53848613ed1 --- /dev/null +++ b/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLResult.java @@ -0,0 +1,532 @@ +package com.qualcomm.hardware.limelightvision; + +/* +Copyright (c) 2024 Limelight Vision + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Limelight Vision nor the names of its contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import com.qualcomm.hardware.limelightvision.LLResultTypes.BarcodeResult; +import com.qualcomm.hardware.limelightvision.LLResultTypes.ClassifierResult; +import com.qualcomm.hardware.limelightvision.LLResultTypes.ColorResult; +import com.qualcomm.hardware.limelightvision.LLResultTypes.DetectorResult; +import com.qualcomm.hardware.limelightvision.LLResultTypes.FiducialResult; + +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.robotcore.external.navigation.Pose3D; +import org.firstinspires.ftc.robotcore.external.navigation.Position; +import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the result of a Limelight Pipeline. This class parses JSON data from a Limelight + * in the constructor and provides easy access to the results data. + */ +public class LLResult { + private JSONObject jsonData; + private List barcodeResults; + private List classifierResults; + private List detectorResults; + private List fiducialResults; + private List colorResults; + private double parseLatency; + private long controlHubTimeStamp; + + /** + * Constructs an LLResult object from a JSONObject. + * + * @param json The JSONObject containing Limelight vision data. + * @throws JSONException If there's an error parsing the JSON string. + */ + protected LLResult(JSONObject json) throws JSONException { + this.jsonData = json; + this.barcodeResults = new ArrayList<>(); + this.classifierResults = new ArrayList<>(); + this.detectorResults = new ArrayList<>(); + this.fiducialResults = new ArrayList<>(); + this.colorResults = new ArrayList<>(); + this.parseLatency = 0.0; + setControlHubTimeStamp(System.currentTimeMillis()); + parseResults(); + } + + /** + * Sets the timestamp from the control hub in milliseconds. + * + * @param timestamp The timestamp to set in milliseconds. + */ + void setControlHubTimeStamp(long timestamp) { + this.controlHubTimeStamp = timestamp; + } + + /** + * Gets the control hub timestamp in milliseconds. + * + * @return The control hub timestamp in milliseconds. + */ + public long getControlHubTimeStamp() { + return this.controlHubTimeStamp; + } + + /** + * Gets the control hub timestamp in nanoseconds. + * + * @return The control hub timestamp in nanoseconds. + */ + public long getControlHubTimeStampNanos() { + return this.controlHubTimeStamp * 1000000; + } + + + /** + * Calculates the staleness of the data. + * + * @return The staleness in milliseconds. + */ + public long getStaleness() { + return System.currentTimeMillis() - this.controlHubTimeStamp; + } + + /** + * Parses the JSON data into result objects. + * + * @throws JSONException If there's an error parsing the JSON data. + */ + private void parseResults() throws JSONException { + long startTime = System.nanoTime(); + + JSONArray barcodeArray = jsonData.optJSONArray("Barcode"); + if (barcodeArray != null) { + for (int i = 0; i < barcodeArray.length(); i++) { + barcodeResults.add(new BarcodeResult(barcodeArray.getJSONObject(i))); + } + } + + JSONArray classifierArray = jsonData.optJSONArray("Classifier"); + if (classifierArray != null) { + for (int i = 0; i < classifierArray.length(); i++) { + classifierResults.add(new ClassifierResult(classifierArray.getJSONObject(i))); + } + } + + JSONArray detectorArray = jsonData.optJSONArray("Detector"); + if (detectorArray != null) { + for (int i = 0; i < detectorArray.length(); i++) { + detectorResults.add(new DetectorResult(detectorArray.getJSONObject(i))); + } + } + + JSONArray fiducialArray = jsonData.optJSONArray("Fiducial"); + if (fiducialArray != null) { + for (int i = 0; i < fiducialArray.length(); i++) { + fiducialResults.add(new FiducialResult(fiducialArray.getJSONObject(i))); + } + } + + JSONArray colorArray = jsonData.optJSONArray("Retro"); + if (colorArray != null) { + for (int i = 0; i < colorArray.length(); i++) { + colorResults.add(new ColorResult(colorArray.getJSONObject(i))); + } + } + + long endTime = System.nanoTime(); + this.parseLatency = (endTime - startTime) / 1e6; // Convert to milliseconds + } + + /** + * Gets the list of barcode results. + * + * @return A list of BarcodeResult objects. + */ + public List getBarcodeResults() { + return barcodeResults; + } + + /** + * Gets the list of classifier results. + * + * @return A list of ClassifierResult objects. + */ + public List getClassifierResults() { + return classifierResults; + } + + /** + * Gets the list of detector results. + * + * @return A list of DetectorResult objects. + */ + public List getDetectorResults() { + return detectorResults; + } + + /** + * Gets the list of fiducial/apriltag results. + * + * @return A list of FiducialResult objects. + */ + public List getFiducialResults() { + return fiducialResults; + } + + /** + * Gets the list of color results. + * + * @return A list of ColorResult objects. + */ + public List getColorResults() { + return colorResults; + } + + /** + * Gets the focus metric of the image. This is only valid if the focus pipeline is enabled + * + * @return The focus metric value. + */ + public double getFocusMetric() { + return jsonData.optDouble("focus_metric", 0); + } + + /** + * Gets the 3D botpose. + * + * @return An Pose3D representing the bot pose. + */ + public Pose3D getBotpose() { + return createPose3DRobot(getDoubleArray("botpose", 6)); + } + + /** + * Gets the 3D botpose using the WPILIB Blue Alliance Coordinate System. + * + * @return An Pose3D representing the bot pose. + */ + private Pose3D getBotposeWpiblue() { + return createPose3DRobot(getDoubleArray("botpose_wpiblue", 6)); + } + + + /** + * Gets the 3D botpose using the WPILIB Red Alliance Coordinate System. + * + * @return An Pose3D representing the bot pose. + */ + private Pose3D getBotposeWpired() { + return createPose3DRobot(getDoubleArray("botpose_wpired", 6)); + } + + /** + * Gets the 3D botpose using MegaTag2. You must set the orientation of the robot with your imu for this to work. + * + * @return An Pose3D representing the MT2 botpose + */ + public Pose3D getBotpose_MT2() { + return createPose3DRobot(getDoubleArray("botpose_orb", 6)); + } + + /** + * Gets the 3D botpose using MegaTag2 and the WPILIB Blue Alliance Coordinate System. You must set the orientation of the robot with your imu for this to work. + * + * @return An Pose3D representing the MT2 botpose + */ + private Pose3D getBotposeWpiblue_MT2() { + return createPose3DRobot(getDoubleArray("botpose_orb_wpiblue", 6)); + } + + /** + * Gets the 3D botpose using MegaTag2 and the WPILIB Red Alliance Coordinate System. You must set the orientation of the robot with your imu for this to work. + * + * @return An Pose3D representing the MT2 botpose + */ + private Pose3D getBotposeOrbWpired_MT2() { + return createPose3DRobot(getDoubleArray("botpose_orb_wpired", 6)); + } + + + /** + * Gets the standard deviation metrics for MegaTag1 (x,y,z,roll,pitch,yaw) + * + * @return MegaTag1 STDevs + */ + public double[] getStddevMt1() { + return getDoubleArray("stdev_mt1", 6); + } + + /** + * Gets the standard deviation metrics for MegaTag2 (x,y,z,roll,pitch,yaw) + * + * @return MegaTag2 STDevs + */ + public double[] getStddevMt2() { + return getDoubleArray("stdev_mt2", 6); + } + + /** + * Gets the number of tags used in the botpose calculation. + * + * @return The number of tags used in the botpose calculation. + */ + public int getBotposeTagCount() { + return jsonData.optInt("botpose_tagcount", 0); + } + + /** + * Gets the span of tags used in the botpose calculation in meters. + * + * @return The span of tags used in the botpose calculation. + */ + public double getBotposeSpan() { + return jsonData.optDouble("botpose_span", 0); + } + + /** + * Gets the average distance of tags used in the botpose calculation in meters. + * + * @return The average distance of tags used in the botpose calculation. + */ + public double getBotposeAvgDist() { + return jsonData.optDouble("botpose_avgdist", 0); + } + + /** + * Gets the average area of tags used in the botpose calculation. + * + * @return The average area of tags used in the botpose calculation. + */ + public double getBotposeAvgArea() { + return jsonData.optDouble("botpose_avgarea", 0); + } + + /** + * Gets the user-specified python snapscript output data + * + * @return An array of values from the python snapscript pipeline. Returns an array of size 32. + */ + public double[] getPythonOutput() { + double[] output = getDoubleArray("PythonOut", 0); + + // Create a new array of size 32 + double[] result = new double[32]; + + // How many elements do we need to copy to the final result array? + int lengthToCopy = Math.min(output.length, 32); + + // Copy from the python result array to the result array. + System.arraycopy(output, 0, result, 0, lengthToCopy); + return result; + } + + /** + * Gets the current capture latency in milliseconds + * + * @return capture latency in millisecondss + */ + public double getCaptureLatency() { + return jsonData.optDouble("cl", 0); + } + + /** + * Gets the type of the current pipeline. + * + * @return The type of the current pipeline as a string. + */ + public String getPipelineType() { + return jsonData.optString("pipelineType", ""); + } + + /** + * Gets the current tx in degrees from the crosshair + * + * @return horizontal angular offset of the primary target in degrees from the crosshair + */ + public double getTx() { + return jsonData.optDouble("tx", 0); + } + + /** + * Gets the current ty in degrees from the crosshair + * + * @return vertical angular offset of the primary target in degrees from the crosshair + */ + public double getTy() { + return jsonData.optDouble("ty", 0); + } + + /** + * Gets the current tx in degrees from the principal pixel instead of the crosshair + * + * @return horizontal angular offset of the primary target in degrees from the principal pixel instead of the crosshair + */ + public double getTxNC() { + return jsonData.optDouble("txnc", 0); + } + + /** + * Gets the current ty in degrees from the principal pixel instead of the crosshair + * + * @return vertical angular offset of the primary target in degrees from the principal pixel instead of the crosshair + */ + public double getTyNC() { + return jsonData.optDouble("tync", 0); + } + + /** + * Gets the area of the target as a percentage of the image area + * + * @return target area (0-100) + */ + public double getTa() { + return jsonData.optDouble("ta", 0); + } + + /** + * Gets the index of the currently active pipeline + * + * @return index of the currently active pipeline + */ + public int getPipelineIndex() { + return jsonData.optInt("pID", 0); + } + + /** + * Gets the 3D camera pose in robot space as configured in the UI + * + * @return An Pose3D representing the camera pose in the robot's coordinate system. + */ + private Pose3D getCameraPose_RobotSpace() { + return createPose3DRobot(getDoubleArray("t6c_rs", 6)); + } + + /** + * Gets the targeting/pipeline latency in milliseconds. + * + * @return The targeting/pipeline latency. + */ + public double getTargetingLatency() { + return jsonData.optDouble("tl", 0); + } + + /** + * Gets the Limelight-local monotonic timestamp of the result. + * + * @return The limelight-local timestamp. + */ + public double getTimestamp() { + return jsonData.optDouble("ts", 0); + } + + /** + * Gets the validity of the result. + * + * @return Validity (0 or 1). + */ + public boolean isValid() { + int v = jsonData.optInt("v", 0); + if(v == 1) + { + return true; + } + return false; + } + + /** + * Gets the json parse latency. + * + * @return The Control Hub json parse latency in milliseconds. + */ + public double getParseLatency() { + return parseLatency; + } + + /** + * Helper method to get a double array from the JSON data. + * + * @param key The key for the array in the JSON data. + * @return A double array corresponding to the key. + */ + private double[] getDoubleArray(String key, int defaultCount) { + JSONArray array = jsonData.optJSONArray(key); + if (array == null) { + return new double[defaultCount]; + } + double[] result = new double[array.length()]; + for (int i = 0; i < array.length(); i++) { + result[i] = array.optDouble(i); + } + return result; + } + + /** + * Helper method to create an Pose3D instance from a pose array. + * + * @param pose The pose array (x, y, z, roll, pitch, yaw) + * @return An Pose3D instance without acquisition time. + */ + protected static Pose3D createPose3DRobot(double[] pose) { + if (pose.length < 6) { + return new Pose3D(new Position(), new YawPitchRollAngles(AngleUnit.DEGREES,0,0,0,0)); + } + Position position = new Position(DistanceUnit.METER, pose[0], pose[1], pose[2],0); + YawPitchRollAngles orientation = new YawPitchRollAngles(AngleUnit.DEGREES, pose[5], pose[4], pose[3],0); + return new Pose3D(position, orientation); + } + + /** + * Parses a JSONObject into an LLResult object. + * + * @param json The JSON to parse. + * @return An LLResult object, or null if parsing fails. + */ + protected static LLResult parse(JSONObject json) { + try { + return new LLResult(json); + } catch (JSONException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Returns a string representation of the LLResult. + * + * @return A string representation of the JSON data. + */ + @Override + public String toString() { + return jsonData.toString(); + } +} \ No newline at end of file diff --git a/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLResultTypes.java b/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLResultTypes.java new file mode 100644 index 000000000000..ea4910573607 --- /dev/null +++ b/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLResultTypes.java @@ -0,0 +1,883 @@ +package com.qualcomm.hardware.limelightvision; + +import org.firstinspires.ftc.robotcore.external.navigation.Pose3D; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parent class for all Limelight Result Types + */ +public class LLResultTypes { + + /** + * Parses a JSONArray of points into a List of Lists representing 2D coordinates. + * + * @param cornersArray The JSONArray containing point data. Each element should be a JSONArray with two elements representing x and y coordinates. + * @return A List of Lists, where each inner List contains two Double values representing x and y coordinates. + * Returns an empty List if the input is null or improperly formatted. + */ + private static List> parsePoints(JSONArray cornersArray) { + List> points = new ArrayList<>(); + if (cornersArray != null) { + for (int i = 0; i < cornersArray.length(); i++) { + JSONArray point = cornersArray.optJSONArray(i); + if (point != null && point.length() == 2) { + List cornerPoint = new ArrayList<>(); + cornerPoint.add(point.optDouble(0, 0.0)); + cornerPoint.add(point.optDouble(1, 0.0)); + points.add(cornerPoint); + } + } + } + return points; + } + + /** + * Parses a JSONArray into a double array representing a robot pose. + * The array should contain 6 elements: x, y, z, roll, pitch, yaw. + * + * @param array The JSONArray to convert. + * @return A double array representing the robot pose. If the input is null or has fewer than 6 elements, + * returns an array of 6 zeros. + */ + private static double[] parsePoseArray(JSONArray array) { + double[] pose = new double[6]; // Initialize with zeros + if (array != null && array.length() >= 6) { + for (int i = 0; i < 6; i++) { + pose[i] = array.optDouble(i, 0.0); + } + } + return pose; + } + + /** + * Represents a barcode pipeline result. A barcode pipeline may generate multiple valid results. + */ + public static class BarcodeResult { + + private String family; + private String data; + private double targetXPixels; + private double targetYPixels; + private double targetXDegrees; + private double targetYDegrees; + private double targetXDegreesNoCrosshair; + private double targetYDegreesNoCrosshair; + private double targetArea; + private List> targetCorners; + + /** + * Constructs a BarcodeResult from JSON data. + * + * @param data The JSON object containing barcode data. + */ + protected BarcodeResult(JSONObject data) { + this.family = data.optString("fam", ""); + this.data = data.optString("data", ""); + this.targetXPixels = data.optDouble("txp", 0.0); + this.targetYPixels = data.optDouble("typ", 0.0); + this.targetXDegrees = data.optDouble("tx", 0.0); + this.targetYDegrees = data.optDouble("ty", 0.0); + this.targetXDegreesNoCrosshair = data.optDouble("tx_nocross", 0.0); + this.targetYDegreesNoCrosshair = data.optDouble("ty_nocross", 0.0); + this.targetArea = data.optDouble("ta", 0.0); + this.targetCorners = LLResultTypes.parsePoints(data.optJSONArray("pts")); + } + + /** + * Gets the family of the barcode. + * + * @return The barcode family. + */ + public String getFamily() { + return family; + } + + /** + * Gets the data contained in the barcode. + * + * @return The barcode data. + */ + public String getData() { + return data; + } + + /** + * Gets the horizontal offset of the barcode from the crosshair in pixels. + * + * @return The horizontal offset in pixels. + */ + public double getTargetXPixels() { + return targetXPixels; + } + + /** + * Gets the vertical offset of the barcode from the crosshair in pixels. + * + * @return The vertical offset in pixels. + */ + public double getTargetYPixels() { + return targetYPixels; + } + + /** + * Gets the horizontal offset of the barcode from the crosshair in degrees. + * + * @return The horizontal offset in degrees. + */ + public double getTargetXDegrees() { + return targetXDegrees; + } + + /** + * Gets the vertical offset of the barcode from the crosshair in degrees. + * + * @return The vertical offset in degrees. + */ + public double getTargetYDegrees() { + return targetYDegrees; + } + + /** + * Gets the horizontal offset of the barcode from the principal pixel in degrees. + * + * @return The horizontal offset in degrees. + */ + public double getTargetXDegreesNoCrosshair() { + return targetXDegreesNoCrosshair; + } + + /** + * Gets the vertical offset of the barcode from the principal pixel in degrees. + * + * @return The vertical offset in degrees. + */ + public double getTargetYDegreesNoCrosshair() { + return targetYDegreesNoCrosshair; + } + + /** + * Gets the area of the detected barcode as a percentage of the image. + * + * @return The target area (0-1). + */ + public double getTargetArea() { + return targetArea; + } + + /** + * Gets the four corner points of the detected barcode. + * + * @return A list of corner points, each point represented as a list of two doubles [x, y]. + */ + public List> getTargetCorners() { + return targetCorners; + } + + } + + /** + * Represents a classifier pipeline result. + */ + public static class ClassifierResult { + private String className; + private int classId; + private double confidence; + + /** + * Constructs a ClassifierResult from JSON data. + * + * @param data The JSON object containing classifier data. + */ + protected ClassifierResult(JSONObject data) { + this.className = data.optString("class", ""); + this.classId = data.optInt("classID", 0); + this.confidence = data.optDouble("conf", 0.0); + } + + /** + * Gets the class name of the classifier result (eg book, car, gamepiece). + * + * @return The class name + */ + public String getClassName() { + return className; + } + + /** + * Gets the class index of the classifier result. + * + * @return The class index + */ + public int getClassId() { + return classId; + } + + /** + * Gets the confidence score of the classification. + * + * @return The confidence score of the classification (0-100). + */ + public double getConfidence() { + return confidence; + } + } + + /** + * Represents a detector pipeline result. A detector pipeline may generate multiple valid results. + */ + public static class DetectorResult { + private String className; + private int classId; + private double confidence; + private double targetArea; + private double targetXPixels; + private double targetYPixels; + private double targetXDegrees; + private double targetYDegrees; + private double targetXDegreesNoCrosshair; + private double targetYDegreesNoCrosshair; + private List> targetCorners; + + /** + * Constructs a DetectorResult from JSON data. + * + * @param data The JSON object containing classifier data. + */ + protected DetectorResult(JSONObject data) { + this.className = data.optString("class", ""); + this.classId = data.optInt("classID", 0); + this.confidence = data.optDouble("conf", 0.0); + this.targetArea = data.optDouble("ta", 0.0); + this.targetXPixels = data.optDouble("txp", 0.0); + this.targetYPixels = data.optDouble("typ", 0.0); + this.targetXDegrees = data.optDouble("tx", 0.0); + this.targetYDegrees = data.optDouble("ty", 0.0); + this.targetXDegreesNoCrosshair = data.optDouble("tx_nocross", 0.0); + this.targetYDegreesNoCrosshair = data.optDouble("ty_nocross", 0.0); + this.targetCorners = LLResultTypes.parsePoints(data.optJSONArray("pts")); + } + + /** + * Gets the class name of the detector result (eg book, car, gamepiece). + * + * @return The class name + */ + public String getClassName() { + return className; + } + + + /** + * Gets the class index of the detector result. + * + * @return The class index + */ + public int getClassId() { + return classId; + } + + /** + * Gets the confidence score of the classification. + * + * @return The confidence score of the classification (0-100). + */ + public double getConfidence() { + return confidence; + } + + /** + * Gets the four corner points of the detected result. + * + * @return A list of corner points, each point represented as a list of two doubles [x, y]. + */ + public List> getTargetCorners() { + return targetCorners; + } + + /** + * Gets the undistorted area of the target as a percentage of the image area + * + * @return target area (0-100) + */ + public double getTargetArea() { + return targetArea; + } + + /** + * Gets the current tx in pixels from the crosshair + * + * @return horizontal angular offset + */ + public double getTargetXPixels() { + return targetXPixels; + } + + /** + * Gets the current ty in pixels from the crosshair + * + * @return vertical angular offset + */ + public double getTargetYPixels() { + return targetYPixels; + } + + /** + * Gets the current tx in degrees from the crosshair + * + * @return horizontal angular offset + */ + public double getTargetXDegrees() { + return targetXDegrees; + } + + /** + * Gets the current ty in degrees from the crosshair + * + * @return vertical angular offset + */ + public double getTargetYDegrees() { + return targetYDegrees; + } + + /** + * Gets the current tx in degrees from the principal pixel + * + * @return horizontal angular offset + */ + public double getTargetXDegreesNoCrosshair() { + return targetXDegreesNoCrosshair; + } + + /** + * Gets the current ty in degrees from the principal pixel + * + * @return vertical angular offset + */ + public double getTargetYDegreesNoCrosshair() { + return targetYDegreesNoCrosshair; + } + } + + /** + * Represents a Fiducial/AprilTag pipeline result. A fiducial/apriltag pipeline may generate multiple valid results. + */ + public static class FiducialResult { + private int fiducialId; + private String family; + private double skew; + private Pose3D cameraPoseTargetSpace; + private Pose3D robotPoseFieldSpace; + private Pose3D robotPoseTargetSpace; + private Pose3D targetPoseCameraSpace; + private Pose3D targetPoseRobotSpace; + private double targetArea; + private double targetXPixels; + private double targetYPixels; + private double targetXDegrees; + private double targetYDegrees; + private double targetXDegreesNoCrosshair; + private double targetYDegreesNoCrosshair; + private List> targetCorners; + + + /** + * Constructs a FiducialResult from JSON data. + * + * @param data The JSON object containing fiducial data. + */ + protected FiducialResult(JSONObject data) { + this.fiducialId = data.optInt("fID", 0); + this.family = data.optString("fam", ""); + this.skew = data.optDouble("skew", 0.0); + this.cameraPoseTargetSpace = LLResult.createPose3DRobot(LLResultTypes.parsePoseArray(data.optJSONArray("t6c_ts"))); + this.robotPoseFieldSpace = LLResult.createPose3DRobot(LLResultTypes.parsePoseArray(data.optJSONArray("t6r_fs"))); + this.robotPoseTargetSpace = LLResult.createPose3DRobot(LLResultTypes.parsePoseArray(data.optJSONArray("t6r_ts"))); + this.targetPoseCameraSpace = LLResult.createPose3DRobot(LLResultTypes.parsePoseArray(data.optJSONArray("t6t_cs"))); + this.targetPoseRobotSpace = LLResult.createPose3DRobot(LLResultTypes.parsePoseArray(data.optJSONArray("t6t_rs"))); + this.targetArea = data.optDouble("ta", 0.0); + this.targetXPixels = data.optDouble("txp", 0.0); + this.targetYPixels = data.optDouble("typ", 0.0); + this.targetXDegrees = data.optDouble("tx", 0.0); + this.targetYDegrees = data.optDouble("ty", 0.0); + this.targetXDegreesNoCrosshair = data.optDouble("tx_nocross", 0.0); + this.targetYDegreesNoCrosshair = data.optDouble("ty_nocross", 0.0); + this.targetCorners = LLResultTypes.parsePoints(data.optJSONArray("pts")); + } + + /** + * Gets the ID of the fiducial. + * + * @return The fiducial ID. + */ + public int getFiducialId() { + return fiducialId; + } + + /** + * Gets the family of the fiducial (eg 36h11). + * + * @return The fiducial family. + */ + public String getFamily() { + return family; + } + + /** + * Gets the four corner points of the detected fiducial/apriltag. + * + * @return A list of corner points, each point represented as a list of two doubles [x, y]. + */ + public List> getTargetCorners() { + return targetCorners; + } + + /** + * Gets the skew of the detected fiducial. Not recommended. + * + * @return The skew value. + */ + public double getSkew() { + return skew; + } + + /** + * Gets the camera pose in target space. + * + * @return An Pose3D representing the camera pose. + */ + public Pose3D getCameraPoseTargetSpace() { + return cameraPoseTargetSpace; + } + + /** + * Gets the robot pose in field based on this fiducial/apriltag alone. + * + * @return An Pose3D representing the robot pose. + */ + public Pose3D getRobotPoseFieldSpace() { + return robotPoseFieldSpace; + } + + /** + * Gets the robot pose in target space. + * + * @return An Pose3D representing the robot pose. + */ + public Pose3D getRobotPoseTargetSpace() { + return robotPoseTargetSpace; + } + + /** + * Gets the target pose in camera space. + * + * @return An Pose3D representing the target pose. + */ + public Pose3D getTargetPoseCameraSpace() { + return targetPoseCameraSpace; + } + + /** + * Gets the target pose in robot space. + * + * @return An Pose3D representing the target pose. + */ + public Pose3D getTargetPoseRobotSpace() { + return targetPoseRobotSpace; + } + + /** + * Gets the area of the detected fiducial as a percentage of the image. + * + * @return The target area (0-100). + */ + public double getTargetArea() { + return targetArea; + } + + /** + * Gets the current tx in pixels from the crosshair + * + * @return horizontal angular offset + */ + public double getTargetXPixels() { + return targetXPixels; + } + + /** + * Gets the current ty in pixels from the crosshair + * + * @return vertical angular offset + */ + public double getTargetYPixels() { + return targetYPixels; + } + + /** + * Gets the current tx in degrees from the crosshair + * + * @return horizontal angular offset + */ + public double getTargetXDegrees() { + return targetXDegrees; + } + + /** + * Gets the current ty in degrees from the crosshair + * + * @return vertical angular offset + */ + public double getTargetYDegrees() { + return targetYDegrees; + } + + /** + * Gets the current tx in degrees from the principal pixel + * + * @return horizontal angular offset + */ + public double getTargetXDegreesNoCrosshair() { + return targetXDegreesNoCrosshair; + } + + /** + * Gets the current ty in degrees from the principal pixel + * + * @return vertical angular offset + */ + public double getTargetYDegreesNoCrosshair() { + return targetYDegreesNoCrosshair; + } + } + + /** + * Represents a color pipeline result. A color pipeline may generate multiple valid results. + */ + public static class ColorResult { + private Pose3D cameraPoseTargetSpace; + private Pose3D robotPoseFieldSpace; + private Pose3D robotPoseTargetSpace; + private Pose3D targetPoseCameraSpace; + private Pose3D targetPoseRobotSpace; + private double targetArea; + private double targetXPixels; + private double targetYPixels; + private double targetXDegrees; + private double targetYDegrees; + private double targetXDegreesNoCrosshair; + private double targetYDegreesNoCrosshair; + private List> targetCorners; + + /** + * Constructs a Color/ColorResult from JSON data. + * + * @param data The JSON object containing color data. + */ + protected ColorResult(JSONObject data) { + this.cameraPoseTargetSpace = LLResult.createPose3DRobot(LLResultTypes.parsePoseArray(data.optJSONArray("t6c_ts"))); + this.robotPoseFieldSpace = LLResult.createPose3DRobot(LLResultTypes.parsePoseArray(data.optJSONArray("t6r_fs"))); + this.robotPoseTargetSpace = LLResult.createPose3DRobot(LLResultTypes.parsePoseArray(data.optJSONArray("t6r_ts"))); + this.targetPoseCameraSpace = LLResult.createPose3DRobot(LLResultTypes.parsePoseArray(data.optJSONArray("t6t_cs"))); + this.targetPoseRobotSpace = LLResult.createPose3DRobot( LLResultTypes.parsePoseArray(data.optJSONArray("t6t_rs"))); + this.targetArea = data.optDouble("ta", 0.0); + this.targetXPixels = data.optDouble("txp", 0.0); + this.targetYPixels = data.optDouble("typ", 0.0); + this.targetXDegrees = data.optDouble("tx", 0.0); + this.targetYDegrees = data.optDouble("ty", 0.0); + this.targetXDegreesNoCrosshair = data.optDouble("tx_nocross", 0.0); + this.targetYDegreesNoCrosshair = data.optDouble("ty_nocross", 0.0); + this.targetCorners = LLResultTypes.parsePoints(data.optJSONArray("pts")); + } + + /** + * Gets the corner points of the detected target. The number of corners is not fixed. + * + * @return A list of corner points, each point represented as a list of two doubles [x, y]. + */ + public List> getTargetCorners() { + return targetCorners; + } + + /** + * Gets the camera pose in target space. + * + * @return An Pose3D representing the camera pose. + */ + public Pose3D getCameraPoseTargetSpace() { + return cameraPoseTargetSpace; + } + + /** + * Gets the robot pose in field space based on this color target. + * + * @return An Pose3D representing the robot pose. + */ + public Pose3D getRobotPoseFieldSpace() { + return robotPoseFieldSpace; + } + + /** + * Gets the robot pose in target space. + * + * @return An Pose3D representing the robot pose. + */ + public Pose3D getRobotPoseTargetSpace() { + return robotPoseTargetSpace; + } + + /** + * Gets the target pose in camera space. + * + * @return An Pose3D representing the target pose. + */ + public Pose3D getTargetPoseCameraSpace() { + return targetPoseCameraSpace; + } + + /** + * Gets the target pose in robot space. + * + * @return An Pose3D representing the target pose. + */ + public Pose3D getTargetPoseRobotSpace() { + return targetPoseRobotSpace; + } + + /** + * Gets the area of the detected color target as a percentage of the image. + * + * @return The target area (0-100). + */ + public double getTargetArea() { + return targetArea; + } + + /** + * Gets the current tx in pixels from the crosshair + * + * @return horizontal angular offset + */ + public double getTargetXPixels() { + return targetXPixels; + } + + /** + * Gets the current ty in pixels from the crosshair + * + * @return vertical angular offset + */ + public double getTargetYPixels() { + return targetYPixels; + } + + /** + * Gets the current tx in degrees from the crosshair + * + * @return horizontal angular offset + */ + public double getTargetXDegrees() { + return targetXDegrees; + } + + /** + * Gets the current ty in degrees from the crosshair + * + * @return vertical angular offset + */ + public double getTargetYDegrees() { + return targetYDegrees; + } + + /** + * Gets the current tx in degrees from the principal pixel + * + * @return horizontal angular offset + */ + public double getTargetXDegreesNoCrosshair() { + return targetXDegreesNoCrosshair; + } + + /** + * Gets the current ty in degrees from the principal pixel + * + * @return vertical angular offset + */ + public double getTargetYDegreesNoCrosshair() { + return targetYDegreesNoCrosshair; + } + } + + + /** + * Represents a calibration result. Calibration results are generated by the user in the UI's calibration tab. + */ + public static class CalibrationResult { + private String displayName; + private double resX; + private double resY; + private double reprojectionError; + private double[] camMatVector; + private double[] distortionCoefficients; + private boolean valid; + + private static final double DEFAULT_REPORT_VAL = 0.0; + + /** + * Constructs a CalibrationResult with default values. + */ + public CalibrationResult() { + this("", DEFAULT_REPORT_VAL, DEFAULT_REPORT_VAL, DEFAULT_REPORT_VAL, + new double[9], new double[5]); + } + + /** + * Constructs a CalibrationResult with specified values. + * + * @param displayName The display name for the calibration. + * @param resX The X resolution of the calibration. + * @param resY The Y resolution of the calibration. + * @param reprojectionError The reprojection error of the calibration. + * @param camMatVector The camera matrix vector (9 elements). + * @param distortionCoefficients The distortion coefficients (5 elements). + */ + public CalibrationResult(String displayName, double resX, double resY, double reprojectionError, + double[] camMatVector, double[] distortionCoefficients) { + this.displayName = displayName; + this.resX = resX; + this.resY = resY; + this.reprojectionError = reprojectionError; + this.camMatVector = camMatVector.clone(); + this.distortionCoefficients = distortionCoefficients.clone(); + this.valid = true; + } + + /** + * Constructs a CalibrationResult from JSON data. (Package-private) + * + * @param json The JSON object containing calibration result data. + */ + protected CalibrationResult(JSONObject json) { + //default value construction + this(); + //parse json + if(json!=null) + { + try{ + this.displayName = json.optString("DISPLAY_NAME", ""); + this.reprojectionError = json.optDouble("REPROJECTION_ERROR", DEFAULT_REPORT_VAL); + this.resX = json.optDouble("RES_X", DEFAULT_REPORT_VAL); + this.resY = json.optDouble("RES_Y", DEFAULT_REPORT_VAL); + + JSONArray intrinsicsMatrix = json.optJSONArray("INTRINSICS_MATRIX"); + if (intrinsicsMatrix != null && intrinsicsMatrix.length() == 9) { + for (int i = 0; i < 9; i++) { + this.camMatVector[i] = intrinsicsMatrix.optDouble(i, 0.0); + } + } else { + this.valid = false; + } + + JSONArray distortionCoeffs = json.optJSONArray("DISTORTION_COEFFICIENTS"); + if (distortionCoeffs != null && distortionCoeffs.length() == 5) { + for (int i = 0; i < 5; i++) { + this.distortionCoefficients[i] = distortionCoeffs.optDouble(i, 0.0); + } + } else { + this.valid = false; + } + + if (this.resX == DEFAULT_REPORT_VAL || this.resY == DEFAULT_REPORT_VAL) { + this.valid = false; + } + } catch (Exception e){ } + } + } + + /** + * Checks if the calibration result is valid. + * + * @return true if the calibration result is valid, false otherwise. + */ + public boolean isValid() { + return valid && resX != 0 && resY != 0 && camMatVector[0] != 0; + } + + /** + * Gets the display name of the calibration. + * + * @return The display name. + */ + public String getDisplayName() { + return displayName; + } + + /** + * Gets the X resolution of the calibration. + * + * @return The X resolution. + */ + public double getResX() { + return resX; + } + + /** + * Gets the Y resolution of the calibration. + * + * @return The Y resolution. + */ + public double getResY() { + return resY; + } + + /** + * Gets the reprojection error of the calibration. + * + * @return The reprojection error. + */ + public double getReprojectionError() { + return reprojectionError; + } + + /** + * Gets the camera matrix vector. + * + * @return A copy of the camera matrix vector. + */ + public double[] getCamMatVector() { + return camMatVector.clone(); + } + + /** + * Gets the distortion coefficients. + * + * @return A copy of the distortion coefficients. + */ + public double[] getDistortionCoefficients() { + return distortionCoefficients.clone(); + } + + /** + * Converts a CalibrationResult to a JSONObject. + * + * @return A JSONObject representation of the CalibrationResult. + */ + protected JSONObject toJson() { + JSONObject json = new JSONObject(); + try { + json.put("DISPLAY_NAME", this.getDisplayName()); + json.put("REPROJECTION_ERROR", this.getReprojectionError()); + json.put("RES_X", this.getResX()); + json.put("RES_Y", this.getResY()); + json.put("INTRINSICS_MATRIX", new JSONArray(this.getCamMatVector())); + json.put("DISTORTION_COEFFICIENTS", new JSONArray(this.getDistortionCoefficients())); + } catch (JSONException e) { + e.printStackTrace(); + } + return json; + } + } +} \ No newline at end of file diff --git a/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLStatus.java b/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLStatus.java new file mode 100644 index 000000000000..23f4bb9e3e7b --- /dev/null +++ b/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/LLStatus.java @@ -0,0 +1,247 @@ +package com.qualcomm.hardware.limelightvision; + +/* +Copyright (c) 2024 Limelight Vision + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Limelight Vision nor the names of its contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import org.firstinspires.ftc.robotcore.external.navigation.Quaternion; +import org.json.JSONObject; + +/** + * Represents the status of a Limelight. + */ +public class LLStatus { + private Quaternion cameraQuat; + private int cid; + private double cpu; + private double finalYaw; + private double fps; + private int hwType; + private int ignoreNT; + private int interfaceNeedsRefresh; + private String name; + private int pipeImgCount; + private int pipelineIndex; + private String pipelineType; + private double ram; + private int snapshotMode; + private double temp; + + /** + * Constructs an LLStatus object with default values. + */ + public LLStatus() { + this.cameraQuat = new Quaternion(1, 0, 0, 0, 0); + this.cid = 0; + this.cpu = 0.0; + this.finalYaw = 0.0; + this.fps = 0.0; + this.hwType = 0; + this.ignoreNT = 0; + this.interfaceNeedsRefresh = 0; + this.name = ""; + this.pipeImgCount = 0; + this.pipelineIndex = 0; + this.pipelineType = ""; + this.ram = 0.0; + this.snapshotMode = 0; + this.temp = 0.0; + } + + /** + * Constructs an LLStatus object from a JSON string. + * + * @param json The JSONobject containing Limelight status data. + */ + protected LLStatus(JSONObject json) { + this(); + if(json != null) + { + try { + JSONObject quatObj = json.optJSONObject("cameraQuat"); + if (quatObj != null) { + this.cameraQuat.w = (float)quatObj.optDouble("w", 1); + this.cameraQuat.x = (float)quatObj.optDouble("x", 0); + this.cameraQuat.y = (float)quatObj.optDouble("y", 0); + this.cameraQuat.z = (float)quatObj.optDouble("z", 0); + }; + this.cid = json.optInt("cid", 0); + this.cpu = json.optDouble("cpu", 0.0); + this.finalYaw = json.optDouble("finalYaw", 0.0); + this.fps = json.optDouble("fps", 0.0); + this.hwType = json.optInt("hwType", 0); + this.ignoreNT = json.optInt("ignoreNT", 0); + this.interfaceNeedsRefresh = json.optInt("interfaceNeedsRefresh", 0); + this.name = json.optString("name", ""); + this.pipeImgCount = json.optInt("pipeImgCount", 0); + this.pipelineIndex = json.optInt("pipelineIndex", 0); + this.pipelineType = json.optString("pipelineType", ""); + this.ram = json.optDouble("ram", 0.0); + this.snapshotMode = json.optInt("snapshotMode", 0); + this.temp = json.optDouble("temp", 0.0); + } catch (Exception e) { + + } + } + } + + /** + * @return The camera quaternion as a double array [w, x, y, z]. + */ + public Quaternion getCameraQuat() { + return cameraQuat; + } + + /** + * @return The camera sensor ID. + */ + public int getCid() { + return cid; + } + + /** + * @return The CPU usage as a percentage. + */ + public double getCpu() { + return cpu; + } + + /** + * @return The final yaw angle in degrees. + */ + public double getFinalYaw() { + return finalYaw; + } + + /** + * @return The frames per second being processed. + */ + public double getFps() { + return fps; + } + + /** + * @return The hardware type identifier. + */ + public int getHwType() { + return hwType; + } + + /** + * @return The ignore NetworkTables flag. + */ + private int getIgnoreNT() { + return ignoreNT; + } + + /** + * @return The interface needs refresh flag. + */ + private int getInterfaceNeedsRefresh() { + return interfaceNeedsRefresh; + } + + /** + * @return The name of the Limelight. + */ + public String getName() { + return name; + } + + /** + * @return The pipeline image count. + */ + public int getPipeImgCount() { + return pipeImgCount; + } + + /** + * @return The current pipeline index. + */ + public int getPipelineIndex() { + return pipelineIndex; + } + + /** + * @return The type of pipeline being used. + */ + public String getPipelineType() { + return pipelineType; + } + + /** + * @return The RAM usage as a percentage. + */ + public double getRam() { + return ram; + } + + /** + * @return The snapshot mode flag. + */ + public int getSnapshotMode() { + return snapshotMode; + } + + /** + * @return The temperature of the Limelight in degrees Celsius. + */ + public double getTemp() { + return temp; + } + + /** + * Returns a string representation of the LLStatus object. + * + * @return A string containing all the status parameters. + */ + @Override + public String toString() { + return "LLStatus{" + + "cameraQuat=" + cameraQuat.toString() + + ", cid=" + cid + + ", cpu=" + cpu + + ", finalYaw=" + finalYaw + + ", fps=" + fps + + ", hwType=" + hwType + + ", ignoreNT=" + ignoreNT + + ", interfaceNeedsRefresh=" + interfaceNeedsRefresh + + ", name='" + name + '\'' + + ", pipeImgCount=" + pipeImgCount + + ", pipelineIndex=" + pipelineIndex + + ", pipelineType='" + pipelineType + '\'' + + ", ram=" + ram + + ", snapshotMode=" + snapshotMode + + ", temp=" + temp + + '}'; + } +} \ No newline at end of file diff --git a/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/Limelight3A.java b/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/Limelight3A.java new file mode 100644 index 000000000000..f75a655c2f5c --- /dev/null +++ b/WilyCore/src/main/java/com/qualcomm/hardware/limelightvision/Limelight3A.java @@ -0,0 +1,732 @@ +package com.qualcomm.hardware.limelightvision; + +/* +Copyright (c) 2024 Limelight Vision + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Limelight Vision nor the names of its contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import com.qualcomm.hardware.limelightvision.LLResultTypes.CalibrationResult; +import com.qualcomm.robotcore.hardware.HardwareDevice; +import com.qualcomm.robotcore.util.SerialNumber; + +import org.firstinspires.ftc.robotcore.internal.usb.EthernetOverUsbSerialNumber; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URL; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + + +/** + * Driver for Limelight3A Vision Sensor. + * + * {@link Limelight3A } provides support for the Limelight Vision Limelight3A Vision Sensor. + * + * @see Limelight + * + * Notes on configuration: + * + * The device presents itself, when plugged into a USB port on a Control Hub as an ethernet + * interface. A DHCP server running on the Limelight automatically assigns the Control Hub an + * ip address for the new ethernet interface. + * + * Since the Limelight is plugged into a USB port, it will be listed on the top level configuration + * activity along with the Control Hub Portal and other USB devices such as webcams. Typically + * serial numbers are displayed below the device's names. In the case of the Limelight device, the + * Control Hub's assigned ip address for that ethernet interface is used as the "serial number". + * + * Tapping the Limelight's name, transitions to a new screen where the user can rename the Limelight + * and specify the Limelight's ip address. Users should take care not to confuse the ip address of + * the Limelight itself, which can be configured through the Limelight settings page via a web browser, + * and the ip address the Limelight device assigned the Control Hub and which is displayed in small text + * below the name of the Limelight on the top level configuration screen. + */ +public class Limelight3A implements HardwareDevice +{ + private static final String TAG = "Limelight3A"; + private InetAddress inetAddress; + private EthernetOverUsbSerialNumber serialNumber; + private String name; + + private final String baseUrl; + private volatile LLResult latestResult; + private volatile ScheduledExecutorService executor; + private final Object executorLock = new Object(); + + private final AtomicInteger state = new AtomicInteger(0); + private static final int STOPPED = 0, STARTING = 1, RUNNING = 2; + + private volatile boolean isExecutorInitialized = false; + + private volatile long lastUpdateTime = 0; + private static final long ISCONNECTED_THRESHOLD = 250; + private static final int CONNECTION_TIMEOUT = 100; + private static final int GETREQUEST_TIMEOUT = 100; + private static final int POSTREQUEST_TIMEOUT = 15000; + private static final int DELETEREQUEST_TIMEOUT = 15000; + + private static final int PYTHON_INPUT_MAX_SIZE = 32; + private long pollIntervalMs = 10; // Default poll rate = 100Hz + private static final int MIN_POLL_RATE_HZ = 1; + private static final int MAX_POLL_RATE_HZ = 250; + + + public Limelight3A(SerialNumber serialNumber, String name, InetAddress ipAddress) { + this.name = name; + this.serialNumber = (EthernetOverUsbSerialNumber)serialNumber; + this.inetAddress = ipAddress; + this.baseUrl = "http://blargh:5807"; + this.executor = Executors.newSingleThreadScheduledExecutor(); + } + + /** + * Allows for re-initialization + */ + private synchronized void ensureExecutorRunning() { + synchronized (executorLock) { + //lock to make sure executor doesn't change right after we check here (bh) + if (executor == null || executor.isShutdown()) { + executor = Executors.newSingleThreadScheduledExecutor(); + isExecutorInitialized = false; + } + } + } + + /** + * Starts or resumes periodic polling of Limelight data. + */ + public synchronized void start() { + // handling rapid consecutive start calls with getandset bh + if (!state.compareAndSet(STOPPED, STARTING)) { + return; + } + + try { + ensureExecutorRunning(); + if (!isExecutorInitialized) { + executor.scheduleAtFixedRate(this::updateLatestResult, 0, pollIntervalMs, TimeUnit.MILLISECONDS); + isExecutorInitialized = true; + } + state.set(RUNNING); + } catch (Exception e) { + state.set(STOPPED); + } + } + + /** + * Pauses polling of Limelight data. + */ + public synchronized void pause() { + if (state.get() == RUNNING) { + state.set(STOPPED); + } + } + + + /** + * Stops polling of Limelight data. + */ + public synchronized void stop() { + state.set(STOPPED); + isExecutorInitialized = false; + if (!executor.isShutdown()) { + executor.shutdownNow(); + } + } + + /** + * Checks if the polling is enabled. + * + * @return true if the polling is enabled + */ + public boolean isRunning() { + return state.get() == RUNNING; + } + + /** + * Sets the poll rate in Hertz (Hz). Must be called before start() + * The rate is clamped between 1 and 250 Hz. + * + * @param rateHz The desired poll rate in Hz. + */ + public synchronized void setPollRateHz(int rateHz) { + if (state.get() == RUNNING) + { + return; // Early return if the executor is running + } + + int clampedRate = Math.max(MIN_POLL_RATE_HZ, Math.min(rateHz, MAX_POLL_RATE_HZ)); + this.pollIntervalMs = 1000 / clampedRate; + } + + /** + * Gets the time elapsed since the last update. + * + * @return The time in milliseconds since the last update. + */ + public long getTimeSinceLastUpdate() { + return System.currentTimeMillis() - lastUpdateTime; + } + + /** + * Checks if the Limelight is currently connected. + * + * @return true if connected, false otherwise. + */ + public boolean isConnected() { + return getTimeSinceLastUpdate() <= ISCONNECTED_THRESHOLD; + } + + /** + * Updates the latest result from the Limelight. + */ + private void updateLatestResult() { + if (state.get() != RUNNING) return; + + try { + + JSONObject result = sendGetRequest("/results"); + if (result != null) { + this.latestResult = LLResult.parse(result); + this.lastUpdateTime = System.currentTimeMillis(); + } + } + catch (Exception e){ + //todo + } + } + + /** + * Gets the latest result from the Limelight. + * + * @return The latest LLResult object. + */ + public LLResult getLatestResult(){ + if (this.latestResult == null) { + JSONObject fakeJSON = new JSONObject(); + this.latestResult = LLResult.parse(fakeJSON); + } + return this.latestResult; + } + + /** + * Gets the current status of the Limelight. + * + * @return A populated LLStatus object, or a default status object if request fails. + */ + public LLStatus getStatus() { + JSONObject statusJson = sendGetRequest("/status"); + if(statusJson == null) + { + return new LLStatus(); + } + return new LLStatus(statusJson); + } + + /** + * Gets the hardware report from the Limelight. The Hardware Report contains calibration data. + * This is a highly advanced feature that is not necessary for FTC Teams. Marked private for now. + * + * @return A JSONObject containing the hardware report. + */ + private JSONObject getHWReport() { + return sendGetRequest("/hwreport"); + } + + /** + * Reloads the current Limelight pipeline. + * + * @return true if successful, false otherwise. + */ + public boolean reloadPipeline() { + return sendPostRequest("/reload-pipeline", null); + } + + /** + * Gets the default pipeline. + * + * @return A JSONObject containing the default pipeline configuration. + */ + private JSONObject getPipelineDefault() { + return sendGetRequest("/pipeline-default"); + } + + /** + * Gets the pipeline at a specific index. + * + * @param index The index of the pipeline to retrieve. + * @return A JSONObject containing the pipeline configuration. + */ + private JSONObject getPipelineAtIndex(int index) { + return sendGetRequest("/pipeline-atindex?index=" + index); + } + + /** + * Switches to a pipeline at the specified index. + * + * @param index The index of the pipeline to switch to. + * @return true if successful, false otherwise. + */ + public boolean pipelineSwitch(int index) { + return sendPostRequest("/pipeline-switch?index=" + index, null); + } + + /** + * Gets the names of available snapscripts. + * This method is not necessary for FTC teams. Marked as private + * + * @return A JSONObject containing the names of available snapscripts. + */ + private JSONObject getSnapscriptNames() { + return sendGetRequest("/getsnapscriptnames"); + } + + /** + * Captures a snapshot with the given name. + * + * @param snapname The name to give to the captured snapshot. + * @return true if successful, false otherwise. + */ + public boolean captureSnapshot(String snapname) { + return sendPostRequest("/capture-snapshot?snapname=" + snapname, null); + } + + /** + * Gets the manifest of available snapshots. + * This method is not necessary for FTC teams. Marked as private + * + * @return A JSONObject containing the snapshot manifest. + */ + private JSONObject snapshotManifest() { + return sendGetRequest("/snapshotmanifest"); + } + + /** + * Deletes all snapshots. + * + * @return true if successful, false otherwise. + */ + public boolean deleteSnapshots() { + return sendDeleteRequest("/delete-snapshots"); + } + + /** + * Deletes a specific snapshot. + * + * @param snapname The name of the snapshot to delete. + * @return true if successful, false otherwise. + */ + public boolean deleteSnapshot(String snapname) { + return sendDeleteRequest("/delete-snapshot?snapname=" + snapname); + } + + /** + * Updates the current pipeline configuration. + * + * @param profileJson The new pipeline configuration as a JSONObject. + * @param flush Whether to flush the configuration to persistent storage. + * @return true if successful, false otherwise. + */ + private boolean updatePipeline(JSONObject profileJson, boolean flush) { + String url = "/update-pipeline" + (flush ? "?flush=1" : ""); + return sendPostRequest(url, profileJson.toString()); + } + + /** + * Updates the Python SnapScript inputs with 8 double values. + * + * @param input1 The first input value + * @param input2 The second input value + * @param input3 The third input value + * @param input4 The fourth input value + * @param input5 The fifth input value + * @param input6 The sixth input value + * @param input7 The seventh input value + * @param input8 The eighth input value + * @return True if successful, false otherwise. + */ + public boolean updatePythonInputs(double input1, double input2, double input3, double input4, + double input5, double input6, double input7, double input8) { + double[] inputs = new double[]{input1, input2, input3, input4, input5, input6, input7, input8}; + return updatePythonInputs(inputs); + } + + /** + * Updates the Python SnapScript inputs. + * + * @param inputs A double array containing the new Python inputs. + * @return True if successful, false otherwise. False if array is empty or contains more than 32 elements + */ + public boolean updatePythonInputs(double[] inputs) { + if (inputs == null || inputs.length == 0 || inputs.length > PYTHON_INPUT_MAX_SIZE) { + return false; // Invalid input size + } + + try { + JSONArray jsonArray = new JSONArray(inputs); + return sendPostRequest("/update-pythoninputs", jsonArray.toString()); + } catch (Exception e) { + // + return false; + } + } + + /** + * Updates the robot orientation for MegaTag2. + * + * @param yaw The yaw value of the robot, aligned with the field map + * @return true if successful, false otherwise. + */ + public boolean updateRobotOrientation(double yaw) { + double[] orientationData = {yaw, 0, 0, 0, 0, 0}; + try{ + JSONArray jsonArray = new JSONArray(orientationData); + return sendPostRequest("/update-robotorientation", jsonArray.toString()); + } catch (Exception e){ + return false; + } + } + + /** + * Uploads a pipeline to a specific slot. + * + * @param profileJson The new pipeline configuration as a JSONObject. + * @param index The index at which to upload the pipeline (null for default). + * @return true if successful, false otherwise. + */ + private boolean uploadPipeline(JSONObject profileJson, Integer index) { + String url = "/upload-pipeline" + (index != null ? "?index=" + index : ""); + return sendPostRequest(url, profileJson.toString()); + } + + /** + * Uploads a pipeline to a specific slot. + * + * @param jsonString The new pipeline configuration as a String containing JSON. + * @param index The index at which to upload the pipeline (null for default). + * @return true if successful, false otherwise. + */ + public boolean uploadPipeline(String jsonString, Integer index){ + String url = "/upload-pipeline" + (index != null ? "?index=" + index : ""); + return sendPostRequest(url, jsonString); + } + + /** + * Uploads a new fiducial field map. Early exits if map is empty or doesn't specify a type + * + * @param fieldmap The new field map configuration. + * @param index The index at which to upload the field map (null for default). + * @return true if successful, false otherwise. + */ + public boolean uploadFieldmap(LLFieldMap fieldmap, Integer index) { + if(!fieldmap.isValid()) + { + return false; + } + String url = "/upload-fieldmap" + (index != null ? "?index=" + index : ""); + return sendPostRequest(url, fieldmap.toJson().toString()); + } + + /** + * Uploads new Python code. + * + * @param pythonString The Python code as a string. + * @param index The index at which to upload the Python code (null for default). + * @return true if successful, false otherwise. + */ + public boolean uploadPython(String pythonString, Integer index) { + String url = "/upload-python" + (index != null ? "?index=" + index : ""); + return sendPostRequest(url, pythonString); + } + + /** + * Gets the default calibration data. + * + * @return A CalibrationResult containing the default calibration data. + */ + public CalibrationResult getCalDefault() { + JSONObject calData = sendGetRequest("/cal-default"); + return new CalibrationResult(calData); + } + + + /** + * Gets calibration data from the user-generated calibration file. + * + * @return A CalibrationResult containing the calibration data from the file. + */ + public CalibrationResult getCalFile() { + JSONObject calData = sendGetRequest("/cal-file"); + return new CalibrationResult(calData); + } + + /** + * Gets the calibration data from the Limelight EEPROM. + * + * @return A CalibrationResult containing the calibration data from the EEPROM. + */ + public CalibrationResult getCalEEPROM() { + JSONObject calData = sendGetRequest("/cal-eeprom"); + return new CalibrationResult(calData); + } + + /** + * Gets the latest calibration result. This result is not necessarily used by the camera in any way + * + * @return A CalibrationResult containing the latest calibration result. + */ + public CalibrationResult getCalLatest() { + JSONObject calData = sendGetRequest("/cal-latest"); + return new CalibrationResult(calData); + } + + /** + * Upload calibration data to the EEPROM + * + * @param calResult A CalibrationResult containing the new calibration data. + * @return true if successful, false otherwise. + */ + private boolean updateCalEEPROM(CalibrationResult calResult) { + return sendPostRequest("/cal-eeprom", calResult.toJson().toString()); + } + + /** + * Updates the calibration file stored in the Limelight filesystem + * + * @param calResult A CalibrationResult containing the new calibration data. + * @return true if successful, false otherwise. + */ + private boolean updateCalFile(CalibrationResult calResult) { + return sendPostRequest("/cal-file", calResult.toJson().toString()); + } + + /** + * Deletes the latest calibration data. + * + * @return true if successful, false otherwise. + */ + private boolean deleteCalLatest() { + return sendDeleteRequest("/cal-latest"); + } + + /** + * Deletes the calibration data from the EEPROM. + * + * @return true if successful, false otherwise. + */ + private boolean deleteCalEEPROM() { + return sendDeleteRequest("/cal-eeprom"); + } + + /** + * Deletes the calibration data from the file. + * + * @return true if successful, false otherwise. + */ + private boolean deleteCalFile() { + return sendDeleteRequest("/cal-file"); + } + + /** + * Sends a GET request to the specified endpoint. + * + * @param endpoint The endpoint to send the request to. + * @return A JSONObject containing the response, or null if the request fails. + */ + private JSONObject sendGetRequest(String endpoint) { + HttpURLConnection connection = null; + try { + String urlString = baseUrl + endpoint; + URL url = new URL(urlString); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setReadTimeout(GETREQUEST_TIMEOUT); + connection.setConnectTimeout(CONNECTION_TIMEOUT); + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + String response = readResponse(connection); + return new JSONObject(response); + } else { + //System.out.println("HTTP GET Error: " + responseCode); + } + } catch (Exception e) { + //e.printStackTrace(); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + return null; + } + + /** + * Reads the response from an HTTP connection. + * + * @param connection The HttpURLConnection to read from. + * @return A String containing the response. + * @throws IOException If an I/O error occurs. + */ + private String readResponse(HttpURLConnection connection) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + + return response.toString(); + } + + /** + * Sends a POST request to the specified endpoint. + * + * @param endpoint The endpoint to send the request to. + * @param data The data to send in the request body. + * @return true if successful, false otherwise. + */ + private boolean sendPostRequest(String endpoint, String data) { + return false; // Wily Works override without a timeout + +// HttpURLConnection connection = null; +// try { +// String urlString = baseUrl + endpoint; +// URL url = new URL(urlString); +// connection = (HttpURLConnection) url.openConnection(); +// connection.setRequestMethod("POST"); +// connection.setDoOutput(true); +// connection.setRequestProperty("Content-Type", "application/json"); +// connection.setReadTimeout(POSTREQUEST_TIMEOUT); +// connection.setConnectTimeout(CONNECTION_TIMEOUT); +// +// if (data != null) { +// try (OutputStream os = connection.getOutputStream()) { +// byte[] input = data.getBytes(StandardCharsets.UTF_8); +// os.write(input, 0, input.length); +// } +// } +// +// int responseCode = connection.getResponseCode(); +// if (responseCode == HttpURLConnection.HTTP_OK) { +// return true; +// } else { +// //System.out.println("HTTP POST Error: " + responseCode); +// } +// } catch (Exception e) { +// //e.printStackTrace(); +// } finally { +// if (connection != null) { +// connection.disconnect(); +// } +// } +// return false; + } + + /** + * Sends a DELETE request to the specified endpoint. + * + * @param endpoint The endpoint to send the request to. + * @return true if successful, false otherwise. + */ + private boolean sendDeleteRequest(String endpoint) { + HttpURLConnection connection = null; + try { + String urlString = baseUrl + endpoint; + URL url = new URL(urlString); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("DELETE"); + connection.setReadTimeout(DELETEREQUEST_TIMEOUT); + connection.setConnectTimeout(CONNECTION_TIMEOUT); + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + return true; + } else { + //System.out.println("HTTP DELETE Error: " + responseCode); + } + } catch (Exception e) { + //e.printStackTrace(); + } finally{ + if (connection != null) { + connection.disconnect(); + } + } + return false; + } + + /** + * Shuts down the Limelight connection and stops all ongoing processes. + */ + public void shutdown() { + stop(); + } + + @Override + public Manufacturer getManufacturer() { + return Manufacturer.LimelightVision; + } + + @Override + public String getDeviceName() { + return name; + } + + @Override + public String getConnectionInfo() { + return ""; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public void resetDeviceConfigurationForOpMode() { + } + + @Override + public void close() { + stop(); + } + +} + diff --git a/WilyCore/src/main/java/com/qualcomm/hardware/sparkfun/SparkFunLEDStick.java b/WilyCore/src/main/java/com/qualcomm/hardware/sparkfun/SparkFunLEDStick.java new file mode 100644 index 000000000000..330449ec8e50 --- /dev/null +++ b/WilyCore/src/main/java/com/qualcomm/hardware/sparkfun/SparkFunLEDStick.java @@ -0,0 +1,195 @@ +package com.qualcomm.hardware.sparkfun; + +import android.graphics.Color; + +import androidx.annotation.ColorInt; + +import com.qualcomm.robotcore.hardware.I2cAddr; +import com.qualcomm.robotcore.hardware.I2cDeviceSynchDevice; +import com.qualcomm.robotcore.hardware.I2cDeviceSynchSimple; +import com.qualcomm.robotcore.hardware.configuration.annotations.DeviceProperties; +import com.qualcomm.robotcore.hardware.configuration.annotations.I2cDeviceType; + +/** + * Support for the Sparkfun QWIIC LED Stick + * To connect it directly, you need this cable + */ +@I2cDeviceType() +@DeviceProperties(name = "@string/sparkfun_led_stick_name", + description = "@string/sparkfun_led_stick_description", + xmlTag = "QWIIC_LED_STICK") +public class SparkFunLEDStick extends I2cDeviceSynchDevice { + private final static boolean LIMIT_TO_ONE_STICK = true; + private final static int MAX_SEGMENT_LENGTH = 12; + private enum Commands { + CHANGE_LED_LENGTH(0x70), + WRITE_SINGLE_LED_COLOR(0x71), + WRITE_ALL_LED_COLOR(0x72), + WRITE_RED_ARRAY(0x73), + WRITE_GREEN_ARRAY(0x74), + WRITE_BLUE_ARRAY(0x75), + WRITE_SINGLE_LED_BRIGHTNESS(0x76), + WRITE_ALL_LED_BRIGHTNESS(0x77), + WRITE_ALL_LED_OFF(0x78); + final int bVal; + + Commands(int bVal) { + this.bVal = bVal; + } + } + + /** + * Change the color of a specific LED + * + * @param position which LED to change (1 - 255) + * @param color what color to set it to + */ + public void setColor(int position, @ColorInt int color) { + byte[] data = new byte[4]; + data[0] = (byte) position; + data[1] = (byte) Color.red(color); + data[2] = (byte) Color.green(color); + data[3] = (byte) Color.blue(color); + writeI2C(Commands.WRITE_SINGLE_LED_COLOR, data); + } + + /** + * Change the color of all LEDs to a single color + * + * @param color what the color should be + */ + public void setColor(@ColorInt int color) { + byte[] data = new byte[3]; + data[0] = (byte) Color.red(color); + data[1] = (byte) Color.green(color); + data[2] = (byte) Color.blue(color); + writeI2C(Commands.WRITE_ALL_LED_COLOR, data); + } + + /** + * Send a segment of the LED array + * + * @param cmd command to send + * @param array the values (limited from 0..255) + * @param offset the starting value (LED only, array starts at 0) + * @param length the length to send + */ + private void sendSegment(Commands cmd, int[] array, int offset, int length) { + byte[] data = new byte[length + 2]; + data[0] = (byte) length; + data[1] = (byte) offset; + + for (int i = 0; i < length; i++) { + data[2 + i] = (byte) array[i]; + } + writeI2C(cmd, data); + } + + /** + * Change the color of an LED color segment + * + * @param colors what the colors should be + * @param offset where in the array to start + * @param length length to send (limited to 12) + */ + private void setLEDColorSegment(@ColorInt int[] colors, int offset, int length) { + // Copy a segment of elements from the colors array into separate arrays for red, green, and blue. + int[] redArray = new int[length]; + int[] greenArray = new int[length]; + int[] blueArray = new int[length]; + + // Here we iterate [0, length) because we are filling the red, green, and blue arrays. + for (int i = 0; i < length; i++) { + // Use the offset when indexing into the colors array so we get the appropriate segement of elements. + redArray[i] = Color.red(colors[i + offset]); + greenArray[i] = Color.green(colors[i + offset]); + blueArray[i] = Color.blue(colors[i + offset]); + } + sendSegment(Commands.WRITE_RED_ARRAY, redArray, offset, length); + sendSegment(Commands.WRITE_GREEN_ARRAY, greenArray, offset, length); + sendSegment(Commands.WRITE_BLUE_ARRAY, blueArray, offset, length); + } + + /** + * Change the color of all LEDs using arrays + * + * @param colors array of colors to set lights to + */ + public void setColors(@ColorInt int[] colors) { + int length = colors.length; + + if (LIMIT_TO_ONE_STICK && length > 10) { + length = 10; + } + + int numInLastSegment = length % MAX_SEGMENT_LENGTH; + int numSegments = length / MAX_SEGMENT_LENGTH; + for (int i = 0; i < numSegments; i++) { + setLEDColorSegment(colors, i * MAX_SEGMENT_LENGTH, MAX_SEGMENT_LENGTH); + } + if (numInLastSegment > 0) { + setLEDColorSegment(colors, numSegments * MAX_SEGMENT_LENGTH, numInLastSegment); + } + } + + /** + * Set the brightness of an individual LED + * + * @param position which LED to change (1-255) + * @param brightness brightness level (0-31) + */ + public void setBrightness(int position, int brightness) { + byte[] data = new byte[2]; + data[0] = (byte) position; + data[1] = (byte) brightness; + writeI2C(Commands.WRITE_SINGLE_LED_BRIGHTNESS, data); + } + + /** + * Set the brightness of all LEDs + * + * @param brightness brightness level (0-31) + */ + public void setBrightness(int brightness) { + byte[] data = new byte[1]; + data[0] = (byte) brightness; + writeI2C(Commands.WRITE_ALL_LED_BRIGHTNESS, data); + } + + /** + * Turn all LEDS off... + */ + public void turnAllOff() { + setColor(0); + } + + private void writeI2C(Commands cmd, byte[] data) {} +// { +// deviceClient.write(cmd.bVal, data); +// } + + @Override + public Manufacturer getManufacturer() { + return Manufacturer.SparkFun; + } + + @Override + protected synchronized boolean doInitialize() { + return true; + } + + @Override + public String getDeviceName() { + return "SparkFun Qwiic LED Strip"; + } + + private final static I2cAddr ADDRESS_I2C_DEFAULT = I2cAddr.create7bit(0x23); + + public SparkFunLEDStick(I2cDeviceSynchSimple deviceClient, boolean deviceClientIsOwned) { + super(deviceClient, deviceClientIsOwned); + +// deviceClient.setI2cAddress(ADDRESS_I2C_DEFAULT); +// super.registerArmingStateCallback(false); + } + +} diff --git a/WilyCore/src/main/java/com/qualcomm/robotcore/eventloop/EventLoopManager.java b/WilyCore/src/main/java/com/qualcomm/robotcore/eventloop/EventLoopManager.java new file mode 100644 index 000000000000..6dd1455ca25d --- /dev/null +++ b/WilyCore/src/main/java/com/qualcomm/robotcore/eventloop/EventLoopManager.java @@ -0,0 +1,15 @@ +package com.qualcomm.robotcore.eventloop; + +// public class EventLoopManager implements RecvLoopRunnable.RecvLoopCallback, NetworkConnection.NetworkConnectionCallback, PeerStatusCallback, SyncdDevice.Manager { +public class EventLoopManager implements SyncdDevice.Manager { + + @Override + public void registerSyncdDevice(SyncdDevice device) { + + } + + @Override + public void unregisterSyncdDevice(SyncdDevice device) { + + } +} diff --git a/WilyCore/src/main/java/com/qualcomm/robotcore/eventloop/SyncdDevice.java b/WilyCore/src/main/java/com/qualcomm/robotcore/eventloop/SyncdDevice.java new file mode 100644 index 000000000000..bf2743f648ec --- /dev/null +++ b/WilyCore/src/main/java/com/qualcomm/robotcore/eventloop/SyncdDevice.java @@ -0,0 +1,9 @@ +package com.qualcomm.robotcore.eventloop; + +public interface SyncdDevice { + + interface Manager { + void registerSyncdDevice(SyncdDevice device); + void unregisterSyncdDevice(SyncdDevice device); + } +} diff --git a/WilyCore/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModeManagerImpl.java b/WilyCore/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModeManagerImpl.java index 698f94cc0afe..c4f3a7c1f6a2 100644 --- a/WilyCore/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModeManagerImpl.java +++ b/WilyCore/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModeManagerImpl.java @@ -2,8 +2,13 @@ import androidx.annotation.Nullable; -public class OpModeManagerImpl { +public class OpModeManagerImpl implements OpModeManagerNotifier { public @Nullable OpMode registerListener(OpModeManagerNotifier.Notifications listener) { return null; } + + @Override + public void unregisterListener(Notifications listener) { + + } } diff --git a/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/AnalogInput.java b/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/AnalogInput.java new file mode 100644 index 000000000000..958389666839 --- /dev/null +++ b/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/AnalogInput.java @@ -0,0 +1,35 @@ +package com.qualcomm.robotcore.hardware; + +import com.qualcomm.robotcore.hardware.configuration.annotations.DeviceProperties; + +/** + * Control a single analog device + */ +@DeviceProperties(name = "@string/configTypeAnalogInput", xmlTag = "AnalogInput", builtIn = true) +public class AnalogInput implements HardwareDevice { + @Override public Manufacturer getManufacturer() { return Manufacturer.Other; } + + public double getVoltage() { return 0; } + + public double getMaxVoltage() { return 0; } + + @Override + public String getDeviceName() { return ""; } + + @Override + public String getConnectionInfo() { return ""; } + + @Override + public int getVersion() { + return 1; + } + + @Override + public void resetDeviceConfigurationForOpMode() { + } + + @Override + public void close() { + // take no action + } +} diff --git a/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java b/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java index 0e7c9601d17d..cace5ab66d4d 100644 --- a/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java +++ b/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java @@ -37,7 +37,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE public interface HardwareDevice { enum Manufacturer { - Unknown, Other, Lego, HiTechnic, ModernRobotics, Adafruit, Matrix, Lynx, AMS, STMicroelectronics, Broadcom, DFRobot, SparkFun + Unknown, Other, Lego, HiTechnic, ModernRobotics, Adafruit, Matrix, Lynx, AMS, STMicroelectronics, Broadcom, DFRobot, SparkFun, LimelightVision } /** diff --git a/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java b/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java index 4535f06a70b6..246943823f1a 100644 --- a/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java +++ b/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java @@ -1,6 +1,15 @@ package com.qualcomm.robotcore.hardware; +import android.content.Context; + +import com.qualcomm.robotcore.eventloop.opmode.OpModeManagerNotifier; import com.wilyworks.simulator.framework.WilyHardwareMap; public class HardwareMap extends WilyHardwareMap { + public HardwareMap() { + super(null, null); + } + public HardwareMap(Context appContext, OpModeManagerNotifier notifier) { + super(appContext, notifier); + } } diff --git a/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/LED.java b/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/LED.java index d9fe88193759..e0c14ea86bd4 100644 --- a/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/LED.java +++ b/WilyCore/src/main/java/com/qualcomm/robotcore/hardware/LED.java @@ -34,10 +34,9 @@ @DeviceProperties(name = "@string/configTypeLED", xmlTag = "Led", builtIn = true, description = "@string/led_description") public class LED implements HardwareDevice, SwitchableLight { - @Override public Manufacturer getManufacturer() { - return null; + return Manufacturer.Other; } @Override @@ -73,6 +72,8 @@ public void enableLight(boolean enable) { public boolean isLightOn() { return false; } + + public void enable(boolean enableLed) { } /** * Turns the light on */ diff --git a/WilyCore/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java b/WilyCore/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java index 3ff07af8eaed..6f1cbd51cbef 100644 --- a/WilyCore/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java +++ b/WilyCore/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java @@ -31,5 +31,198 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE package com.qualcomm.robotcore.util; -public abstract class SerialNumber { +/* Copyright (c) 2014, 2015 Qualcomm Technologies Inc + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Qualcomm Technologies Inc nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +import androidx.annotation.Nullable; + +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import org.firstinspires.ftc.robotcore.internal.system.Misc; + +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.UUID; + +/** + */ +@SuppressWarnings("WeakerAccess") +@JsonAdapter(SerialNumber.GsonTypeAdapter.class) +public abstract class SerialNumber implements Serializable { + + protected SerialNumber(String serialNumberString) { + } + + public static SerialNumber createFake() { + return null; + } + + public static SerialNumber createEmbedded() { return null; } + + public static SerialNumber fromString(@Nullable String serialNumberString) { + return null; + } + + public static @Nullable SerialNumber fromStringOrNull(@Nullable String serialNumberString) { + return null; + } + + public static @Nullable SerialNumber fromUsbOrNull(@Nullable String serialNumberString) { + return null; + } + + /** Makes up a serial-number-like-thing for USB devices that internally lack a serial number. */ + public static SerialNumber fromVidPid(int vid, int pid, String connectionPath) { + return null; + } + + //------------------------------------------------------------------------------------------------ + // Gson + //------------------------------------------------------------------------------------------------ + + static class GsonTypeAdapter extends TypeAdapter { + @Override public void write(JsonWriter writer, SerialNumber serialNumber) throws IOException { + if (serialNumber==null) { + writer.nullValue(); + } else { + writer.value(serialNumber.getString()); + } + } + + @Override public SerialNumber read(JsonReader reader) throws IOException { + return SerialNumber.fromStringOrNull(reader.nextString()); + } + } + + //------------------------------------------------------------------------------------------------ + // Accessing + //------------------------------------------------------------------------------------------------ + + public boolean isVendorProduct() { + return false; + } + + /** + * Returns whether the indicated serial number is one of the legacy + * fake serial number forms or not. + * @return whether the the serial number is a legacy fake form of serial number + */ + public boolean isFake() { + return false; + } + + /** + * Returns whether the serial number is one of an actual USB device. + */ + public boolean isUsb() { + return false; + } + + /** + * Returns whether the serial number is the one used for the embedded + * Expansion Hub inside a Rev Control Hub. + */ + public boolean isEmbedded() { + return false; + } + + /** + * Returns the string contents of the serial number. Result is not intended to be + * displayed to humans. + * @see #toString() + */ + public String getString() { return ""; } + + + /** + */ + public SerialNumber getScannableDeviceSerialNumber() { + return this; + } + + //------------------------------------------------------------------------------------------------ + // Comparison + //------------------------------------------------------------------------------------------------ + + public boolean matches(Object pattern) { + return this.equals(pattern); + } + + @Override + public boolean equals(Object object) { + if (object == null) return false; + if (object == this) return true; + + if (object instanceof SerialNumber) { + return false; + } + + if (object instanceof String) { + return this.equals((String)object); + } + + return false; + } + + // separate method to avoid annoying Android Studio inspection warnings when comparing SerialNumber against String + public boolean equals(String string) { + return false; + } + + @Override + public int hashCode() { + return 0; + } + + //------------------------------------------------------------------------------------------------ + // Serial number display name management + //------------------------------------------------------------------------------------------------ + + protected static final HashMap deviceDisplayNames = new HashMap(); + + public static void noteSerialNumberType(SerialNumber serialNumber, String typeName) { + synchronized (deviceDisplayNames) { + deviceDisplayNames.put(serialNumber.getString(), Misc.formatForUser("%s [%s]", typeName, serialNumber)); + } + } + + public static String getDeviceDisplayName(SerialNumber serialNumber) { + synchronized (deviceDisplayNames) { + String result = deviceDisplayNames.get(serialNumber.getString()); + return result; + } + } + } diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/WilyCore.java b/WilyCore/src/main/java/com/wilyworks/simulator/WilyCore.java index 49297f3ab5ac..9c214b87cf6b 100644 --- a/WilyCore/src/main/java/com/wilyworks/simulator/WilyCore.java +++ b/WilyCore/src/main/java/com/wilyworks/simulator/WilyCore.java @@ -1,5 +1,6 @@ package com.wilyworks.simulator; +import static java.lang.ClassLoader.getSystemClassLoader; import static java.lang.System.nanoTime; import static java.lang.Thread.currentThread; import static java.lang.Thread.sleep; @@ -11,15 +12,15 @@ import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; import com.qualcomm.robotcore.eventloop.opmode.OpMode; import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.hardware.Gamepad; import com.qualcomm.robotcore.hardware.HardwareMap; import com.wilyworks.common.Wily; import com.wilyworks.common.WilyWorks; +import com.wilyworks.simulator.framework.Field; import com.wilyworks.simulator.framework.InputManager; import com.wilyworks.simulator.framework.Simulation; import com.wilyworks.simulator.framework.WilyTelemetry; -import com.qualcomm.robotcore.hardware.Gamepad; - -import com.wilyworks.simulator.framework.Field; +import com.wilyworks.simulator.framework.mechsim.MechSim; import org.firstinspires.ftc.robotcore.external.Telemetry; import org.reflections.Reflections; @@ -30,8 +31,6 @@ import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Image; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferStrategy; @@ -41,6 +40,7 @@ import java.io.StringWriter; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; @@ -105,7 +105,7 @@ public void windowClosing(WindowEvent e) { }); setSize(WINDOW_WIDTH, WINDOW_HEIGHT); - setLocation(400, 0); + setLocation(250, 0); setResizable(false); Choice dropDown = new Choice(); @@ -148,10 +148,13 @@ public void windowClosing(WindowEvent e) { button.addActionListener(actionEvent -> { switch (WilyCore.status.state) { - case STOPPED: - // Inform the main thread of the choice and save the preference: + case STOPPED: // It was in STOPPED state, now transition to INITIALIZED OpModeChoice opModeChoice = opModeChoices.get(dropDown.getSelectedIndex()); - WilyCore.status = new WilyCore.Status(WilyCore.State.INITIALIZED, opModeChoice.klass, button); + OpMode opMode = WilyCore.createOpMode(opModeChoice.klass); + + // Inform the main thread of the choice and save the preference: + WilyCore.opModeNotification("onOpModePreInit", opMode); + WilyCore.status = new WilyCore.Status(WilyCore.State.INITIALIZED, opMode, button); WilyCore.startTime = 0; dropDown.setMaximumSize(new Dimension(0, 0)); dropDown.setVisible(false); // Needed for long opMode names, for whatever reason @@ -162,15 +165,21 @@ public void windowClosing(WindowEvent e) { label.setText(opModeName); break; - case INITIALIZED: - WilyCore.status = new WilyCore.Status(WilyCore.State.STARTED, WilyCore.status.klass, button); + case INITIALIZED: // It was in INITIALIZED state, now transition to STARTED + WilyCore.opModeNotification("onOpModePreStart", WilyCore.status.opMode); + WilyCore.status = new WilyCore.Status(WilyCore.State.STARTED, null, button); WilyCore.startTime = WilyCore.wallClockTime(); button.setText("Stop"); break; - case STARTED: - WilyCore.opModeThread.interrupt(); + case STARTED: // It was in STARTED state, now transition to STOPPED + // I used to call WilyCore.opModeThread.interrupt(); here, but that prevented + // any user code from running after the Stop button was pressed. But maybe + // that's how it works on the robot, too? + // WilyCore.opModeThread.interrupt(); WilyCore.status = new WilyCore.Status(WilyCore.State.STOPPED, null, null); + WilyCore.opModeNotification("onOpModePostStop", WilyCore.status.opMode); + WilyCore.terminateOpMode(); button.setText("Init"); dropDown.setMaximumSize(new Dimension(400, 100)); dropDown.setVisible(true); @@ -262,6 +271,7 @@ public class WilyCore { public static Gamepad gamepad1; public static Gamepad gamepad2; public static HardwareMap hardwareMap; + public static MechSim mechSim; public static InputManager inputManager; public static DashboardWindow dashboardWindow; public static Telemetry telemetry; @@ -291,11 +301,11 @@ public static double time() { */ public enum State { STOPPED, INITIALIZED, STARTED } public static class Status { - public Class klass; + public OpMode opMode; public State state; public JButton stopButton; - public Status(State state, Class klass, JButton button) { - this.state = state; this.klass = klass; this.stopButton = button; + public Status(State state, OpMode opMode, JButton button) { + this.state = state; this.opMode = opMode; this.stopButton = button; } } @@ -318,7 +328,7 @@ static public void render(boolean startScreenOverlay) { dashboardWindow.errorLabel.setText(simulation.getErrorLabel()); } - // Advance the time: + // Advance the time. Zero means to use the realtime clock. static double advanceTime(double deltaT) { if (deltaT <= 0) { deltaT = nanoTime() * 1e-9 - lastUpdateWallClockTime; @@ -339,18 +349,6 @@ static double advanceTime(double deltaT) { // Callbacks provided to the guest. These are all called via reflection from the WilyWorks // class. - // The guest can specify the delta-t (which is handy when single stepping): - static public void updateSimulation(double deltaT) { - // Advance the time then advance the simulation: - deltaT = advanceTime(deltaT); - simulation.advance(deltaT); - - // Render everything: - render(); - - simulationUpdated = true; - } - // Set the robot to a given pose and (optional) velocity in the simulation. The // localizer will not register a move. static public void setStartPose(Pose2d pose, PoseVelocity2d velocity) { @@ -358,6 +356,17 @@ static public void setStartPose(Pose2d pose, PoseVelocity2d velocity) { simulation.setStartPose(pose, velocity); } + // The guest can specify the delta-t (which is handy when single stepping). Zero means to + // use the real-time clock. + static public void updateSimulation(double deltaT) { + // Advance the time then advance the simulation: + deltaT = advanceTime(deltaT); + simulation.advance(deltaT); + mechSim.advance(deltaT); + simulationUpdated = true; + render(); + } + // MecanumDrive uses this while running a trajectory to update the simulator as to its // current intermediate pose and velocity. This update will be reflected in the localizer // results. @@ -365,6 +374,7 @@ static public void runTo(Pose2d pose, PoseVelocity2d velocity) { // If the user didn't explicitly call the simulation update() API, do it now: double deltaT = advanceTime(0); simulation.runTo(deltaT, pose, velocity); + mechSim.advance(deltaT); simulationUpdated = true; render(); } @@ -479,25 +489,34 @@ static WilyWorks.Config getConfig(Class configKlass) { return new WilyWorks.Config(); } - // Call from the window manager to invoke the user's chosen "runOpMode" method: - static void runOpMode(Class klass) { + // Create the opMode from the user's choice of class. + static OpMode createOpMode(Class opModeClass) { OpMode opMode; try { //noinspection deprecation - opMode = (OpMode) klass.newInstance(); - } catch (InstantiationException|IllegalAccessException e) { + opMode = (OpMode) opModeClass.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } + // Before we initialize the HardwareMap, create this year's game simulation to go with + // the new opMode. It may override some of the HardwareMap. + mechSim = MechSim.create(); + // We need to re-instantiate hardware map on every run: hardwareMap = new HardwareMap(); - opMode.hardwareMap = hardwareMap; opMode.gamepad1 = gamepad1; opMode.gamepad2 = gamepad2; opMode.telemetry = telemetry; - if (LinearOpMode.class.isAssignableFrom(klass)) { + return opMode; + } + + // Call from the window manager to invoke the user's chosen "runOpMode" method. Instantiates + // the user's chosen opMode and invokes it. + static void runOpMode(OpMode opMode) { + if (LinearOpMode.class.isAssignableFrom(opMode.getClass())) { LinearOpMode linearOpMode = (LinearOpMode) opMode; try { linearOpMode.runOpMode(); @@ -527,15 +546,15 @@ static void runOpMode(Class klass) { // Thread dedicated to running the user's opMode: static class OpModeThread extends Thread { - Class opModeClass; - OpModeThread(Class opModeClass) { - this.opModeClass = opModeClass; + OpMode opMode; + OpModeThread(OpMode opMode) { + this.opMode = opMode; setName("Wily OpMode thread"); start(); } @Override public void run() { - WilyCore.runOpMode(status.klass); + WilyCore.runOpMode(opMode); } } @@ -556,6 +575,30 @@ public static Image getImage(String imagePath, int width, int height) { return image; } + // Do some cleanup when an OpMode is terminated: + public static void terminateOpMode() { + telemetry.clearAll(); + telemetry.log().clear(); + } + + // Call Sidekick Core's "onOpModePreInit", "onOpModePreStart", "onOpModePostStop" notifications. + public static void opModeNotification(String method, OpMode opMode) { + Class sidekickCore; + try { + sidekickCore = getSystemClassLoader().loadClass("com.loonybot.sidekick.Sidekick"); + } catch (ClassNotFoundException e) { + return; + } + try { + Method getInstance = sidekickCore.getMethod("getWilyWorksInstance"); + Object core = getInstance.invoke(null); + Method notification = sidekickCore.getMethod(method, OpMode.class); + notification.invoke(core, opMode); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + //////////////////////////////////////////////////////////////////////////////////////////////// // This is the application entry point that starts up all of Wily Works! public static void main(String[] args) @@ -593,7 +636,7 @@ public static void main(String[] args) // noinspection InfiniteLoopStatement while (true) { // Wait for the DashboardWindow UI to tell us what opMode to run: - while (status.state == State.STOPPED) { + while ((status.state == State.STOPPED) || (status.opMode == null)) { try { //noinspection BusyWait sleep(30); @@ -605,7 +648,7 @@ public static void main(String[] args) // The user has selected an opMode and pressed Init! Run the opMode on a dedicated // thread so that it can be interrupted as necessary: simulation.totalDistance = 0; - opModeThread = new OpModeThread(status.klass); + opModeThread = new OpModeThread(status.opMode); try { // Wait for the opMode thread to complete: opModeThread.join(); diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/Field.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/Field.java index 5beddcc7c733..ecf9230946e6 100644 --- a/WilyCore/src/main/java/com/wilyworks/simulator/framework/Field.java +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/Field.java @@ -6,9 +6,6 @@ import android.annotation.SuppressLint; import com.acmerobotics.dashboard.FtcDashboard; -import com.acmerobotics.roadrunner.Pose2d; -import com.qualcomm.robotcore.hardware.HardwareMap; -import com.qualcomm.robotcore.hardware.LED; import com.wilyworks.common.WilyWorks; import com.wilyworks.simulator.WilyCore; import com.wilyworks.simulator.helpers.Point; @@ -19,19 +16,14 @@ import java.awt.GraphicsConfiguration; import java.awt.GraphicsEnvironment; import java.awt.Image; -import java.awt.Polygon; import java.awt.Rectangle; import java.awt.RenderingHints; -import java.awt.Shape; import java.awt.Transparency; import java.awt.geom.AffineTransform; -import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; -import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import javax.imageio.ImageIO; @@ -64,7 +56,7 @@ public Field(Simulation simulation) { InputStream compassStream = classLoader.getResourceAsStream( "background/misc/compass-rose-white-text.png"); InputStream fieldStream = classLoader.getResourceAsStream( - "background/season-2024-intothedeep/field-2024-juice-dark.png"); + "background/season-2025-decode/field-2025-juice-dark.png"); try { if (compassStream != null) { @@ -133,38 +125,9 @@ private void initializeRobotImage() { round(ROBOT_IMAGE_HEIGHT * DIRECTION_LINE_HEIGHT)); } - // Render the outline of the robot's true position: - void renderRobot(Graphics2D g) { - Pose2d pose = simulation.getPose(0, true); - AffineTransform oldTransform = g.getTransform(); - g.translate(pose.position.x, pose.position.y); - g.rotate(pose.heading.log()); - g.setColor(Color.RED); - setAlpha(g, 0.5); - - g.draw(new Rectangle2D.Double( - -WilyCore.config.robotWidth / 2.0, -WilyCore.config.robotLength / 2.0, - WilyCore.config.robotWidth, WilyCore.config.robotLength)); - - // Restore the graphics state: - setAlpha(g, 1.0); - g.setTransform(oldTransform); - -// Pose2d simulationPose = simulation.getPose(0, true); -// AffineTransform imageTransform = new AffineTransform(); -// imageTransform.translate(simulationPose.position.x, simulationPose.position.y); -// imageTransform.scale(1.0 / ROBOT_IMAGE_WIDTH,1.0 / ROBOT_IMAGE_HEIGHT); -// imageTransform.rotate(simulationPose.heading.log() + Math.toRadians(90)); -// imageTransform.scale(WilyCore.config.robotWidth, WilyCore.config.robotLength); -// imageTransform.translate(-ROBOT_IMAGE_HEIGHT / 2.0, -ROBOT_IMAGE_HEIGHT / 2.0); -// setAlpha(g, 0.5); -// g.drawImage(robotImage, imageTransform, null); -// setAlpha(g, 1.0); - } - // Set the transform to use inches and have the origin at the center of field. This // returns the current transform to restore via Graphics2D.setTransform() once done: - public static AffineTransform setFieldTransform(Graphics2D g) { + public static AffineTransform setFieldViewportAndClip(Graphics2D g) { // Prime the viewport/transform and the clipping for field and overlay rendering: AffineTransform oldTransform = g.getTransform(); g.setClip(FIELD_VIEW.x, FIELD_VIEW.y, FIELD_VIEW.width, FIELD_VIEW.height); @@ -187,55 +150,6 @@ public static AffineTransform setPageFrameTransform(Graphics2D g) { return oldTransform; } - // Render the LED for REV Digital LEDs: - void renderLeds(Graphics2D g) { - HardwareMap hardwareMap = WilyCore.hardwareMap; - if (hardwareMap == null) - return; // Might not have been created yet - - final int[] colors = { 0, 0xff0000, 0x00ff00, 0xffbf00 }; // black, red, green, amber - final double radius = 2.0; // Circle radius, in inches - Pose2d pose = simulation.getPose(0, true); - - ArrayList ledArray = new ArrayList<>(); - for (LED led: hardwareMap.led) { - ledArray.add((WilyLED) led); - } - for (int i = 0; i < ledArray.size(); i++) { - WilyLED led = ledArray.get(i); - int colorIndex = 0; - colorIndex |= (led.isRed && led.enable) ? 1 : 0; - colorIndex |= (!led.isRed && led.enable) ? 2 : 0; - - // The LED actually needs two digital channels to describe all 4 possible colors. - // Assume that consecutively registered channels make a pair: - if (i + 1 < ledArray.size()) { - WilyLED nextLed = ledArray.get(i + 1); - if ((nextLed.x == led.x) && - (nextLed.y == led.y) && - (nextLed.isRed == !led.isRed)) { - - colorIndex |= (nextLed.isRed && nextLed.enable) ? 1 : 0; - colorIndex |= (!nextLed.isRed && nextLed.enable) ? 2 : 0; - i++; - } - } - - // Draw the circle at the location of the sensor on the robot, accounting for its - // current heading: - Point point = new Point(led.x, led.y) - .rotate(pose.heading.log()) - .add(new Point(pose.position)); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - g.setColor(new Color(0xffffff)); - g.fill(new Ellipse2D.Double(point.x - radius - 0.5, point.y - radius - 0.5,2 * radius + 1, 2 * radius + 1)); - g.setColor(new Color(colors[colorIndex])); - g.fill(new Ellipse2D.Double(point.x - radius, point.y - radius,2 * radius, 2 * radius)); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - } - } - // Render the field, the robot, and the field overlay: @SuppressLint("DefaultLocale") public void render(Graphics2D g, String caption) { @@ -246,13 +160,15 @@ public void render(Graphics2D g, String caption) { // Lay down the background image without needing a transform: g.drawImage(backgroundImage, FIELD_VIEW.x + FIELD_INSET, FIELD_VIEW.y + FIELD_INSET, null); - defaultTransform = g.getTransform(); - AffineTransform oldTransform = setFieldTransform(g); - if (FtcDashboard.fieldOverlay != null) + defaultTransform = setFieldViewportAndClip(g); + if (FtcDashboard.fieldOverlay != null) // Can't remember why this could be null FtcDashboard.fieldOverlay.render(g); - renderRobot(g); - renderLeds(g); - g.setTransform(oldTransform); + + // Render the robot via the appropriate MechSim: + if (WilyCore.mechSim != null) + WilyCore.mechSim.update(g, simulation.getPose(0, true)); + + g.setTransform(defaultTransform); } // Set the global alpha in the range [0.0, 1.0]: @@ -282,7 +198,7 @@ void renderFieldOfView(Graphics2D g, Point origin, double orientation, double fo public void renderStartScreenOverlay(Graphics2D g) { g.drawImage(compassImage, 20, 20, null); - AffineTransform oldTransform = setFieldTransform(g); + AffineTransform oldTransform = setFieldViewportAndClip(g); for (WilyWorks.Config.Camera camera: WilyCore.config.cameras) { g.setColor(new Color(0xffffff)); renderFieldOfView(g, new Point(camera.x, camera.y), camera.orientation, camera.fieldOfView); diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/InputManager.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/InputManager.java index c3d6d9fac007..ab675e0734cb 100644 --- a/WilyCore/src/main/java/com/wilyworks/simulator/framework/InputManager.java +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/InputManager.java @@ -195,12 +195,17 @@ class GamepadInput { GLFWGamepadState gamepadState; // Gamepad input object, null if no controller is plugged in GamepadInput() { - if (!GLFW.glfwInit()) { - System.out.println("Failed to initialize GLFW!"); - return; - } - if (GLFW.glfwJoystickIsGamepad(GLFW.GLFW_JOYSTICK_1)) { - gamepadState = GLFWGamepadState.create(); + // Don't attempt to do gamepads on Macs because of -XstartOnFirstThread is required + // on Macs and that magical incantation isn't working: + String osName = System.getProperty("os.name").toLowerCase(); + if (!osName.contains("mac")) { + if (!GLFW.glfwInit()) { + System.out.println("Failed to initialize GLFW!"); + return; + } + if (GLFW.glfwJoystickIsGamepad(GLFW.GLFW_JOYSTICK_1)) { + gamepadState = GLFWGamepadState.create(); + } } } @@ -348,6 +353,7 @@ void update(Gamepad gamepad, int gamepadId) { gamepad.right_trigger = getAxis(gamepadId, SDL.SDL_CONTROLLER_AXIS_TRIGGERRIGHT); gamepad.updateButtonAliases(); + gamepad.updateEdgeDetection(); } // Get a string describing which gamepads the inputs correspond to. diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyAnalogInput.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyAnalogInput.java new file mode 100644 index 000000000000..f5a9aea48b89 --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyAnalogInput.java @@ -0,0 +1,18 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.AnalogInput; + +/** + * Wily Works AnalogInput implementation. + */ +public class WilyAnalogInput extends AnalogInput { + @Override + public double getVoltage() { + return 0; + } + + @Override + public double getMaxVoltage() { + return 0; + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyCRServo.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyCRServo.java new file mode 100644 index 000000000000..c53e7c3b55db --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyCRServo.java @@ -0,0 +1,42 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.CRServo; +import com.qualcomm.robotcore.hardware.ServoController; + +/** + * Wily Works CRServo implementation. + */ +public class WilyCRServo extends WilyHardwareDevice implements CRServo { + double power; + Direction direction; + + @Override + public ServoController getController() { + return new WilyServoController(); + } + + @Override + public int getPortNumber() { + return 0; + } + + @Override + public void setDirection(Direction direction) { + this.direction = direction; + } + + @Override + public Direction getDirection() { + return direction; + } + + @Override + public void setPower(double power) { + this.power = power; + } + + @Override + public double getPower() { + return power; + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyColorSensor.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyColorSensor.java new file mode 100644 index 000000000000..8faab7df9acc --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyColorSensor.java @@ -0,0 +1,47 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.ColorSensor; +import com.qualcomm.robotcore.hardware.I2cAddr; + +/** + * Wily Works color sensor implementation. + */ +public class WilyColorSensor extends WilyHardwareDevice implements ColorSensor { + @Override + public int red() { + return 0; + } + + @Override + public int green() { + return 0; + } + + @Override + public int blue() { + return 0; + } + + @Override + public int alpha() { + return 0; + } + + @Override + public int argb() { + return 0; + } + + @Override + public void enableLed(boolean enable) { + } + + @Override + public void setI2cAddress(I2cAddr newAddress) { + } + + @Override + public I2cAddr getI2cAddress() { + return null; + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyDcMotorEx.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyDcMotorEx.java index 2597af4657ba..8000e43ffa1e 100644 --- a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyDcMotorEx.java +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyDcMotorEx.java @@ -11,6 +11,10 @@ * Wily Works DcMotorEx implementation. */ public class WilyDcMotorEx extends WilyHardwareDevice implements DcMotorEx { + RunMode mode; + double velocity; + double power; + Direction direction; @Override public DcMotorController getController() { @@ -40,6 +44,7 @@ public boolean getPowerFloat() { @Override public void setTargetPosition(int position) { + } @Override @@ -59,12 +64,12 @@ public int getCurrentPosition() { @Override public void setMode(RunMode mode) { - + this.mode = mode; } @Override public RunMode getMode() { - return null; + return mode; } @Override @@ -84,7 +89,7 @@ public boolean isMotorEnabled() { @Override public void setVelocity(double angularRate) { - + velocity = angularRate; } @Override @@ -94,7 +99,7 @@ public void setVelocity(double angularRate, AngleUnit unit) { @Override public double getVelocity() { - return 0; + return velocity; } @Override @@ -143,18 +148,20 @@ public boolean isOverCurrent() { } @Override - public void setDirection(Direction direction) { } + public void setDirection(Direction direction) { + this.direction = direction; + } @Override public Direction getDirection() { - return null; + return direction; } @Override - public void setPower(double power) { } + public void setPower(double power) { this.power = power; } @Override public double getPower() { - return 0; + return power; } } diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyDigitalChannel.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyDigitalChannel.java new file mode 100644 index 000000000000..13c4752bb0a4 --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyDigitalChannel.java @@ -0,0 +1,29 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.DigitalChannel; + +/** + * Wily Works DigitalChannel implementation. + */ +public class WilyDigitalChannel extends WilyHardwareDevice implements DigitalChannel { + boolean state; + + @Override + public Mode getMode() { + return null; + } + + @Override + public void setMode(Mode mode) { + } + + @Override + public boolean getState() { + return state; + } + + @Override + public void setState(boolean state) { + this.state = state; + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyDistanceSensor.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyDistanceSensor.java new file mode 100644 index 000000000000..05471c831737 --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyDistanceSensor.java @@ -0,0 +1,15 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.DistanceSensor; + +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; + +/** + * Wily Works distance sensor implementation. + */ +public class WilyDistanceSensor extends WilyHardwareDevice implements DistanceSensor { + @Override + public double getDistance(DistanceUnit unit) { + return unit.fromMm(65535); + } // Distance when not responding +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyGamepad.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyGamepad.java index e8776118bb18..429961f96acb 100644 --- a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyGamepad.java +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyGamepad.java @@ -1,15 +1,146 @@ package com.wilyworks.simulator.framework; -import com.qualcomm.robotcore.util.Range; +import com.qualcomm.robotcore.hardware.Gamepad; + +import org.firstinspires.ftc.robotcore.internal.ui.GamepadUser; import java.util.ArrayList; +/** + * Monitors changes to buttons on a Gamepad. + *

+ * wasPressed() will return true if the button was pressed since the last call + * of that method. + *

+ * wasReleased() will return ture if the button was released since the last call + * of that method. + *

+ * @see Gamepad + */ +class GamepadStateChanges { + protected ButtonStateMonitor dpadDown = new ButtonStateMonitor(); + protected ButtonStateMonitor dpadLeft = new ButtonStateMonitor(); + protected ButtonStateMonitor dpadRight = new ButtonStateMonitor(); + protected ButtonStateMonitor a = new ButtonStateMonitor(); + protected ButtonStateMonitor dpadUp = new ButtonStateMonitor(); + protected ButtonStateMonitor b = new ButtonStateMonitor(); + protected ButtonStateMonitor x = new ButtonStateMonitor(); + protected ButtonStateMonitor y = new ButtonStateMonitor(); + protected ButtonStateMonitor guide = new ButtonStateMonitor(); + protected ButtonStateMonitor start = new ButtonStateMonitor(); + protected ButtonStateMonitor back = new ButtonStateMonitor(); + protected ButtonStateMonitor leftBumper = new ButtonStateMonitor(); + protected ButtonStateMonitor rightBumper = new ButtonStateMonitor(); + protected ButtonStateMonitor leftStickButton = new ButtonStateMonitor(); + protected ButtonStateMonitor rightStickButton = new ButtonStateMonitor(); + protected ButtonStateMonitor circle = new ButtonStateMonitor(); + protected ButtonStateMonitor cross = new ButtonStateMonitor(); + protected ButtonStateMonitor triangle = new ButtonStateMonitor(); + protected ButtonStateMonitor square = new ButtonStateMonitor(); + protected ButtonStateMonitor share = new ButtonStateMonitor(); + protected ButtonStateMonitor options = new ButtonStateMonitor(); + protected ButtonStateMonitor touchpad = new ButtonStateMonitor(); + protected ButtonStateMonitor ps = new ButtonStateMonitor(); + + protected class ButtonStateMonitor { + private boolean lastPressed = false; + private boolean pressNotify = false; + private boolean releaseNotify = false; + + private void update(boolean nowPressed) { + if (!lastPressed && nowPressed) { + pressNotify = true; + } + if (lastPressed && !nowPressed) { + releaseNotify = true; + } + lastPressed = nowPressed; + } + + protected boolean wasPressed() { + boolean pressed = pressNotify; + pressNotify = false; + return pressed; + } + + protected boolean wasReleased() { + boolean released = releaseNotify; + releaseNotify = false; + return released; + } + } + + protected void updateAllButtons(WilyGamepad gamepad) { + dpadUp.update(gamepad.dpad_up); + dpadDown.update(gamepad.dpad_down); + dpadLeft.update(gamepad.dpad_left); + dpadRight.update(gamepad.dpad_right); + a.update(gamepad.a); + b.update(gamepad.b); + x.update(gamepad.x); + y.update(gamepad.y); + guide.update(gamepad.guide); + start.update(gamepad.start); + back.update(gamepad.back); + leftBumper.update(gamepad.left_bumper); + rightBumper.update(gamepad.right_bumper); + leftStickButton.update(gamepad.left_stick_button); + rightStickButton.update(gamepad.right_stick_button); + circle.update(gamepad.circle); + cross.update(gamepad.cross); + triangle.update(gamepad.triangle); + square.update(gamepad.square); + share.update(gamepad.share); + options.update(gamepad.options); + touchpad.update(gamepad.touchpad); + ps.update(gamepad.ps); + } +} + /** * Wily Works Gamepad implementation that takes input either from a connected gamepad or * from the keyboard. */ public class WilyGamepad { + + public enum Type { + // Do NOT change the order/names of existing entries, + // you will break backwards compatibility!! + UNKNOWN(LegacyType.UNKNOWN), + LOGITECH_F310(LegacyType.LOGITECH_F310), + XBOX_360(LegacyType.XBOX_360), + SONY_PS4(LegacyType.SONY_PS4), // This indicates a PS4-compatible controller that is being used through our compatibility mode + SONY_PS4_SUPPORTED_BY_KERNEL(LegacyType.SONY_PS4); // This indicates a PS4-compatible controller that is being used through the DualShock 4 Linux kernel driver. + + private final LegacyType correspondingLegacyType; + Type(LegacyType correspondingLegacyType) { + this.correspondingLegacyType = correspondingLegacyType; + } + } + + // LegacyType is necessary because robocol gamepad version 3 was written in a way that was not + // forwards-compatible, so we have to keep sending V3-compatible values. + public enum LegacyType { + // Do NOT change the order or names of existing entries, or add new entries. + // You will break backwards compatibility!! + UNKNOWN, + LOGITECH_F310, + XBOX_360, + SONY_PS4; + } + + /** + * Get the type of gamepad as a {@link Type}. This method defaults to "UNKNOWN". + * @return gamepad type + */ + public Type type() { + return type; + } + + @SuppressWarnings("UnusedAssignment") + public volatile Type type = Type.XBOX_360; // IntelliJ thinks this is redundant, but it is NOT. Must be a bug in the analyzer? + public volatile float left_stick_x = 0f; public volatile float left_stick_y = 0f; public volatile float right_stick_x = 0f; @@ -50,6 +181,11 @@ public class WilyGamepad { public WilyGamepad() { } + /** + * Edge detection for gamepads + */ + private volatile GamepadStateChanges changes = new GamepadStateChanges(); + public void updateButtonAliases(){ // There is no assignment for touchpad because there is no equivalent on XBOX controllers. circle = b; @@ -61,6 +197,10 @@ public void updateButtonAliases(){ ps = guide; } + public void updateEdgeDetection(){ + changes.updateAllButtons(this); + } + public void runRumbleEffect(RumbleEffect effect) { } public void rumble(int durationMs) { } public void rumble(double rumble1, double rumble2, int durationMs) { } @@ -93,4 +233,377 @@ public RumbleEffect build() { } } + public GamepadUser getUser() { + return GamepadUser.ONE; + } + + /** + * Checks if dpad_up was pressed since the last call of this method + * @return true if dpad_up was pressed since the last call of this method; otherwise false + */ + public boolean dpadUpWasPressed() { + return changes.dpadUp.wasPressed(); + } + + /** + * Checks if dpad_up was released since the last call of this method + * @return true if dpad_up was released since the last call of this method; otherwise false + */ + public boolean dpadUpWasReleased() { + return changes.dpadUp.wasReleased(); + } + + /** + * Checks if dpad_down was pressed since the last call of this method + * @return true if dpad_down was pressed since the last call of this method; otherwise false + */ + public boolean dpadDownWasPressed() { + return changes.dpadDown.wasPressed(); + } + + /** + * Checks if dpad_down was released since the last call of this method + * @return true if dpad_down was released since the last call of this method; otherwise false + */ + public boolean dpadDownWasReleased() { + return changes.dpadDown.wasReleased(); + } + + /** + * Checks if dpad_left was pressed since the last call of this method + * @return true if dpad_left was pressed since the last call of this method; otherwise false + */ + public boolean dpadLeftWasPressed() { + return changes.dpadLeft.wasPressed(); + } + + /** + * Checks if dpad_left was released since the last call of this method + * @return true if dpad_left was released since the last call of this method; otherwise false + */ + public boolean dpadLeftWasReleased() { + return changes.dpadLeft.wasReleased(); + } + + /** + * Checks if dpad_right was pressed since the last call of this method + * @return true if dpad_right was pressed since the last call of this method; otherwise false + */ + public boolean dpadRightWasPressed() { + return changes.dpadRight.wasPressed(); + } + + /** + * Checks if dpad_right was released since the last call of this methmethodod + * @return true if dpad_right was released since the last call of this ; otherwise false + */ + public boolean dpadRightWasReleased() { + return changes.dpadRight.wasReleased(); + } + + /** + * Checks if a was pressed since the last call of this method + * @return true if a was pressed since the last call of this method; otherwise false + */ + public boolean aWasPressed() { + return changes.a.wasPressed(); + } + + /** + * Checks if a was released since the last call of this method + * @return true if a was released since the last call of this method; otherwise false + */ + public boolean aWasReleased() { + return changes.a.wasReleased(); + } + + /** + * Checks if b was pressed since the last call of this method + * @return true if b was pressed since the last call of this method; otherwise false + */ + public boolean bWasPressed() { + return changes.b.wasPressed(); + } + + /** + * Checks if b was released since the last call of this method + * @return true if b was released since the last call of this method; otherwise false + */ + public boolean bWasReleased() { + return changes.b.wasReleased(); + } + + /** + * Checks if x was pressed since the last call of this method + * @return true if x was pressed since the last call of this method; otherwise false + */ + public boolean xWasPressed() { + return changes.x.wasPressed(); + } + + /** + * Checks if x was released since the last call of this method + * @return true if x was released since the last call of this method; otherwise false + */ + public boolean xWasReleased() { + return changes.x.wasReleased(); + } + + /** + * Checks if y was pressed since the last call of this method + * @return true if y was pressed since the last call of this method; otherwise false + */ + public boolean yWasPressed() { + return changes.y.wasPressed(); + } + + /** + * Checks if y was released since the last call of this method + * @return true if y was released since the last call of this method; otherwise false + */ + public boolean yWasReleased() { + return changes.y.wasReleased(); + } + + /** + * Checks if guide was pressed since the last call of this method + * @return true if guide was pressed since the last call of this method; otherwise false + */ + public boolean guideWasPressed() { + return changes.guide.wasPressed(); + } + + /** + * Checks if guide was released since the last call of this method + * @return true if guide was released since the last call of this method; otherwise false + */ + public boolean guideWasReleased() { + return changes.guide.wasReleased(); + } + + /** + * Checks if start was pressed since the last call of this method + * @return true if start was pressed since the last call of this method; otherwise false + */ + public boolean startWasPressed() { + return changes.start.wasPressed(); + } + + /** + * Checks if start was released since the last call of this method + * @return true if start was released since the last call of this method; otherwise false + */ + public boolean startWasReleased() { + return changes.start.wasReleased(); + } + + /** + * Checks if back was pressed since the last call of this method + * @return true if back was pressed since the last call of this method; otherwise false + */ + public boolean backWasPressed() { + return changes.back.wasPressed(); + } + + /** + * Checks if back was released since the last call of this method + * @return true if back was released since the last call of this method; otherwise false + */ + public boolean backWasReleased() { + return changes.back.wasReleased(); + } + + /** + * Checks if left_bumper was pressed since the last call of this method + * @return true if left_bumper was pressed since the last call of this method; otherwise false + */ + public boolean leftBumperWasPressed() { + return changes.leftBumper.wasPressed(); + } + + /** + * Checks if left_bumper was released since the last call of this method + * @return true if left_bumper was released since the last call of this method; otherwise false + */ + public boolean leftBumperWasReleased() { + return changes.leftBumper.wasReleased(); + } + + /** + * Checks if right_bumper was pressed since the last call of this method + * @return true if right_bumper was pressed since the last call of this method; otherwise false + */ + public boolean rightBumperWasPressed() { + return changes.rightBumper.wasPressed(); + } + + /** + * Checks if right_bumper was released since the last call of this method + * @return true if right_bumper was released since the last call of this method; otherwise false + */ + public boolean rightBumperWasReleased() { + return changes.rightBumper.wasReleased(); + } + + /** + * Checks if left_stick_button was pressed since the last call of this method + * @return true if left_stick_button was pressed since the last call of this method; otherwise false + */ + public boolean leftStickButtonWasPressed() { + return changes.leftStickButton.wasPressed(); + } + + /** + * Checks if left_stick_button was released since the last call of this method + * @return true if left_stick_button was released since the last call of this method; otherwise false + */ + public boolean leftStickButtonWasReleased() { + return changes.leftStickButton.wasReleased(); + } + + /** + * Checks if right_stick_button was pressed since the last call of this method + * @return true if right_stick_button was pressed since the last call of this method; otherwise false + */ + public boolean rightStickButtonWasPressed() { + return changes.rightStickButton.wasPressed(); + } + + /** + * Checks if right_stick_button was released since the last call of this method + * @return true if right_stick_button was released since the last call of this method; otherwise false + */ + public boolean rightStickButtonWasReleased() { + return changes.rightStickButton.wasReleased(); + } + + /** + * Checks if circle was pressed since the last call of this method + * @return true if circle was pressed since the last call of this method; otherwise false + */ + public boolean circleWasPressed() { + return changes.circle.wasPressed(); + } + + /** + * Checks if circle was released since the last call of this method + * @return true if circle was released since the last call of this method; otherwise false + */ + public boolean circleWasReleased() { + return changes.circle.wasReleased(); + } + + /** + * Checks if cross was pressed since the last call of this method + * @return true if cross was pressed since the last call of this method; otherwise false + */ + public boolean crossWasPressed() { + return changes.cross.wasPressed(); + } + + /** + * Checks if cross was released since the last call of this method + * @return true if cross was released since the last call of this method; otherwise false + */ + public boolean crossWasReleased() { + return changes.cross.wasReleased(); + } + + /** + * Checks if triangle was pressed since the last call of this method + * @return true if triangle was pressed since the last call of this method; otherwise false + */ + public boolean triangleWasPressed() { + return changes.triangle.wasPressed(); + } + + /** + * Checks if triangle was released since the last call of this method + * @return true if triangle was released since the last call of this method; otherwise false + */ + public boolean triangleWasReleased() { + return changes.triangle.wasReleased(); + } + + /** + * Checks if square was pressed since the last call of this method + * @return true if square was pressed since the last call of this method; otherwise false + */ + public boolean squareWasPressed() { + return changes.square.wasPressed(); + } + + /** + * Checks if square was released since the last call of this method + * @return true if square was released since the last call of this method; otherwise false + */ + public boolean squareWasReleased() { + return changes.square.wasReleased(); + } + + /** + * Checks if share was pressed since the last call of this method + * @return true if share was pressed since the last call of this method; otherwise false + */ + public boolean shareWasPressed() { + return changes.share.wasPressed(); + } + + /** + * Checks if share was released since the last call of this method + * @return true if share was released since the last call of this method; otherwise false + */ + public boolean shareWasReleased() { + return changes.share.wasReleased(); + } + + /** + * Checks if options was pressed since the last call of this method + * @return true if options was pressed since the last call of this method; otherwise false + */ + public boolean optionsWasPressed() { + return changes.options.wasPressed(); + } + + /** + * Checks if options was released since the last call of this method + * @return true if options was released since the last call of this method; otherwise false + */ + public boolean optionsWasReleased() { + return changes.options.wasReleased(); + } + + /** + * Checks if touchpad was pressed since the last call of this method + * @return true if touchpad was pressed since the last call of this method; otherwise false + */ + public boolean touchpadWasPressed() { + return changes.touchpad.wasPressed(); + } + + /** + * Checks if touchpad was released since the last call of this method + * @return true if touchpad was released since the last call of this method; otherwise false + */ + public boolean touchpadWasReleased() { + return changes.touchpad.wasReleased(); + } + + /** + * Checks if ps was pressed since the last call of this method + * @return true if ps was pressed since the last call of this method; otherwise false + */ + public boolean psWasPressed() { + return changes.ps.wasPressed(); + } + + /** + * Checks if ps was released since the last call of this method + * @return true if ps was released since the last call of this method; otherwise false + */ + public boolean psWasReleased() { + return changes.ps.wasReleased(); + } + + } diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyHardwareDevice.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyHardwareDevice.java index e53f8fa883dd..42aab53a9722 100644 --- a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyHardwareDevice.java +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyHardwareDevice.java @@ -13,9 +13,7 @@ public Manufacturer getManufacturer() { } @Override - public String getDeviceName() { - return ""; - } + public String getDeviceName() { return ""; } @Override public String getConnectionInfo() { diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyHardwareMap.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyHardwareMap.java index 053526501945..64bc601d1bf5 100644 --- a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyHardwareMap.java +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyHardwareMap.java @@ -6,37 +6,27 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.qualcomm.hardware.gobilda.GoBildaPinpointDriver; +import com.qualcomm.hardware.limelightvision.Limelight3A; +import com.qualcomm.hardware.sparkfun.SparkFunLEDStick; import com.qualcomm.hardware.sparkfun.SparkFunOTOS; +import com.qualcomm.robotcore.eventloop.opmode.OpModeManagerNotifier; +import com.qualcomm.robotcore.hardware.AnalogInput; import com.qualcomm.robotcore.hardware.CRServo; import com.qualcomm.robotcore.hardware.ColorSensor; import com.qualcomm.robotcore.hardware.DcMotor; import com.qualcomm.robotcore.hardware.DigitalChannel; import com.qualcomm.robotcore.hardware.DistanceSensor; import com.qualcomm.robotcore.hardware.HardwareDevice; -import com.qualcomm.robotcore.hardware.I2cAddr; import com.qualcomm.robotcore.hardware.IMU; import com.qualcomm.robotcore.hardware.LED; import com.qualcomm.robotcore.hardware.NormalizedColorSensor; -import com.qualcomm.robotcore.hardware.NormalizedRGBA; import com.qualcomm.robotcore.hardware.Servo; -import com.qualcomm.robotcore.hardware.ServoController; import com.qualcomm.robotcore.hardware.VoltageSensor; import com.qualcomm.robotcore.util.SerialNumber; -import com.wilyworks.common.WilyWorks; import com.wilyworks.simulator.WilyCore; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraCharacteristics; import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; -import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; -import org.firstinspires.ftc.robotcore.external.navigation.AngularVelocity; -import org.firstinspires.ftc.robotcore.external.navigation.AxesOrder; -import org.firstinspires.ftc.robotcore.external.navigation.AxesReference; -import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; -import org.firstinspires.ftc.robotcore.external.navigation.Orientation; -import org.firstinspires.ftc.robotcore.external.navigation.Quaternion; -import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; -import org.firstinspires.ftc.robotcore.internal.system.Deadline; -import org.swerverobotics.ftc.GoBildaPinpointDriver; import org.swerverobotics.ftc.UltrasonicDistanceSensor; import java.util.ArrayList; @@ -49,345 +39,6 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; -import java.util.function.Consumer; - -import kotlin.coroutines.Continuation; - -/** - * Wily Works simulated IMU implementation. - */ -class WilyIMU extends WilyHardwareDevice implements IMU { - double startYaw; - @Override - public boolean initialize(Parameters parameters) { - resetYaw(); - return true; - } - - @Override - public void resetYaw() { - startYaw = WilyWorks.getPose().heading.log(); - } - - @Override - public YawPitchRollAngles getRobotYawPitchRollAngles() { - return new YawPitchRollAngles( - AngleUnit.RADIANS, - WilyWorks.getPose().heading.log() - startYaw, - 0, - 0, - 0); - } - - @Override - public Orientation getRobotOrientation(AxesReference reference, AxesOrder order, AngleUnit angleUnit) { - return new Orientation(); - } - - @Override - public Quaternion getRobotOrientationAsQuaternion() { - return new Quaternion(); - } - - @Override - public AngularVelocity getRobotAngularVelocity(AngleUnit angleUnit) { - return new AngularVelocity( - angleUnit, - (float) WilyWorks.getPoseVelocity().angVel, // ### transformedAngularVelocityVector.get(0), - 0, // ### transformedAngularVelocityVector.get(1), - 0, // ### transformedAngularVelocityVector.get(2), - 0); // ### rawAngularVelocity.acquisitionTime); - } -} - -/** - * Wily Works voltage sensor implementation. - */ -class WilyVoltageSensor extends WilyHardwareDevice implements VoltageSensor { - @Override - public double getVoltage() { - return 13.0; - } - - @Override - public String getDeviceName() { - return "Voltage Sensor"; - } -} - -/** - * Wily Works distance sensor implementation. - */ -class WilyDistanceSensor extends WilyHardwareDevice implements DistanceSensor { - @Override - public double getDistance(DistanceUnit unit) { return unit.fromMm(65535); } // Distance when not responding -} - -/** - * Wily Works normalized color sensor implementation. - */ -class WilyNormalizedColorSensor extends WilyHardwareDevice implements NormalizedColorSensor { - @Override - public NormalizedRGBA getNormalizedColors() { return new NormalizedRGBA(); } - - @Override - public float getGain() { return 0; } - - @Override - public void setGain(float newGain) { } -} - -/** - * Wily Works color sensor implementation. - */ -class WilyColorSensor extends WilyHardwareDevice implements ColorSensor { - - @Override - public int red() { return 0; } - - @Override - public int green() { return 0; } - - @Override - public int blue() { return 0; } - - @Override - public int alpha() { return 0; } - - @Override - public int argb() { return 0; } - - @Override - public void enableLed(boolean enable) { } - - @Override - public void setI2cAddress(I2cAddr newAddress) { } - - @Override - public I2cAddr getI2cAddress() { return null; } -} - -/** - * Wily Works named webcam implementation. - */ -class WilyWebcam extends WilyHardwareDevice implements WebcamName { - WilyWorks.Config.Camera wilyCamera; - - WilyWebcam(String deviceName) { - for (WilyWorks.Config.Camera camera: WilyCore.config.cameras) { - if (camera.name.equals(deviceName)) { - wilyCamera = camera; - } - } - if (wilyCamera == null) { - System.out.printf("WilyWorks: Couldn't find configuration data for camera '%s'", deviceName); - } - } - - @Override - public boolean isWebcam() { - return true; - } - - @Override - public boolean isCameraDirection() { - return false; - } - - @Override - public boolean isSwitchable() { - return false; - } - - @Override - public boolean isUnknown() { - return false; - } - - @Override - public void asyncRequestCameraPermission(Context context, Deadline deadline, Continuation> continuation) { - - } - - @Override - public boolean requestCameraPermission(Deadline deadline) { - return false; - } - - @Override - public CameraCharacteristics getCameraCharacteristics() { - return null; - } - - @Override - public SerialNumber getSerialNumber() { - return null; - } - - @Nullable - @Override - public String getUsbDeviceNameIfAttached() { - return null; - } - - @Override - public boolean isAttached() { - return false; - } -} - -/** - * Wily Works ServoController implementation. - */ -class WilyServoController extends WilyHardwareDevice implements ServoController { - @Override - public void pwmEnable() { } - - @Override - public void pwmDisable() { } - - @Override - public PwmStatus getPwmStatus() { return PwmStatus.DISABLED; } - - @Override - public void setServoPosition(int servo, double position) { } - - @Override - public double getServoPosition(int servo) { return 0; } - - @Override - public void close() { } -} - -/** - * Wily Works Servo implementation. - */ -class WilyServo extends WilyHardwareDevice implements Servo { - double position; - - @Override - public ServoController getController() { - return new WilyServoController(); - } - - @Override - public int getPortNumber() { - return 0; - } - - @Override - public void setDirection(Direction direction) { - - } - - @Override - public Direction getDirection() { - return null; - } - - @Override - public void setPosition(double position) { this.position = Math.max(0, Math.min(1, position)); } - - @Override - public double getPosition() {return position; } - - @Override - public void scaleRange(double min, double max) { - - } -} - -/** - * Wily Works CRServo implementation. - */ -class WilyCRServo extends WilyHardwareDevice implements CRServo { - - @Override - public ServoController getController() { - return new WilyServoController(); - } - - @Override - public int getPortNumber() { - return 0; - } - - @Override - public void setDirection(Direction direction) { - - } - - @Override - public Direction getDirection() { - return null; - } - - @Override - public void setPower(double power) { - - } - - @Override - public double getPower() { - return 0; - } -} - -/** - * Wily Works DigitalChannel implementation. - */ -class WilyDigitalChannel extends WilyHardwareDevice implements DigitalChannel { - boolean state; - - @Override - public Mode getMode() { return null; } - - @Override - public void setMode(Mode mode) {} - - @Override - public boolean getState() { return state; } - - @Override - public void setState(boolean state) { this.state = state; } -} - -/** - * Wily Works LED implementation. - */ -class WilyLED extends LED { - // Assume that every digital channels is a REV LED indicator. Doesn't hurt if that's not - // the case: - boolean enable = true; // They're always on by default - double x; - double y; - boolean isRed; - WilyLED(String deviceName) { - WilyWorks.Config.LEDIndicator wilyLed = null; - for (WilyWorks.Config.LEDIndicator led: WilyCore.config.ledIndicators) { - if (led.name.equals(deviceName)) { - wilyLed = led; - } - } - if (wilyLed != null) { - x = wilyLed.x; - y = wilyLed.y; - isRed = wilyLed.isRed; - } else { - isRed = !(deviceName.toLowerCase().contains("green")); - } - } - - @Override - public void enableLight(boolean enable) { - this.enable = enable; - } - - @Override - public boolean isLightOn() { - return enable; - } -} /** * Wily Works hardware map. @@ -407,10 +58,18 @@ public class WilyHardwareMap implements Iterable { public DeviceMapping sparkFunOTOS = new DeviceMapping<>(SparkFunOTOS.class); public DeviceMapping goBildaPinpointDrivers = new DeviceMapping<>(GoBildaPinpointDriver.class); public DeviceMapping ultrasonicDistanceSensor = new DeviceMapping<>(UltrasonicDistanceSensor.class); + public DeviceMapping analogInput = new DeviceMapping<>(AnalogInput.class); + public DeviceMapping imu = new DeviceMapping<>(IMU.class); + public DeviceMapping limelight = new DeviceMapping<>(Limelight3A.class); + public DeviceMapping sparkFunLEDStick = new DeviceMapping<>(SparkFunLEDStick.class); protected Map> allDevicesMap = new HashMap<>(); protected List allDevicesList = new ArrayList<>(); - public WilyHardwareMap() { + public final List> allDeviceMappings = new ArrayList<>(); + + public WilyHardwareMap(Context appContext, OpModeManagerNotifier notifier) { + // Road Runner expects this object to be already created because it references + // hardwareMap.voltageSensor.iterator().next() directly: put("voltage_sensor", VoltageSensor.class); } @@ -436,9 +95,6 @@ public T tryGet(Class classOrInterface, String deviceName) { return get(classOrInterface, deviceName); } public T get(Class classOrInterface, String deviceName) { - if (classOrInterface.equals(IMU.class)) { - return (T) new WilyIMU(); - } T result = wilyTryGet(classOrInterface, deviceName); if (result == null) { // Wily Works behavior is that we automatically add the device if it's not found: @@ -448,13 +104,15 @@ public T get(Class classOrInterface, String deviceName) { return result; } - @Deprecated public HardwareDevice get(String deviceName) { - List list = allDevicesMap.get(deviceName.trim()); + deviceName = deviceName.trim(); + List list = allDevicesMap.get(deviceName); if (list != null) { - return list.get(0); + for (HardwareDevice device : list) { + initializeDeviceIfNecessary(device); + return device; + } } - throw new IllegalArgumentException("Use the typed version of get(), e.g. get(DcMotorEx.class, \"leftMotor\")"); } @@ -497,18 +155,36 @@ public synchronized void put(String deviceName, Class klass) { } else if (LED.class.isAssignableFrom(klass)) { device = new WilyLED(deviceName); led.put(deviceName, (LED) device); + } else if (AnalogInput.class.isAssignableFrom(klass)) { + device = new WilyAnalogInput(); + analogInput.put(deviceName, (WilyAnalogInput) device); + } else if (IMU.class.isAssignableFrom(klass)) { + device = new WilyIMU(); + imu.put(deviceName, (WilyIMU) device); + + // Not actually built into HardwareMap.java: + } else if (UltrasonicDistanceSensor.class.isAssignableFrom(klass)) { + device = new WilyUltrasonicDistanceSensor(deviceName); + ultrasonicDistanceSensor.put(deviceName, (WilyUltrasonicDistanceSensor) device); } else if (SparkFunOTOS.class.isAssignableFrom(klass)) { device = new SparkFunOTOS(null); sparkFunOTOS.put(deviceName, (SparkFunOTOS) device); } else if (GoBildaPinpointDriver.class.isAssignableFrom(klass)) { device = new GoBildaPinpointDriver(null, false); goBildaPinpointDrivers.put(deviceName, (GoBildaPinpointDriver) device); - } else if (UltrasonicDistanceSensor.class.isAssignableFrom(klass)) { - device = new WilyUltrasonicDistanceSensor(deviceName); - ultrasonicDistanceSensor.put(deviceName, (WilyUltrasonicDistanceSensor) device); + } else if (Limelight3A.class.isAssignableFrom(klass)) { + device = new Limelight3A(null, "", null); + limelight.put(deviceName, (Limelight3A) device); + } else if (SparkFunLEDStick.class.isAssignableFrom(klass)) { + device = new SparkFunLEDStick(null, false); + sparkFunLEDStick.put(deviceName, (SparkFunLEDStick) device); } else { throw new IllegalArgumentException("Unexpected device type for HardwareMap"); } + + // Let the game simulation change the behavior of this device: + device = WilyCore.mechSim.hookDevice(deviceName, device); + list.add(device); allDevicesList.add(device); } @@ -524,7 +200,12 @@ private void initializeMultipleDevicesIfNecessary(Iterable getAllNames(Class classOrInterface) { SortedSet result = new TreeSet<>(); - result.add("voltage_sensor"); + for (String userName: allDevicesMap.keySet()) { + HardwareDevice device = allDevicesMap.get(userName).get(0); + if (classOrInterface.isInstance(device)) { + result.add(userName); + } + } return result; } diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyIMU.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyIMU.java new file mode 100644 index 000000000000..b26da584b708 --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyIMU.java @@ -0,0 +1,60 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.IMU; +import com.wilyworks.common.WilyWorks; + +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AngularVelocity; +import org.firstinspires.ftc.robotcore.external.navigation.AxesOrder; +import org.firstinspires.ftc.robotcore.external.navigation.AxesReference; +import org.firstinspires.ftc.robotcore.external.navigation.Orientation; +import org.firstinspires.ftc.robotcore.external.navigation.Quaternion; +import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; + +/** + * Wily Works simulated IMU implementation. + */ +public class WilyIMU extends WilyHardwareDevice implements IMU { + double startYaw; + + @Override + public boolean initialize(Parameters parameters) { + resetYaw(); + return true; + } + + @Override + public void resetYaw() { + startYaw = WilyWorks.getPose().heading.log(); + } + + @Override + public YawPitchRollAngles getRobotYawPitchRollAngles() { + return new YawPitchRollAngles( + AngleUnit.RADIANS, + WilyWorks.getPose().heading.log() - startYaw, + 0, + 0, + 0); + } + + @Override + public Orientation getRobotOrientation(AxesReference reference, AxesOrder order, AngleUnit angleUnit) { + return new Orientation(); + } + + @Override + public Quaternion getRobotOrientationAsQuaternion() { + return new Quaternion(); + } + + @Override + public AngularVelocity getRobotAngularVelocity(AngleUnit angleUnit) { + return new AngularVelocity( + angleUnit, + (float) WilyWorks.getPoseVelocity().angVel, // ### transformedAngularVelocityVector.get(0), + 0, // ### transformedAngularVelocityVector.get(1), + 0, // ### transformedAngularVelocityVector.get(2), + 0); // ### rawAngularVelocity.acquisitionTime); + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyLED.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyLED.java new file mode 100644 index 000000000000..1907266f4471 --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyLED.java @@ -0,0 +1,44 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.LED; +import com.wilyworks.common.WilyWorks; +import com.wilyworks.simulator.WilyCore; + +/** + * Wily Works LED implementation. + */ +public class WilyLED extends LED { + // Assume that every digital channels is a REV LED indicator. Doesn't hurt if that's not + // the case: + public boolean enable = true; // They're always on by default + public double x; + public double y; + public boolean isRed; + public WilyLED(String deviceName) { + WilyWorks.Config.LEDIndicator wilyLed = null; + for (WilyWorks.Config.LEDIndicator led: WilyCore.config.ledIndicators) { + if (led.name.equals(deviceName)) { + wilyLed = led; + } + } + if (wilyLed != null) { + x = wilyLed.x; + y = wilyLed.y; + isRed = wilyLed.isRed; + } else { + isRed = !(deviceName.toLowerCase().contains("green")); + } + } + + @Override + public void enable(boolean enableLed) { this.enable = enableLed; } + + @Override + public void enableLight(boolean enable) { enable(enable); } + + @Override + public boolean isLightOn() { + return enable; + } +} + diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyNormalizedColorSensor.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyNormalizedColorSensor.java new file mode 100644 index 000000000000..f37d08b78294 --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyNormalizedColorSensor.java @@ -0,0 +1,31 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.DistanceSensor; +import com.qualcomm.robotcore.hardware.NormalizedColorSensor; +import com.qualcomm.robotcore.hardware.NormalizedRGBA; + +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; + +/** + * Wily Works normalized color sensor implementation. + */ +public class WilyNormalizedColorSensor extends WilyHardwareDevice implements NormalizedColorSensor, DistanceSensor { + @Override + public NormalizedRGBA getNormalizedColors() { + return new NormalizedRGBA(); + } + + @Override + public float getGain() { + return 0; + } + + @Override + public void setGain(float newGain) { + } + + @Override + public double getDistance(DistanceUnit unit) { + return 0; + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyOpMode.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyOpMode.java index f6fc6abe1994..650fd54f8440 100644 --- a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyOpMode.java +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyOpMode.java @@ -19,6 +19,10 @@ public final void idle() { Thread.yield(); } + public final boolean opModeInInit() { + return !isStarted() && !isStopRequested(); + } + public void waitForStart() { WilyCore.render(); while (!isStarted()) diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyServo.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyServo.java new file mode 100644 index 000000000000..44022174de2c --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyServo.java @@ -0,0 +1,46 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.Servo; +import com.qualcomm.robotcore.hardware.ServoController; + +/** + * Wily Works Servo implementation. + */ +public class WilyServo extends WilyHardwareDevice implements Servo { + double position; + + @Override + public ServoController getController() { + return new WilyServoController(); + } + + @Override + public int getPortNumber() { + return 0; + } + + @Override + public void setDirection(Direction direction) { + + } + + @Override + public Direction getDirection() { + return null; + } + + @Override + public void setPosition(double position) { + this.position = Math.max(0, Math.min(1, position)); + } + + @Override + public double getPosition() { + return position; + } + + @Override + public void scaleRange(double min, double max) { + + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyServoController.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyServoController.java new file mode 100644 index 000000000000..b6ee70013097 --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyServoController.java @@ -0,0 +1,34 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.ServoController; + +/** + * Wily Works ServoController implementation. + */ +public class WilyServoController extends WilyHardwareDevice implements ServoController { + @Override + public void pwmEnable() { + } + + @Override + public void pwmDisable() { + } + + @Override + public PwmStatus getPwmStatus() { + return PwmStatus.DISABLED; + } + + @Override + public void setServoPosition(int servo, double position) { + } + + @Override + public double getServoPosition(int servo) { + return 0; + } + + @Override + public void close() { + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyTelemetry.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyTelemetry.java index 6ef7048ef057..b5aac6ef48f8 100644 --- a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyTelemetry.java +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyTelemetry.java @@ -1,18 +1,17 @@ package com.wilyworks.simulator.framework; +import static org.firstinspires.ftc.robotcore.external.Telemetry.DisplayFormat; import static java.awt.Font.MONOSPACED; import static java.awt.font.TextAttribute.POSTURE_REGULAR; import static java.awt.font.TextAttribute.WEIGHT_REGULAR; -import org.firstinspires.ftc.robotcore.external.Func; -import org.firstinspires.ftc.robotcore.external.Telemetry; - -import static org.firstinspires.ftc.robotcore.external.Telemetry.DisplayFormat; - import androidx.annotation.Nullable; import com.qualcomm.robotcore.robocol.TelemetryMessage; +import org.firstinspires.ftc.robotcore.external.Func; +import org.firstinspires.ftc.robotcore.external.Telemetry; + import java.awt.Canvas; import java.awt.Color; import java.awt.Graphics2D; @@ -517,12 +516,66 @@ public Telemetry.Item addData(String caption, String format, Func valuePr } } +class WilyLog implements Telemetry.Log { + LinkedList lines = new LinkedList<>(); + DisplayOrder displayOrder = DisplayOrder.OLDEST_FIRST; + int capacity = 9; + + @Override + public int getCapacity() { + return capacity; + } + + @Override + public void setCapacity(int capacity) { + this.capacity = capacity; + } + + @Override + public DisplayOrder getDisplayOrder() { + return displayOrder; + } + + @Override + public void setDisplayOrder(DisplayOrder displayOrder) { + this.displayOrder = displayOrder; + } + + @Override + public void add(String entry) { + lines.add(entry); + if (lines.size() > capacity) + lines.removeFirst(); + } + + @Override + public void add(String format, Object... args) { + add(String.format(format, args)); + } + + @Override + public void clear() { + lines.clear(); + } + + // This is a private function called by Telemetry.update() to add all of the lines + // of the log to the telemetry: + public void get(List telemetry) { + if (displayOrder == DisplayOrder.OLDEST_FIRST) { + telemetry.addAll(lines); + } else { + Iterator iterator = lines.descendingIterator(); + while (iterator.hasNext()) { + telemetry.add(iterator.next()); + } + } + } +} + /** * This class implements a lightweight emulation of FTC Telemetry that can run on the PC. */ public class WilyTelemetry implements Telemetry { - final int MAX_LINES = 36; // It's 18 with a regular sized font - // Global state: public static WilyTelemetry instance; @@ -532,6 +585,7 @@ public class WilyTelemetry implements Telemetry { ArrayList lineList = new ArrayList<>(); DisplayFormat displayFormat = DisplayFormat.CLASSIC; // HTML vs. monospace modes Layout layout = new Layout(); + WilyLog log = new WilyLog(); // Wily Works constructor for a Telemetry object: public WilyTelemetry(java.awt.Image icon) { @@ -544,15 +598,13 @@ public WilyTelemetry(java.awt.Image icon) { } public Line addLine(String string) { - if (lineList.size() <= MAX_LINES) { - int newLineIndex; - while ((newLineIndex = string.indexOf("\n")) != -1) { - String line = string.substring(0, newLineIndex); - lineList.add(line); - string = string.substring(newLineIndex + 1); - } - lineList.add(string); + int newLineIndex; + while ((newLineIndex = string.indexOf("\n")) != -1) { + String line = string.substring(0, newLineIndex); + lineList.add(line); + string = string.substring(newLineIndex + 1); } + lineList.add(string); return new WilyLine(this); } @@ -600,11 +652,12 @@ public void setDisplayFormat(DisplayFormat displayFormat) { @Override public Log log() { - return null; + return log; } public Item addData(String caption, Object value) { - addLine(String.format("%s : %s", caption, value.toString())); + String string = (value == null) ? "null" : value.toString(); + addLine(String.format("%s : %s", caption, string)); return null; // ### } @@ -651,6 +704,7 @@ public void speak(String text, String languageCode, String countryCode) { } public boolean update() { Graphics2D g = (Graphics2D) canvas.getBufferStrategy().getDrawGraphics(); g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); + log.get(lineList); // Append the log to the line list layout.parseAndRender(g, displayFormat, lineList); g.dispose(); diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyVoltageSensor.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyVoltageSensor.java new file mode 100644 index 000000000000..9d846f19b514 --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyVoltageSensor.java @@ -0,0 +1,18 @@ +package com.wilyworks.simulator.framework; + +import com.qualcomm.robotcore.hardware.VoltageSensor; + +/** + * Wily Works voltage sensor implementation. + */ +public class WilyVoltageSensor extends WilyHardwareDevice implements VoltageSensor { + @Override + public double getVoltage() { + return 13.0; + } + + @Override + public String getDeviceName() { + return "Voltage Sensor"; + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyWebcam.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyWebcam.java new file mode 100644 index 000000000000..8aa242fa397b --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/WilyWebcam.java @@ -0,0 +1,86 @@ +package com.wilyworks.simulator.framework; + +import android.content.Context; + +import androidx.annotation.Nullable; + +import com.qualcomm.robotcore.util.SerialNumber; +import com.wilyworks.common.WilyWorks; +import com.wilyworks.simulator.WilyCore; + +import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraCharacteristics; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.internal.system.Deadline; + +import java.util.function.Consumer; + +import kotlin.coroutines.Continuation; + +/** + * Wily Works named webcam implementation. + */ +public class WilyWebcam extends WilyHardwareDevice implements WebcamName { + WilyWorks.Config.Camera wilyCamera; + + WilyWebcam(String deviceName) { + for (WilyWorks.Config.Camera camera : WilyCore.config.cameras) { + if (camera.name.equals(deviceName)) { + wilyCamera = camera; + } + } + if (wilyCamera == null) { + System.out.printf("WilyWorks: Couldn't find configuration data for camera '%s'", deviceName); + } + } + + @Override + public boolean isWebcam() { + return true; + } + + @Override + public boolean isCameraDirection() { + return false; + } + + @Override + public boolean isSwitchable() { + return false; + } + + @Override + public boolean isUnknown() { + return false; + } + + @Override + public void asyncRequestCameraPermission(Context context, Deadline deadline, Continuation> continuation) { + + } + + @Override + public boolean requestCameraPermission(Deadline deadline) { + return false; + } + + @Override + public CameraCharacteristics getCameraCharacteristics() { + return null; + } + + @Override + public SerialNumber getSerialNumber() { + return null; + } + + @Nullable + @Override + public String getUsbDeviceNameIfAttached() { + return null; + } + + @Override + public boolean isAttached() { + return false; + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/DrumAnalogInput.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/DrumAnalogInput.java new file mode 100644 index 000000000000..6947273e92da --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/DrumAnalogInput.java @@ -0,0 +1,19 @@ +package com.wilyworks.simulator.framework.mechsim; + +import com.wilyworks.simulator.framework.WilyAnalogInput; + +// Hooked class for measuring the position of the drum: +public class DrumAnalogInput extends WilyAnalogInput { + DecodeSlowBotMechSim mechSim; + + DrumAnalogInput(DecodeSlowBotMechSim mechSim) { + this.mechSim = mechSim; + } + + // Return a voltage that is proportional to the drum location, with some variation: + @Override + public double getVoltage() { + double variation = -0.1 + Math.random() * 0.2; // random() generates numbers between 0 and 1 + return 3.5 * mechSim.actualDrumPosition + variation; + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/DrumColorSensor.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/DrumColorSensor.java new file mode 100644 index 000000000000..ec1a42bc95d9 --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/DrumColorSensor.java @@ -0,0 +1,63 @@ +package com.wilyworks.simulator.framework.mechsim; + +import com.qualcomm.robotcore.hardware.NormalizedRGBA; +import com.wilyworks.simulator.framework.WilyNormalizedColorSensor; + +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; + +import java.awt.Color; + +// Hooked class for determining the color of the ball once it's in the drum: +public class DrumColorSensor extends WilyNormalizedColorSensor { + DecodeSlowBotMechSim mechSim; + int idMask; // Sensor 0 or 1 + + DrumColorSensor(DecodeSlowBotMechSim mechSim, int index) { + this.mechSim = mechSim; + this.idMask = 1 << index; + } + + // Returns true if this sensor can read a ball; false if a hole in the ball is positioned + // over the sensor. + boolean sensorCanReadBall() { + // Every time we get a new ball, reset our variations: + if (mechSim.colorSensorMask == -1) { + mechSim.colorSensorMask = 1 + (int) (Math.random() * 3.0); // Mask = 1, 2 or 3 + } + return ((mechSim.colorSensorMask & idMask) != 0); + } + + @Override + public NormalizedRGBA getNormalizedColors() { + NormalizedRGBA normalizedColor = new NormalizedRGBA(); + + // Simulate the ball holes for some reads: + int rgbColor = 0; + // Figure out what slot is being input into, if any: + int slot = mechSim.findDrumSlot(mechSim.INTAKE_POSITIONS); + if (slot != -1) { + DecodeSlowBotMechSim.Ball ball = mechSim.slotBalls.get(slot); + if (ball != null) { + // There's ball over the sensors. See if they can be read: + if (sensorCanReadBall()) { + if (ball.color == DecodeSlowBotMechSim.BallColor.GREEN) { + rgbColor = android.graphics.Color.HSVToColor(new float[]{175, 1, 1}); + } else { + rgbColor = android.graphics.Color.HSVToColor(new float[]{210, 1, 1}); + } + } + } + } + normalizedColor.red = new Color(rgbColor).getRed() / 255.0f; + normalizedColor.green = new Color(rgbColor).getGreen() / 255.0f; + normalizedColor.blue = new Color(rgbColor).getBlue() / 255.0f; + return normalizedColor; + } + + @Override + public double getDistance(DistanceUnit unit) { + int slot = mechSim.findDrumSlot(mechSim.INTAKE_POSITIONS); + boolean ballPositionedForRead = (slot != -1) && mechSim.slotBalls.get(slot) != null; + return ballPositionedForRead && sensorCanReadBall() ? unit.fromMm(18) : unit.fromMm(70); + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/DrumDigitalChannel.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/DrumDigitalChannel.java new file mode 100644 index 000000000000..daabafd7594d --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/DrumDigitalChannel.java @@ -0,0 +1,18 @@ +package com.wilyworks.simulator.framework.mechsim; + +import com.wilyworks.simulator.framework.WilyDigitalChannel; + +public class DrumDigitalChannel extends WilyDigitalChannel { + DecodeSlowBotMechSim mechSim; + int index; + + DrumDigitalChannel(DecodeSlowBotMechSim mechSim, int index) { + this.mechSim = mechSim; + this.index = index; + } + + @Override + public boolean getState() { + return super.getState(); + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/LaunchMotor.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/LaunchMotor.java new file mode 100644 index 000000000000..c1fc627bb3cc --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/LaunchMotor.java @@ -0,0 +1,19 @@ +package com.wilyworks.simulator.framework.mechsim; + +import com.wilyworks.simulator.framework.WilyDcMotorEx; + +// Let us ramp up the launcher motor velocity. +public class LaunchMotor extends WilyDcMotorEx { + double targetVelocity; + double actualVelocity; + + @Override + public void setVelocity(double angularRate) { + targetVelocity = angularRate; + } + + @Override + public double getVelocity() { + return actualVelocity; + } +} diff --git a/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/MechSim.java b/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/MechSim.java new file mode 100644 index 000000000000..fd9568b61f80 --- /dev/null +++ b/WilyCore/src/main/java/com/wilyworks/simulator/framework/mechsim/MechSim.java @@ -0,0 +1,588 @@ +package com.wilyworks.simulator.framework.mechsim; + +import static com.wilyworks.simulator.WilyCore.time; + +import com.acmerobotics.roadrunner.Pose2d; +import com.qualcomm.robotcore.hardware.AnalogInput; +import com.qualcomm.robotcore.hardware.CRServo; +import com.qualcomm.robotcore.hardware.DcMotorEx; +import com.qualcomm.robotcore.hardware.DigitalChannel; +import com.qualcomm.robotcore.hardware.HardwareDevice; +import com.qualcomm.robotcore.hardware.HardwareMap; +import com.qualcomm.robotcore.hardware.LED; +import com.qualcomm.robotcore.hardware.NormalizedColorSensor; +import com.qualcomm.robotcore.hardware.Servo; +import com.wilyworks.simulator.WilyCore; +import com.wilyworks.simulator.framework.WilyLED; +import com.wilyworks.simulator.helpers.Point; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.geom.Arc2D; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +// This is the base class for game simulations for particular years and robots. +public class MechSim { + // Create the game simulation appropriate to this year. + static public MechSim create() { + return new DecodeSlowBotMechSim(); + } + + // Hook the creation of a particular device. + public HardwareDevice hookDevice(String deviceName, HardwareDevice original) { + return original; + } + + // Run the simulation and render the mechanisms. + public void update(Graphics2D g, Pose2d pose) { + renderRobotBox(g, pose); + renderLeds(g, pose); + } + + // Advance the simulation time. + public void advance(double deltaT) {} + + // Render the robot box. + protected void renderRobotBox(Graphics2D g, Pose2d pose) { + AffineTransform originalTransform = g.getTransform(); + g.translate(pose.position.x, pose.position.y); + g.rotate(pose.heading.log()); + g.setColor(Color.RED); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) 0.5)); + // Draw a rectangle for the true position: + g.draw(new Rectangle2D.Double( + -WilyCore.config.robotWidth / 2.0, -WilyCore.config.robotLength / 2.0, + WilyCore.config.robotWidth, WilyCore.config.robotLength)); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) 1.0)); + g.setTransform(originalTransform); + } + + // Render the LED for REV Digital LEDs. + protected void renderLeds(Graphics2D g, Pose2d pose) { + HardwareMap hardwareMap = WilyCore.hardwareMap; + if (hardwareMap == null) + return; // Might not have been created yet + + final int[] colors = { 0, 0xff0000, 0x00ff00, 0xffbf00 }; // black, red, green, amber + final double radius = 2.0; // Circle radius, in inches + + ArrayList ledArray = new ArrayList<>(); + for (LED led: hardwareMap.led) { + ledArray.add((WilyLED) led); + } + for (int i = 0; i < ledArray.size(); i++) { + WilyLED led = ledArray.get(i); + int colorIndex = 0; + colorIndex |= (led.isRed && led.enable) ? 1 : 0; + colorIndex |= (!led.isRed && led.enable) ? 2 : 0; + + // The LED actually needs two digital channels to describe all 4 possible colors. + // Assume that consecutively registered channels make a pair: + if (i + 1 < ledArray.size()) { + WilyLED nextLed = ledArray.get(i + 1); + if ((nextLed.x == led.x) && + (nextLed.y == led.y) && + (nextLed.isRed == !led.isRed)) { + + colorIndex |= (nextLed.isRed && nextLed.enable) ? 1 : 0; + colorIndex |= (!nextLed.isRed && nextLed.enable) ? 2 : 0; + i++; + } + } + + // Draw the circle at the location of the sensor on the robot, accounting for its + // current heading: + Point point = new Point(led.x, led.y) + .rotate(pose.heading.log()) + .add(new Point(pose.position)); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g.setColor(new Color(0xffffff)); + g.fill(new Ellipse2D.Double(point.x - radius - 0.5, point.y - radius - 0.5,2 * radius + 1, 2 * radius + 1)); + g.setColor(new Color(colors[colorIndex])); + g.fill(new Ellipse2D.Double(point.x - radius, point.y - radius,2 * radius, 2 * radius)); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + } + } + +} + +// Simulation for the SlowBot in the 2025/2026 Decode game. +class DecodeSlowBotMechSim extends MechSim { + enum BallColor {PURPLE, GREEN, GOLD} + + final double WIDTH = 18; // Robot width + final double HEIGHT = 18; // Robot height + final double BALL_SIZE = 5; // 5 inches in diameter + final double DRUM_SERVO_SPEED = 1.0 / 0.9; // Speed of the drum servo, position/s + final Point[] INTAKE_OFFSETS = { new Point(8, -1), new Point(8, +1) }; + final double INTAKE_EPSILON = 2.5; // Epsilon for intake distance + final double INTAKE_POWER = 0.1; // Minimum power for intake + final double[] INTAKE_POSITIONS = { 0.0/6, 2.0/6, 4.0/6 }; // AKA 'launch' positions + final double[] TRANSFER_POSITIONS = { 3.0/6, 5.0/6, 1.0/6 }; // Servo positions for intaking + final double SLOT_EPSILON = 0.02; // Epsilon for determining a slot relative to a [0, 1] range + final double MIN_TRANSFER_TIME = 0.020; // Second it takes for a transfer + final double MIN_TRANSFER_POSITION = 0.6; // Minimum position to start a transfer + final double TRANSFER_SERVO_SPEED = (60.0 / 360) / 0.25; // Speed of a goBilda torque servo, position/s + final double LAUNCH_SPEED = 144; // Ball launch speed, inches per second + final Point LAUNCH_OFFSET = new Point(-4, 0); + final double FEEDER_POWER = 0.1; // Minimum power for the feeder servo + final double GOAL_EPSILON = 12; // Distance from goal center to consider a goal + final Point[] GOAL_CENTERS = { new Point(-72, 72), new Point(-72, -72) }; + final Point[] CLASSIFIER_STARTS = { new Point(-BALL_SIZE/2, 72-BALL_SIZE/2), + new Point(-BALL_SIZE/2, -72-BALL_SIZE/2) }; // Start locations, corresponding to goals + final double LAUNCH_ACCELERATION = 1000; // Increase flywheel speed by this many ticks per second + final double LAUNCH_DROP = 500; // Drop flywheel speed by this many ticks on launch + final double LAUNCH_EPSILON = 50; // Target and actual flywheel velocities must be within this amount + final double INTAKE_ERROR_PROBABILITY = 0.2; // Probability that a ball is missed on intake + + // Struct for tracking ball locations: + static class Ball { + BallColor color; // GREEN or PURPLE + Point point; // Current location; not relevant if in robot mechanisms + Point velocity; // Velocity vector; null for balls lying on the field + public Ball(BallColor color, double x, double y) { + this.color = color; + this.point = new Point(x, y); + } + } + + // These preset balls are mirrored in y the constructor: + Ball[] ballPresets = { + new Ball(BallColor.GREEN, -12, 53), + new Ball(BallColor.PURPLE, -12, 48), + new Ball(BallColor.PURPLE, -12, 43), + new Ball(BallColor.PURPLE, 12, 53), + new Ball(BallColor.GREEN, 12, 48), + new Ball(BallColor.PURPLE, 12, 43), + new Ball(BallColor.PURPLE, 36, 53), + new Ball(BallColor.PURPLE, 36, 48), + new Ball(BallColor.GREEN, 36, 43), + new Ball(BallColor.PURPLE, 69.5, 69.5), + new Ball(BallColor.GREEN, 64.5, 69.5), + new Ball(BallColor.PURPLE, 59.5, 69.5) + }; + Ball[] ballPreloads = { + new Ball(BallColor.GOLD, 0, 0), + new Ball(BallColor.GOLD, 0, 0), + new Ball(BallColor.GOLD, 0, 0) + }; + + // Hooked devices: + LaunchMotor upperLaunchMotor; + LaunchMotor lowerLaunchMotor; + AnalogInput analogDrum; + NormalizedColorSensor sensorColor1; + NormalizedColorSensor sensorColor2; + DigitalChannel digitalChannel1; + DigitalChannel digitalChannel2; + DcMotorEx intakeMotor; + Servo drumServo; + Servo transferServo; + CRServo forwardFeederServo; + CRServo backwardFeederServo; + + // State: + double accumulatedDeltaT; + Ball intakeBall; // Ball in the intake, may be null + List slotBalls = new ArrayList<>(Collections.nCopies(3, null)); + List airBalls = new LinkedList<>(); // Balls flying through the air + List fieldBalls = new LinkedList<>(); // Pickable balls on the field + List> classifierBalls = IntStream.range(0, 2) + .mapToObj(i -> new LinkedList()).collect(Collectors.toList()); + double actualDrumPosition; // Current location of the drum, [0, 1] + double actualTransferPosition; // Current transfer servo position, [0, 1] + double transferStartTime; // Time that a transfer started, zero when not transferring + int colorSensorMask = -1; // Random 2-bit mask indicating which sensors return true data; -1 if reset + boolean hasIntaken = false; // True once any ball has been taken into the drum + boolean hasLaunched = false; // True once any ball has been launched + + // Initialize the beast. + DecodeSlowBotMechSim() { + // Add the presets to the field, along with the mirrored-in y versions: + for (Ball ball: ballPresets) { + fieldBalls.add(ball); + fieldBalls.add(new Ball(ball.color, ball.point.x, -ball.point.y)); + } + } + + // Compute the draw color from the ball object. + private Color ballColor(Ball ball) { + if (ball == null) + return Color.BLACK; + else if (ball.color == BallColor.PURPLE) + return new Color(128, 0, 128); + else if (ball.color == BallColor.GREEN) + return Color.GREEN; + else + return new Color(255, 215, 0); // Gold + } + + // WilyHardwareMap calls this method when it creates a device, allowing us to substitute + // with a different device object. + @Override + public HardwareDevice hookDevice(String name, HardwareDevice device) { + // These are input-only devices: + if (name.equals("motIntake")) { + intakeMotor = (DcMotorEx) device; + } + if (name.equals("servoDrum")) { + drumServo = (Servo) device; + } + if (name.equals("servoTransfer")) { + transferServo = (Servo) device; + } + if (name.equals("servoFLaunchFeeder")) { + forwardFeederServo = (CRServo) device; + } + if (name.equals("servoBLaunchFeeder")) { + backwardFeederServo = (CRServo) device; + } + + // There have outputs: + if (name.equals("motULauncher")) { + device = upperLaunchMotor = new LaunchMotor(); + } + if (name.equals("motLLauncher")) { + device = lowerLaunchMotor = new LaunchMotor(); + } + if (name.equals("analogDrum")) { + device = analogDrum = new DrumAnalogInput(this); + } + if (name.equals("sensorColor1")) { + device = sensorColor1 = new DrumColorSensor(this, 0); + } + if (name.equals("sensorColor2")) { + device = sensorColor2 = new DrumColorSensor(this, 1); + } + if (name.equals("breakBeam1")) { + device = digitalChannel1 = new DrumDigitalChannel(this, 1); + } + if (name.equals("breakBeam2")) { + device = digitalChannel2 = new DrumDigitalChannel(this, 2); + } + return device; + } + + // Check to see if the caller created all of the expected state. + void verifyState() { + if (upperLaunchMotor == null) + throw new RuntimeException("Missing upper launch motor"); + if (lowerLaunchMotor == null) + throw new RuntimeException("Missing lower launch motor"); + if (intakeMotor == null) + throw new RuntimeException("Missing intake motor"); + if (drumServo == null) + throw new RuntimeException("Missing drum servo"); + if (transferServo == null) + throw new RuntimeException("Missing transfer servo"); + if (forwardFeederServo == null) + throw new RuntimeException("Missing forward feeder servo"); + if (backwardFeederServo == null) + throw new RuntimeException("Missing backward feeder servo"); + if (analogDrum == null) + throw new RuntimeException("Missing analog drum"); + if (sensorColor1 == null) + throw new RuntimeException("Missing color sensor 1"); + if (sensorColor2 == null) + throw new RuntimeException("Missing color sensor 2"); + } + + void render(Graphics2D g, Pose2d pose) { + // Draw the balls on the field: + for (Ball ball : fieldBalls) { + g.setColor(ballColor(ball)); + g.fill(ballEllipse(ball.point.x, ball.point.y)); + } + + // Draw the classifier balls: + for (int i = 0; i < 2; i++) { + double x = CLASSIFIER_STARTS[i].x; + double y = CLASSIFIER_STARTS[i].y; + int count = 0; + for (Ball ball: classifierBalls.get(i)) { + g.setColor(ballColor(ball)); + g.fill(ballEllipse(x, y)); + x -= BALL_SIZE; + count++; + if (count >= 9) + break; // Classifier maxes out at 9 balls, more causes an overflow + } + } + + AffineTransform fieldTransform = g.getTransform(); + + // Set the transform to draw the robots in the canonical space, from (-1, -1) to (1, 1): + g.translate(pose.position.x, pose.position.y); + g.rotate(pose.heading.log()); + g.scale(WIDTH / 2, HEIGHT / 2); + + // Draw the robot outline: + g.setStroke(new BasicStroke(0.1f)); + g.setColor(Color.DARK_GRAY); + g.fill(new Rectangle2D.Double(-1, -1, 2, 2)); + + // Draw the intake wheels: + Color intakeColor = Color.GRAY; + if (intakeMotor.getPower() > 0) { + intakeColor = Color.GREEN; // Green when intaking + } else if (intakeMotor.getPower() < 0) { + intakeColor = Color.RED; // Red when spitting out + } + g.setColor(intakeColor); + final int INTAKE_WHEEL_COUNT = 4; + final double INTAKE_HEIGHT = 0.2; + final double INTAKE_SPACE = (1.6 - INTAKE_WHEEL_COUNT * INTAKE_HEIGHT) / (INTAKE_WHEEL_COUNT - 1); + for (int i = 1; i < 3; i++) { // Just draw the middle two wheels + double y = -0.8 + i * (INTAKE_HEIGHT + INTAKE_SPACE); + g.fill(new Rectangle2D.Double(0.6, y, 0.4, INTAKE_HEIGHT)); + } + + // Draw the drum: + g.setColor(Color.GRAY); + g.translate(-0.33, 0); + g.rotate(-2 * Math.PI * actualDrumPosition); // Note the negative + g.fill(new Ellipse2D.Double(-0.8, -0.8, 1.6, 1.6)); + + // Draw the dead wedge area: + g.setColor(Color.DARK_GRAY); + g.fill(new Arc2D.Double(-0.8, -0.8, 1.6, 1.6, 0.0f, -60, Arc2D.PIE)); + + // Draw each of the drum slot circles: + for (Ball ball: slotBalls) { + g.setColor(ballColor(ball)); + g.fill(new Ellipse2D.Double(0.1, -0.25, 0.6, 0.6)); + g.rotate(Math.toRadians(120)); + } + + // Draw the air balls: + g.setTransform(fieldTransform); + for (Ball ball : airBalls) { + g.setColor(ballColor(ball)); + g.fill(ballEllipse(ball.point.x, ball.point.y)); + } + + // Draw the intake ball, if there is one: + if (intakeBall != null) { + g.translate(pose.position.x, pose.position.y); + g.rotate(pose.heading.log()); + g.setColor(ballColor(intakeBall)); + g.fill(ballEllipse(7, 0)); + } + } + + // Shortcut to create an ellipse representing a ball, centered around the specified point. + Ellipse2D ballEllipse(double x, double y) { + return new Ellipse2D.Double(x - BALL_SIZE/2, y - BALL_SIZE/2, BALL_SIZE, BALL_SIZE); + } + + + // Find the slot that corresponds to the current drum angle. Returns -1 if not found. + int findDrumSlot(double[] slotPositions) { + for (int i = 0; i < 3; i++) { + if (Math.abs(actualDrumPosition - slotPositions[i]) < SLOT_EPSILON) { + return i; + } + } + return -1; + } + + // Advance the simulation: + void simulate(Pose2d pose, double deltaT) { + verifyState(); + + double heading = pose.heading.log(); + // Advance the balls flying through the air: + Iterator ballIterator = airBalls.iterator(); + while (ballIterator.hasNext()) { + Ball ball = ballIterator.next(); + // Move the ball: + ball.point = ball.point.add(ball.velocity.multiply(deltaT)); + + // See if it's scored a goal: + for (int i = 0; i < 2; i++) { // Goal index + if (Math.hypot(GOAL_CENTERS[i].x - ball.point.x, + GOAL_CENTERS[i].y - ball.point.y) < GOAL_EPSILON) { + ballIterator.remove(); + classifierBalls.get(i).add(ball); + } + } + } + + // Ramp up the launcher motors velocities: + double upperDiff = upperLaunchMotor.targetVelocity - upperLaunchMotor.actualVelocity; + double lowerDiff = lowerLaunchMotor.targetVelocity - lowerLaunchMotor.actualVelocity; + upperLaunchMotor.actualVelocity += Math.signum(upperDiff) * Math.min(Math.abs(upperDiff), deltaT * LAUNCH_ACCELERATION); + lowerLaunchMotor.actualVelocity += Math.signum(lowerDiff) * Math.min(Math.abs(lowerDiff), deltaT * LAUNCH_ACCELERATION); + + // Move the transfer servo towards the target angle: + double targetTransferPosition = transferServo.getPosition(); + double transferDiff = targetTransferPosition - actualTransferPosition; + actualTransferPosition += Math.signum(transferDiff) * Math.min(Math.abs(transferDiff), deltaT * TRANSFER_SERVO_SPEED); + + // Move the drum towards the target angle: + double targetDrumPosition = drumServo.getPosition(); + double drumDiff = targetDrumPosition - actualDrumPosition; + actualDrumPosition += Math.signum(drumDiff) * Math.min(Math.abs(drumDiff), deltaT * DRUM_SERVO_SPEED); + + // Reset the color sensor mask whenever the drum moves: + if (targetDrumPosition != actualDrumPosition) { + colorSensorMask = -1; + } + + // Find the slot if in position to transfer: + int transferSlot = findDrumSlot(TRANSFER_POSITIONS); + + // Handle load requests for the launch calibration app, signaled by running the launchers + // backwards: + if (upperLaunchMotor.getVelocity() < 0) { + if (forwardFeederServo.getPower() >= 0) { + throw new RuntimeException("When running launch motors backwards, forward feeder servo must too."); + } + if (backwardFeederServo.getPower() >= 0) { + throw new RuntimeException("When running launch motors backwards, backward feeder servo must too."); + } + // If the slot is empty, fill it up with a ball! + if ((transferSlot != -1) && (slotBalls.get(transferSlot) == null)) { + slotBalls.set(transferSlot, new Ball(BallColor.PURPLE, 0, 0)); + } + } + + // Handle transfer requests: + if (actualTransferPosition <= MIN_TRANSFER_POSITION) { + if (transferStartTime != 0) { + throw new RuntimeException("Didn't transfer for sufficient time."); + } + } else { + if (transferSlot == -1) { + throw new RuntimeException("A transfer is requested when drum isn't in the right spot. That will break things!"); + } + if (targetDrumPosition != actualDrumPosition) { + throw new RuntimeException("The drum is moving during a transfer. That will break things!"); + } + if (upperLaunchMotor.getVelocity() <= 0) { + throw new RuntimeException("A transfer is requested when upper launcher motor isn't running forward. That won't work!"); + } + if (lowerLaunchMotor.getVelocity() <= 0) { + throw new RuntimeException("A transfer is requested when lower launcher motor isn't running forward. That won't work!"); + } + if (forwardFeederServo.getPower() < FEEDER_POWER) { + throw new RuntimeException("A transfer is requested when forward feeder servo isn't running. That won't work!"); + } + if (Math.abs(backwardFeederServo.getPower()) < FEEDER_POWER) { + throw new RuntimeException("A transfer is requested when backward feeder servo isn't running. That won't work!"); + } + if (slotBalls.get(transferSlot) == null) { + if (!hasIntaken && !hasLaunched) { + // A transfer has been requested there was no intake done. Assume that this + // is Auto with preloads and populate the drum with three golden balls. We + // make them gold balls so that we don't have to worry about getting the + // ordering correct. + slotBalls = new ArrayList<>(Arrays.asList(ballPreloads)); + } + } + if (slotBalls.get(transferSlot) != null) { + if (transferStartTime == 0) { + transferStartTime = time(); + + // Check these only at the start of the transfer because we're going to + // immediately drop the speeds: + if (Math.abs(upperLaunchMotor.targetVelocity - upperLaunchMotor.actualVelocity) > LAUNCH_EPSILON) { + throw new RuntimeException("A transfer is requested when upper launcher motor isn't running. That won't work!"); + } + if (Math.abs(lowerLaunchMotor.targetVelocity - lowerLaunchMotor.actualVelocity) > LAUNCH_EPSILON) { + throw new RuntimeException("A transfer is requested when lower launcher motor isn't running. That won't work!"); + } + + upperLaunchMotor.actualVelocity -= LAUNCH_DROP; + lowerLaunchMotor.actualVelocity -= LAUNCH_DROP; + } else if (time() - transferStartTime > MIN_TRANSFER_TIME) { + // Transfer the ball from the drum to the air-balls list: + Ball ball = slotBalls.get(transferSlot); + ball.velocity = new Point(LAUNCH_SPEED, 0).rotate(heading); + ball.point = new Point(pose.position).add(LAUNCH_OFFSET.rotate(heading)); + airBalls.add(ball); + slotBalls.set(transferSlot, null); + transferStartTime = 0; + hasLaunched = true; + } + } + } + + // Finally, check for intake: + if (intakeMotor.getPower() > INTAKE_POWER) { + if (intakeBall == null) { + // We're intaking from the field into the intake: + for (Point intakeOffset : INTAKE_OFFSETS) { + Point intakePoint = new Point(pose.position).add(intakeOffset.rotate(pose.heading.log())); + for (Ball ball : fieldBalls) { + double distance = Math.hypot(ball.point.x - intakePoint.x, ball.point.y - intakePoint.y); + if (distance < INTAKE_EPSILON) { + // Remove the ball from the field. I think this is okay so long as we + // terminate the loop... + fieldBalls.remove(ball); + + // Move the ball into an intake position, unless random error says + // to toss it: + if ((!WilyCore.enableSensorError) || (Math.random() > INTAKE_ERROR_PROBABILITY)) { + intakeBall = ball; + } + break; + } + } + } + } else { + // We're intaking from the intake into a drum slot: + int slot = findDrumSlot(INTAKE_POSITIONS); + if (slot != -1) { + // Accumulate the empty slots: + LinkedList emptySlots = new LinkedList<>(); + for (int i = 0; i < 3; i++) { + if (slotBalls.get(i) == null) { + emptySlots.add(i); + } + } + // Assign the ball to an empty slot: + if (!emptySlots.isEmpty()) { + int emptySlot = emptySlots.get((int) (Math.random() * emptySlots.size())); + slotBalls.set(emptySlot, intakeBall); + intakeBall = null; + hasIntaken = true; + } + } + } + } + } + + // Advance the Mech simulation. + @Override + public void advance(double deltaT) { + // Note that we don't update our simulation until update() time, when we also know the + // new robot pose. + this.accumulatedDeltaT += deltaT; + } + + // Render the robot and the balls. + @Override + public void update(Graphics2D g, Pose2d pose) { + // Don't call simulate() or render() for things like Configuration Tester. + if (upperLaunchMotor != null) { + simulate(pose, accumulatedDeltaT); + render(g, pose); + } + accumulatedDeltaT = 0; + } +} diff --git a/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/AngleUnit.java b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/AngleUnit.java index 0e1d55ab76ee..3a266370f70c 100644 --- a/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/AngleUnit.java +++ b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/AngleUnit.java @@ -223,4 +223,14 @@ public static float normalizeRadians(float radians) { return (float)normalizeRadians((double)radians); } + + public UnnormalizedAngleUnit getUnnormalized() + { + switch (this) + { + default: + case RADIANS: return UnnormalizedAngleUnit.RADIANS; + case DEGREES: return UnnormalizedAngleUnit.DEGREES; + } + } } diff --git a/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/Pose3D.java b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/Pose3D.java new file mode 100644 index 000000000000..ad88ccbabc06 --- /dev/null +++ b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/Pose3D.java @@ -0,0 +1,89 @@ +package org.firstinspires.ftc.robotcore.external.navigation; + +/* + * Copyright (c) 2024 Dryw Wade + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of FIRST nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import androidx.annotation.NonNull; + +import org.firstinspires.ftc.robotcore.external.navigation.Position; +import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; + +import java.util.Locale; + +/** + * Pose3D represents the position and orientation of an object in 3D space. + */ +public class Pose3D +{ + protected final Position position; + protected final YawPitchRollAngles orientation; + + public Pose3D(Position position, YawPitchRollAngles orientation) + { + this.position = position; + this.orientation = orientation; + } + + //---------------------------------------------------------------------------------------------- + // Formatting + //---------------------------------------------------------------------------------------------- + + @NonNull + @Override public String toString() + { + return String.format(Locale.getDefault(), + "position=%s, orientation=%s", + position.toString(), + orientation.toString()); + } + + /** + * A 3D orientation. + * + * The axis mapping is defined by the code that creates objects from this class. One should not assume that + * pitch, for example, is along the x axis. Consult the documentation of the API that returns + * a Pose3D for its axis mapping. + */ + public YawPitchRollAngles getOrientation() + { + return orientation; + } + + /** + * A 3D position. + */ + public Position getPosition() + { + return position; + } +} diff --git a/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/Position.java b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/Position.java new file mode 100644 index 000000000000..feb87ba7c310 --- /dev/null +++ b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/Position.java @@ -0,0 +1,100 @@ +package org.firstinspires.ftc.robotcore.external.navigation; + +/* +Copyright (c) 2016 Robert Atkinson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +import java.util.Locale; + +/** + * Instances of {@link Position} represent a three-dimensional distance in a particular distance unit. + * + * @see Position + */ +public class Position +{ + //---------------------------------------------------------------------------------------------- + // State + //---------------------------------------------------------------------------------------------- + + public DistanceUnit unit; + + public double x; + public double y; + public double z; + + /** + * the time on the System.nanoTime() clock at which the data was acquired. If no + * timestamp is associated with this particular set of data, this value is zero. + */ + public long acquisitionTime; + + //---------------------------------------------------------------------------------------------- + // Construction + //---------------------------------------------------------------------------------------------- + + public Position() + { + this(DistanceUnit.MM, 0, 0, 0, 0); + } + + public Position(DistanceUnit unit, double x, double y, double z, long acquisitionTime) + { + this.unit = unit; + this.x = x; + this.y = y; + this.z = z; + this.acquisitionTime = acquisitionTime; + } + + public Position toUnit(DistanceUnit distanceUnit) + { + if (distanceUnit != this.unit) + { + return new Position(distanceUnit, + distanceUnit.fromUnit(this.unit, x), + distanceUnit.fromUnit(this.unit, y), + distanceUnit.fromUnit(this.unit, z), + this.acquisitionTime); + } + else + return this; + } + + //---------------------------------------------------------------------------------------------- + // Formatting + //---------------------------------------------------------------------------------------------- + + @Override public String toString() + { + return String.format(Locale.getDefault(), "(%.3f %.3f %.3f)%s", x, y, z, unit.toString()); + } +} diff --git a/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/UnnormalizedAngleUnit.java b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/UnnormalizedAngleUnit.java new file mode 100644 index 000000000000..724c144de3c7 --- /dev/null +++ b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/UnnormalizedAngleUnit.java @@ -0,0 +1,198 @@ +package org.firstinspires.ftc.robotcore.external.navigation; + +/* +Copyright (c) 2016 Robert Atkinson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * An {@link UnnormalizedAngleUnit} represents angles in different units of measure and + * provides utility methods to convert across units. {@link UnnormalizedAngleUnit} does not + * maintain angle information internally, but only helps organize + * and use angle measures that may be maintained separately across various contexts. + *

+ * Angles can be distinguished along (at least) two axes: + * Normalized angles are of most utility when dealing with physical angles, as normalization + * removes ambiguity of representation. In particular, two angles can be compared for equality + * by subtracting them, normalizing, and testing whether the absolute value of the result is + * smaller than some tolerance threshold. This approach neatly handles all cases of cyclical + * wrapping without unexpected discontinuities. + *

+ *

+ * Unnormalized angles can be handy when the angular quantity is not a physical angle but some + * related quantity such as an angular velocity or acceleration, where the + * quantity in question lacks the 360-degree cyclical equivalence of a physical angle. + *

+ *

+ * {@link AngleUnit} expresses normalized angles, while {@link UnnormalizedAngleUnit} expresses unnormalized ones + *

+ */ +@SuppressWarnings("WeakerAccess") +public enum UnnormalizedAngleUnit +{ + DEGREES(0), RADIANS(1); + public final byte bVal; + + UnnormalizedAngleUnit(int i) + { + bVal = (byte) i; + } + + //---------------------------------------------------------------------------------------------- + // Primitive operations + //---------------------------------------------------------------------------------------------- + + public double fromDegrees(double degrees) + { + switch (this) + { + default: + case RADIANS: return (degrees / 180.0 * Math.PI); + case DEGREES: return (degrees); + } + } + + public float fromDegrees(float degrees) + { + switch (this) + { + default: + case RADIANS: return (degrees / 180.0f * AngleUnit.Pif); + case DEGREES: return (degrees); + } + } + + public double fromRadians(double radians) + { + switch (this) + { + default: + case RADIANS: return (radians); + case DEGREES: return (radians / Math.PI * 180.0); + } + } + + public float fromRadians(float radians) + { + switch (this) + { + default: + case RADIANS: return (radians); + case DEGREES: return (radians / AngleUnit.Pif * 180.0f); + } + } + + public double fromUnit(UnnormalizedAngleUnit them, double theirs) + { + switch (them) + { + default: + case RADIANS: return this.fromRadians(theirs); + case DEGREES: return this.fromDegrees(theirs); + } + } + + public float fromUnit(UnnormalizedAngleUnit them, float theirs) + { + switch (them) + { + default: + case RADIANS: return this.fromRadians(theirs); + case DEGREES: return this.fromDegrees(theirs); + } + } + + public double fromUnit(AngleUnit them, double theirs) { + return this.fromUnit(them.getUnnormalized(), theirs); + } + + public float fromUnit(AngleUnit them, float theirs) { + return this.fromUnit(them.getUnnormalized(), theirs); + } + + //---------------------------------------------------------------------------------------------- + // Derived operations + //---------------------------------------------------------------------------------------------- + + public double toDegrees(double inOurUnits) + { + switch (this) + { + default: + case RADIANS: return DEGREES.fromRadians(inOurUnits); + case DEGREES: return DEGREES.fromDegrees(inOurUnits); + } + } + + public float toDegrees(float inOurUnits) + { + switch (this) + { + default: + case RADIANS: return DEGREES.fromRadians(inOurUnits); + case DEGREES: return DEGREES.fromDegrees(inOurUnits); + } + } + + public double toRadians(double inOurUnits) + { + switch (this) + { + default: + case RADIANS: return RADIANS.fromRadians(inOurUnits); + case DEGREES: return RADIANS.fromDegrees(inOurUnits); + } + } + + public float toRadians(float inOurUnits) + { + switch (this) + { + default: + case RADIANS: return RADIANS.fromRadians(inOurUnits); + case DEGREES: return RADIANS.fromDegrees(inOurUnits); + } + } + + //---------------------------------------------------------------------------------------------- + // Normalization + //---------------------------------------------------------------------------------------------- + + public AngleUnit getNormalized() + { + switch (this) + { + default: + case RADIANS: return AngleUnit.RADIANS; + case DEGREES: return AngleUnit.DEGREES; + } + } +} diff --git a/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/internal/ui/GamepadUser.java b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/internal/ui/GamepadUser.java new file mode 100644 index 000000000000..e0c0438a17c6 --- /dev/null +++ b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/internal/ui/GamepadUser.java @@ -0,0 +1,17 @@ +package org.firstinspires.ftc.robotcore.internal.ui; + +/** A *typed* integer so that we can more easily keep track of things */ +public enum GamepadUser +{ + ONE(1), TWO(2); + + public byte id; + GamepadUser(int id) { this.id = (byte)id; } + + public static GamepadUser from(int user) + { + if (user==1) return ONE; + if (user==2) return TWO; + return null; + } +} diff --git a/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/internal/usb/EthernetOverUsbSerialNumber.java b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/internal/usb/EthernetOverUsbSerialNumber.java new file mode 100644 index 000000000000..c5ac2f84592c --- /dev/null +++ b/WilyCore/src/main/java/org/firstinspires/ftc/robotcore/internal/usb/EthernetOverUsbSerialNumber.java @@ -0,0 +1,99 @@ +package org.firstinspires.ftc.robotcore.internal.usb; + +/* +Copyright (c) 2024 FIRST + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of FIRST nor the names of their contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +import com.qualcomm.robotcore.util.SerialNumber; + +@SuppressWarnings("WeakerAccess") +public class EthernetOverUsbSerialNumber extends SerialNumber +{ + protected final String ipAddress; + protected final String iface; + private final static String SERIAL_NUMBER_PREFIX = "EthernetOverUsb"; + + //---------------------------------------------------------------------------------------------- + // Construction + //---------------------------------------------------------------------------------------------- + + public static EthernetOverUsbSerialNumber fromIpAddress(String ipAddress, String iface) + { + String serialNumber = SERIAL_NUMBER_PREFIX + ":" + iface + ":" + ipAddress; + return new EthernetOverUsbSerialNumber(serialNumber); + } + + public static EthernetOverUsbSerialNumber fromSerialNumber(String serialNumber) + { + return new EthernetOverUsbSerialNumber(serialNumber); + } + + private EthernetOverUsbSerialNumber(String serialNumber) + { + super(serialNumber.trim()); + this.iface = serialNumber.substring(serialNumber.indexOf(':') + 1, serialNumber.lastIndexOf(':')); + this.ipAddress = serialNumber.substring(serialNumber.lastIndexOf(':') + 1); + } + + //---------------------------------------------------------------------------------------------- + // Accessing + //---------------------------------------------------------------------------------------------- + + public String getIpAddress() + { + return ipAddress; + } + + @Override + public String toString() + { + return iface + ':' + ipAddress; + } + + @Override + public boolean isFake() + { + return false; + } + + /** + * Returns whether the indicated serial number initialization string is one of the legacy + * fake serial number forms or not. + * + * @param initializer the serial number initialization string to test + * @return whether the serial number initialization string is a legacy fake form of serial number + */ + public static boolean isLegacyFake(String initializer) + { + return false; + } + +} diff --git a/WilyCore/src/main/java/org/firstinspires/ftc/teamcode/wilyworks/WilyTelemetry.java b/WilyCore/src/main/java/org/firstinspires/ftc/teamcode/wilyworks/WilyTelemetry.java deleted file mode 100644 index cbb121228e26..000000000000 --- a/WilyCore/src/main/java/org/firstinspires/ftc/teamcode/wilyworks/WilyTelemetry.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.firstinspires.ftc.teamcode.wilyworks; - -import org.firstinspires.ftc.robotcore.external.Func; -import org.firstinspires.ftc.robotcore.external.Telemetry; - -/** - * This is the Wily Works implementation of the standard FTC Telemetry object. - */ -public class WilyTelemetry implements Telemetry { - @Override - public Item addData(String caption, String format, Object... args) { - return null; - } - - @Override - public Item addData(String caption, Object value) { - return null; - } - - @Override - public Item addData(String caption, Func valueProducer) { - return null; - } - - @Override - public Item addData(String caption, String format, Func valueProducer) { - return null; - } - - @Override - public boolean removeItem(Item item) { - return false; - } - - @Override - public void clear() { } - - @Override - public void clearAll() { } - - @Override - public Object addAction(Runnable action) { - return null; - } - - @Override - public boolean removeAction(Object token) { - return false; - } - - @Override - public void speak(String text) { } - - @Override - public void speak(String text, String languageCode, String countryCode) { } - - @Override - public boolean update() { - return false; - } - - @Override - public Line addLine() { - return null; - } - - @Override - public Line addLine(String lineCaption) { - return null; - } - - @Override - public boolean removeLine(Line line) { - return false; - } - - @Override - public boolean isAutoClear() { - return false; - } - - @Override - public void setAutoClear(boolean autoClear) { } - - @Override - public int getMsTransmissionInterval() { - return 0; - } - - @Override - public void setMsTransmissionInterval(int msTransmissionInterval) { } - - @Override - public String getItemSeparator() { - return null; - } - - @Override - public void setItemSeparator(String itemSeparator) { } - - @Override - public String getCaptionValueSeparator() { - return null; - } - - @Override - public void setCaptionValueSeparator(String captionValueSeparator) { } - - @Override - public void setDisplayFormat(DisplayFormat displayFormat) { } - - @Override - public Log log() { - return null; - } -} diff --git a/WilyCore/src/main/resources/background/season-2025-decode/field-2025-juice-dark.png b/WilyCore/src/main/resources/background/season-2025-decode/field-2025-juice-dark.png new file mode 100644 index 000000000000..f0a3fb3f0afc Binary files /dev/null and b/WilyCore/src/main/resources/background/season-2025-decode/field-2025-juice-dark.png differ diff --git a/WilyWorks/src/main/java/com/wilyworks/common/WilyWorks.java b/WilyWorks/src/main/java/com/wilyworks/common/WilyWorks.java index 2988d8f3e235..43394ddc6738 100644 --- a/WilyWorks/src/main/java/com/wilyworks/common/WilyWorks.java +++ b/WilyWorks/src/main/java/com/wilyworks/common/WilyWorks.java @@ -277,7 +277,8 @@ static public Twist2dDual