diff --git a/coreAPI/pom.xml b/coreAPI/pom.xml index 4b252665..c57dc7b0 100644 --- a/coreAPI/pom.xml +++ b/coreAPI/pom.xml @@ -5,7 +5,7 @@ net.java.jinput jinput-parent - 2.0.14v + 2.0.15v coreapi @@ -16,11 +16,56 @@ + org.apache.maven.plugins maven-source-plugin + org.apache.maven.plugins maven-javadoc-plugin - \ No newline at end of file + + + + jitpack.io + https://jitpack.io + + + + + + + org.junit + junit-bom + 5.10.0 + pom + import + + + + + + + com.github.umjammer + vavi-commons + 1.1.9 + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.platform + junit-platform-commons + test + + + diff --git a/coreAPI/src/main/java/net/java/games/input/AbstractController.java b/coreAPI/src/main/java/net/java/games/input/AbstractController.java index 7e63f520..6e13a64c 100644 --- a/coreAPI/src/main/java/net/java/games/input/AbstractController.java +++ b/coreAPI/src/main/java/net/java/games/input/AbstractController.java @@ -210,13 +210,13 @@ protected void fireOnInput(InputEvent event) { listeners.forEach(l -> l.onInput(event)); } - /** */ + /** data structure to output */ public interface Report { - /** */ + /** set value for each rumbler from this report class fields */ void cascadeTo(Rumbler[] rumblers); } - /** */ + /** do output */ public abstract void output(Report report) throws IOException; } diff --git a/coreAPI/src/main/java/net/java/games/input/Component.java b/coreAPI/src/main/java/net/java/games/input/Component.java index 93511052..8b6a3b2e 100644 --- a/coreAPI/src/main/java/net/java/games/input/Component.java +++ b/coreAPI/src/main/java/net/java/games/input/Component.java @@ -895,31 +895,9 @@ public String getName() { } /** - * KeyIDs for standard PC (LATIN-1) keyboards + * for write */ - enum Output implements Identifier { - SMALL_RUMBLE("smallRumble"), - BIG_RUMBLE("bigRumble"), - LED_RED("ledRed"), - LED_GREEN("ledGreen"), - LED_BLUE("ledBlue"), - FLASH_LED1("flashLed1"), - FLASH_LED2("flashLed2"); - - final String name; - - @Override - public String getName() { - return name; - } - - /** - * Protected constructor - */ - Output(String name) { - this.name = name; - } - } + interface Output extends Identifier {} } /** diff --git a/coreAPI/src/main/java/net/java/games/input/WrappedComponent.java b/coreAPI/src/main/java/net/java/games/input/WrappedComponent.java new file mode 100644 index 00000000..865c7090 --- /dev/null +++ b/coreAPI/src/main/java/net/java/games/input/WrappedComponent.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 by Naohide Sano, All rights reserved. + * + * Programmed by Naohide Sano + */ + +package net.java.games.input; + + +/** + * WrappedComponent. + * + * @author Naohide Sano (nsano) + * @version 0.00 2024-01-24 nsano initial version
+ */ +public interface WrappedComponent { + + /** */ + T getWrappedObject(); +} diff --git a/coreAPI/src/main/java/net/java/games/input/plugin/DualShock4PluginBase.java b/coreAPI/src/main/java/net/java/games/input/plugin/DualShock4PluginBase.java new file mode 100644 index 00000000..9898dbc3 --- /dev/null +++ b/coreAPI/src/main/java/net/java/games/input/plugin/DualShock4PluginBase.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2024 by Naohide Sano, All rights reserved. + * + * Programmed by Naohide Sano + */ + +package net.java.games.input.plugin; + +import java.io.PrintStream; +import java.util.Collection; +import java.util.Collections; + +import net.java.games.input.Component; +import net.java.games.input.Controller; +import net.java.games.input.DeviceSupportPlugin; +import net.java.games.input.usb.HidController; +import net.java.games.input.usb.HidRumbler; +import vavi.util.ByteUtil; + + +/** + * DualShock4PluginBase. + * + * @author Naohide Sano (nsano) + * @version 0.00 2024-01-28 nsano initial version
+ */ +public abstract class DualShock4PluginBase implements DeviceSupportPlugin { + + // TODO should have report id??? + public enum DualShock4Output implements Component.Identifier.Output { + SMALL_RUMBLE("smallRumble"), + BIG_RUMBLE("bigRumble"), + LED_RED("ledRed"), + LED_GREEN("ledGreen"), + LED_BLUE("ledBlue"), + FLASH_LED1("flashLed1"), + FLASH_LED2("flashLed2"); + + final String name; + + @Override + public String getName() { + return name; + } + + /** + * Protected constructor + */ + DualShock4Output(String name) { + this.name = name; + } + } + + /** TODO for bluetooth */ + protected static final int BASE_OFFSET = 0; + + /** Represents report id 5 data */ + public static class Report5 extends HidController.HidReport { + private final byte[] data = new byte[31]; + + public int smallRumble; + public int bigRumble; + public int ledRed; + public int ledGreen; + public int ledBlue; + public int flashLed1; + public int flashLed2; + + public Report5() { + data[0] = (byte) 255; + } + + @Override public int getReportId() { + return 5; + } + + @Override public byte[] getData() { + return data; + } + + @Override protected void cascadeTo(HidRumbler rumbler) { + float value = switch ((DualShock4Output) rumbler.getOutputIdentifier()) { + case SMALL_RUMBLE -> smallRumble; + case BIG_RUMBLE -> bigRumble; + case LED_RED -> ledRed; + case LED_GREEN -> ledGreen; + case LED_BLUE -> ledBlue; + case FLASH_LED1 -> flashLed1; + case FLASH_LED2 -> flashLed2; + }; + rumbler.setValue(value); + } + } + + /** + * @param data includes the first byte (report id) + * @see "https://www.psdevwiki.com/ps4/DS4-USB" + */ + public static void display(byte[] data, PrintStream pr) { + int l3x = data[1] & 0xff; + int l3y = data[2] & 0xff; + int r3x = data[3] & 0xff; + int r3y = data[4] & 0xff; + + boolean tri = (data[5] & 0x80) != 0; + boolean cir = (data[5] & 0x40) != 0; + boolean x = (data[5] & 0x20) != 0; + boolean sqr = (data[5] & 0x10) != 0; + int dPad = data[5] & 0x0f; + + enum Hat { + N("↑"), NE("↗"), E("→"), SE("↘"), S("↓"), SW("↙"), W("←"), NW("↖"), Released(" "); final String s; Hat(String s) { this.s = s; } + } + + boolean r3 = (data[6] & 0x80) != 0; + boolean l3 = (data[6] & 0x40) != 0; + boolean opt = (data[6] & 0x20) != 0; + boolean share = (data[6] & 0x10) != 0; + boolean r2 = (data[6] & 0x08) != 0; + boolean l2 = (data[6] & 0x04) != 0; + boolean r1 = (data[6] & 0x02) != 0; + boolean l1 = (data[6] & 0x01) != 0; + + int counter = (data[7] >> 2) & 0x3f; + boolean tPad = (data[7] & 0x02) != 0; + boolean ps = (data[7] & 0x01) != 0; + + int lTrigger = data[8] & 0xff; + int rTrigger = data[9] & 0xff; + + int timeStump = ByteUtil.readLeShort(data, 10) & 0xffff; + int batteryLevel = data[12] & 0xff; + + int gyroX = ByteUtil.readLeShort(data, 13) & 0xffff; + int gyroY = ByteUtil.readLeShort(data, 15) & 0xffff; + int gyroZ = ByteUtil.readLeShort(data, 17) & 0xffff; + + int accelX = ByteUtil.readLeShort(data, 19) & 0xffff; + int accelY = ByteUtil.readLeShort(data, 21) & 0xffff; + int accelZ = ByteUtil.readLeShort(data, 23) & 0xffff; + + boolean headphone = (data[30] & 0x40) != 0; + boolean mic = (data[30] & 0x20) != 0; + + int touch1X = (data[36] & 0xff) | ((data[37] & 0x0f) << 8); + int touch1Y = ((data[38] & 0xff) << 4) | (data[37] & 0xf); + + int touch2X = (data[40] & 0xff) | ((data[41] & 0x0f) << 8); + int touch2Y = ((data[42] & 0xff) << 4) | (data[41] & 0xf); + + pr.printf("L3 x: %02x%n", l3x); + pr.printf("L3 y: %02x%n", l3y); + pr.printf("R3 x: %02x%n", r3x); + pr.printf("R3 y: %02x%n", r3y); + pr.printf("hat: %s%n", Hat.values()[dPad].s); + + pr.printf("counter: %d%n", counter); + + pr.printf("▲: %s%n", tri ? "●" : "◯"); + pr.printf("●: %s%n", cir ? "●" : "◯"); + pr.printf("✖: %s%n", x ? "●" : "◯"); + pr.printf("■: %s%n", sqr ? "●" : "◯"); + + pr.printf("L1: %s%n", l1 ? "●" : "◯"); + pr.printf("R1: %s%n", r1 ? "●" : "◯"); + pr.printf("L2: %s%n", l2 ? "●" : "◯"); + pr.printf("R2: %s%n", r2 ? "●" : "◯"); + pr.printf("Share: %s%n", share ? "●" : "◯"); + pr.printf("Opt: %s%n", opt ? "●" : "◯"); + pr.printf("L3: %s%n", l3 ? "●" : "◯"); + pr.printf("R3: %s%n", r3 ? "●" : "◯"); + + pr.printf("T-PAD: %s%n", tPad ? "●" : "◯"); + pr.printf("PS: %s%n", ps ? "●" : "◯"); + + pr.printf("lTrigger: %02x%n", lTrigger); + pr.printf("rTrigger: %02x%n", rTrigger); + + pr.printf("timeStump: %04x%n", timeStump); + pr.printf("batteryLevel: %d%n", batteryLevel); + + pr.printf("\uD83C\uDFA7: %s%n", headphone ? "●" : "◯"); + pr.printf("🎤: %s%n", mic ? "●" : "◯"); + + pr.printf("gyro x: %04x%n", gyroX); + pr.printf("gyro y: %04x%n", gyroY); + pr.printf("gyro z: %04x%n", gyroZ); + + pr.printf("accel x: %04x%n", accelX); + pr.printf("accel y: %04x%n", accelY); + pr.printf("accel z: %04x%n", accelZ); + + pr.printf("touch1 x: %d%n", touch1X); + pr.printf("touch1 y: %d%n", touch1Y); + + pr.printf("touch2 x: %d%n", touch2X); + pr.printf("touch2 y: %d%n", touch2Y); + pr.println(); + } +} diff --git a/coreAPI/src/main/java/net/java/games/input/usb/HidController.java b/coreAPI/src/main/java/net/java/games/input/usb/HidController.java index 2ba5e90c..38472d04 100644 --- a/coreAPI/src/main/java/net/java/games/input/usb/HidController.java +++ b/coreAPI/src/main/java/net/java/games/input/usb/HidController.java @@ -8,6 +8,7 @@ import net.java.games.input.AbstractController; import net.java.games.input.Controller; +import net.java.games.input.Rumbler; /** @@ -18,19 +19,50 @@ */ public interface HidController extends Controller { - /** */ + /** hid product id */ int getProductId(); - /** */ + /** hid vender id */ int getVendorId(); - /** */ - interface HidReport extends AbstractController.Report { + /** data structure to report for a hid device */ + abstract class HidReport implements AbstractController.Report { - /** */ - int getReportId(); + /** hid report id */ + public abstract int getReportId(); - /** */ - byte[] getData(); + /** data bytes to write */ + public abstract byte[] getData(); + + @Override + public void cascadeTo(Rumbler[] rumblers) { + for (Rumbler rumbler : rumblers) { + if (rumbler instanceof HidRumbler hidRumbler) { + if (getReportId() == hidRumbler.getReportId()) { + cascadeTo(hidRumbler); + } + } + } + } + + /** set value from class field to each rumbler */ + protected abstract void cascadeTo(HidRumbler rumbler); + + /** pack rumbler value into bytes */ + private void pack(Rumbler[] rumblers) { + for (Rumbler rumbler : rumblers) { + if (rumbler instanceof HidRumbler hidRumbler) { + if (hidRumbler.getReportId() == getReportId()) { + hidRumbler.fill(getData()); + } + } + } + } + + /** class fields -> rumblers -> bytes */ + public void setup(Rumbler[] rumblers) { + this.cascadeTo(rumblers); + this.pack(rumblers); + } } } diff --git a/coreAPI/src/main/java/net/java/games/input/usb/HidControllerEnvironment.java b/coreAPI/src/main/java/net/java/games/input/usb/HidControllerEnvironment.java new file mode 100644 index 00000000..ddde06ac --- /dev/null +++ b/coreAPI/src/main/java/net/java/games/input/usb/HidControllerEnvironment.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 by Naohide Sano, All rights reserved. + * + * Programmed by Naohide Sano + */ + +package net.java.games.input.usb; + + +/** + * HidControllerEnvironment. + * + * @author Naohide Sano (nsano) + * @version 0.00 2024-01-24 nsano initial version
+ */ +public interface HidControllerEnvironment { + + /** + * throw NoSuchElementException when there is no matched device of mid and pid. + */ + HidController getController(int mid, int pid); +} diff --git a/coreAPI/src/main/java/net/java/games/input/usb/HidInputEvent.java b/coreAPI/src/main/java/net/java/games/input/usb/HidInputEvent.java new file mode 100644 index 00000000..59b280f6 --- /dev/null +++ b/coreAPI/src/main/java/net/java/games/input/usb/HidInputEvent.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 by Naohide Sano, All rights reserved. + * + * Programmed by Naohide Sano + */ + +package net.java.games.input.usb; + + +/** + * HidInputEvent. + * + * @author Naohide Sano (nsano) + * @version 0.00 2024-01-26 nsano initial version
+ */ +public interface HidInputEvent { + + /** for debug */ + byte[] getData(); +} diff --git a/coreAPI/src/main/java/net/java/games/input/usb/HidRumbler.java b/coreAPI/src/main/java/net/java/games/input/usb/HidRumbler.java index 47abfe28..19acef64 100644 --- a/coreAPI/src/main/java/net/java/games/input/usb/HidRumbler.java +++ b/coreAPI/src/main/java/net/java/games/input/usb/HidRumbler.java @@ -17,6 +17,9 @@ */ public interface HidRumbler extends Rumbler { + /** @since 2.0.15v */ + int getReportId(); + /** */ void fill(byte[] data); } diff --git a/coreAPI/src/main/java/net/java/games/input/usb/parser/Collection.java b/coreAPI/src/main/java/net/java/games/input/usb/parser/Collection.java new file mode 100644 index 00000000..c259d5d0 --- /dev/null +++ b/coreAPI/src/main/java/net/java/games/input/usb/parser/Collection.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2014, Kustaa Nyholm / SpareTimeLabs + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted 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 the Kustaa Nyholm or SpareTimeLabs nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * 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 HOLDER 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. + */ + +package net.java.games.input.usb.parser; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import net.java.games.input.usb.UsagePage; + + +/** + * Represents a collection of {@link Field}. + * + * @see "https://github.com/nyholku/purejavahidapi" + */ +public final class Collection { + + private final Collection parent; + private final LinkedList children; + private final LinkedList fields; + private final int usagePair; + private final int type; + + public Collection getParent() { + return parent; + } + + public LinkedList getChildren() { + return children; + } + + public LinkedList getFields() { + return fields; + } + + public int getUsagePair() { + return usagePair; + } + + public int getUsagePage() { + return (usagePair >> 16) & 0xffff; + } + + public int getUsage() { + return usagePair & 0xffff; + } + + public int getType() { + return type; + } + + public enum Type { + Physical, + Application, + Logical, + Report, + NamedArray, + UsageSwitch, + UsageModifier, + // 0x07 - 0x7F + ReservedForFutureUse, + // 0x80 - 0xFF + VendorDefined; + static Type valueOf(int type) { + if (type < 0x07) { + return values()[type]; + } else if (type <= 0x7F) { + return ReservedForFutureUse; + } else if (type <= 0xff) { + return VendorDefined; + } else { + throw new IllegalArgumentException(String.valueOf(type)); + } + } + } + + Collection(Collection parent, int usagePair, int type) { + this.parent = parent; + this.usagePair = usagePair; + this.type = type; + children = new LinkedList<>(); + if (parent != null) + parent.children.add(this); + fields = new LinkedList<>(); + } + + void reset() { + children.clear(); + } + + void add(Field field) { + fields.add(field); + } + + void dump(PrintStream out, String tab) { + if (parent != null) { + UsagePage usagePage_ = UsagePage.map(getUsagePage()); + out.printf(tab + "collection type %s(%d) usage 0x%04X:0x%04X %s:%s%n", Type.valueOf(type), type, getUsagePage(), getUsage(), usagePage_ == null ? "" : usagePage_, usagePage_ == null ? "" : usagePage_.mapUsage(getUsage())); + tab += " "; + } + for (Collection c : children) { + c.dump(out, tab); + } + for (Field f : fields) { + f.dump(out, tab); + } + } + + public List enumerateFields() { + List result = new ArrayList<>(); + for (Collection c : children) { + result.addAll(c.enumerateFields()); + } + result.addAll(fields); + return result; + } + + @Override + public String toString() { + return "Collection{" + + "parent=" + parent + + ", children=" + children.size() + + ", fields=" + fields.size() + + ", usage=" + usagePair + + ", type=" + Type.valueOf(type) + + '}'; + } +} \ No newline at end of file diff --git a/coreAPI/src/main/java/net/java/games/input/usb/parser/Field.java b/coreAPI/src/main/java/net/java/games/input/usb/parser/Field.java new file mode 100644 index 00000000..d3e8e771 --- /dev/null +++ b/coreAPI/src/main/java/net/java/games/input/usb/parser/Field.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2014, Kustaa Nyholm / SpareTimeLabs + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted 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 the Kustaa Nyholm or SpareTimeLabs nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * 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 HOLDER 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. + */ + +package net.java.games.input.usb.parser; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.logging.Level; + +import net.java.games.input.usb.UsagePage; +import net.java.games.input.usb.parser.HidParser.Feature; +import vavi.util.Debug; +import vavi.util.StringUtil; + +import static net.java.games.input.usb.parser.HidParser.Feature.BUFFERED_BYTE; + + +/** + * Represents one field in a hid device descriptor. + *

+ * TODO hid value is little endian??? + * + * @see "https://github.com/nyholku/purejavahidapi" + */ +public final class Field { + + Report report; + Collection collection; + int physical; + int logical; + int application; + int usage; + /** @see Feature */ + int flags; + /** bits (first one byte (report id) is excluded) TODO really ??? */ + int reportOffset; + /** unit depends on BUFFERED_BYTE of flags [bytes/bits] */ + int reportSize; + int reportType; + int logicalMinimum; + int logicalMaximum; + int physicalMinimum; + int physicalMaximum; + int unitExponent; + int unit; + + int mask; + int offsetByte; + int startBit; + /** data size considered startBits shift */ + int dataBytes; + + public int getUsagePage() { + return (usage >> 16) & 0xffff; + } + + public int getUsageId() { + return usage & 0xffff; + } + + public int getFeature() { + return flags; + } + + public int getLogicalMinimum() { + return logicalMinimum; + } + + public int getLogicalMaximum() { + return logicalMaximum; + } + + public int getPhysicalMinimum() { + return physicalMinimum; + } + + public int getPhysicalMaximum() { + return physicalMaximum; + } + + /** for parser */ + Field(Collection collection) { + this.collection = collection; + collection.add(this); + } + + /** not good way (for performance) */ + void init() { + this.offsetByte = reportOffset / 8; + this.startBit = reportOffset % 8; + this.mask = createMask(); + this.dataBytes = isBytes() ? reportSize : (reportSize + startBit + 7) / 8; + if (dataBytes > 8) { + throw new IllegalArgumentException(String.format("bad descriptor: isBytes: %s, reportSize: %d, startBit: %d", isBytes(), reportSize, startBit)); + } + } + + /** + * for plugin TODO adhoc + * @param offset in bits (must be excluded first one byte (8 bits) for report id) + * @param size in bits + */ + public Field(int offset, int size) { + this.reportOffset = offset; + this.reportSize = size; + + init(); + } + + /** */ + boolean isBytes() { + return Feature.containsIn(BUFFERED_BYTE, flags); + } + + /** */ + int createMask() { + String x = new StringBuilder(toBit()).reverse().toString(); // MSB <- LSB +Debug.println(Level.FINER, x); + return Integer.parseInt(x, 2); + } + + /** LSB -> MSB */ + String toBit() { + return toBit("1", "0"); + } + + /** view LSB -> MSB */ + String toBit(String on, String off) { + int bits; + if (isBytes()) { + bits = reportSize * 8; + } else { + bits = reportSize; + } + return off.repeat(startBit) + + on.repeat(bits) + + off.repeat((startBit + bits) % 8 == 0 ? 0 : 8 - (startBit + bits) % 8); + } + + // int index; + void dump(PrintStream out, String tab) { + UsagePage usagePage = UsagePage.map(getUsagePage()); + out.printf(tab + "-FIELD-------------------------%n"); + out.printf(tab + " usage: 0x%04X:0x%04X %s:%s%n", getUsagePage(), getUsageId(), usagePage == null ? "" : usagePage, usagePage == null ? "" : usagePage.mapUsage(getUsageId())); + out.printf(tab + " flags: %s\n", Feature.asString(Feature.valueOf(flags))); + out.printf(tab + " report id: 0x%02X\n", report.id); + out.printf(tab + " type: %s\n", new String[] {"input", "output", "feature"}[report.type]); + out.printf(tab + " offset: %d byte%s (%d)\n", offsetByte, startBit != 0 ? String.format(" and %d bit", startBit) : "", reportOffset); + out.printf(tab + " size: %d: %s\n", reportSize, isBytes() ? reportSize + " bytes" : toBit("*", "_")); + out.printf(tab + " logical min: %d\n", logicalMinimum); + out.printf(tab + " logical max: %d\n", logicalMaximum); + out.printf(tab + " physical min: %d\n", physicalMinimum); + out.printf(tab + " physical max: %d\n", physicalMaximum); + out.printf(tab + " unit: %d\n", unit); + out.printf(tab + " unit exp: %d\n", unitExponent); + } + + /** TODO when length + startBits > 64bit */ + private int getValueInternal(byte[] data) { + int value = 0; + + int p = offsetByte + 1; // + 1 for the report id at the first byte + + switch (dataBytes) { + case 8: value |= (data[p + 7]) << 56; // fall-through + case 7: value |= (data[p + 6]) << 48; // fall-through + case 6: value |= (data[p + 5]) << 40; // fall-through + case 5: value |= (data[p + 4]) << 32; // fall-through + case 4: value |= (data[p + 3]) << 24; // fall-through + case 3: value |= (data[p + 2]) << 16; // fall-through + case 2: value |= (data[p + 1]) << 8; // fall-through + case 1: value |= data[p + 0]; + } + + return value; + } + + /** TODO when length + startBits > 64bit */ + private void setValueInternal(byte[] data, int value) { + int p = offsetByte + 1; // + 1 for the report id at the first byte + + switch (dataBytes) { + case 8: data[p + 7] = (byte) ((value >> 56) & 0xff); // fall-through + case 7: data[p + 6] = (byte) ((value >> 48) & 0xff); // fall-through + case 6: data[p + 5] = (byte) ((value >> 40) & 0xff); // fall-through + case 5: data[p + 4] = (byte) ((value >> 32) & 0xff); // fall-through + case 4: data[p + 3] = (byte) ((value >> 24) & 0xff); // fall-through + case 3: data[p + 2] = (byte) ((value >> 16) & 0xff); // fall-through + case 2: data[p + 1] = (byte) ((value >> 8 ) & 0xff); // fall-through + case 1: data[p + 0] = (byte) ( value & 0xff); // fall-through + } + } + + /** utility */ + public int getValue(byte[] data) { +Debug.printf(Level.FINER, "masked: 0x%02x, %s, moved: 0x%02x, %s", getValueInternal(data) & mask, StringUtil.toBits(getValueInternal(data) & mask), (getValueInternal(data) & mask) >> startBit, StringUtil.toBits((getValueInternal(data) & mask) >> startBit)); + return (getValueInternal(data) & mask) >> startBit; + } + + /** utility */ + public void setValue(byte[] data, int v) { + setValueInternal(data, ((getValueInternal(data) & ~mask) | ((v << startBit) & mask))); + } + + @Override + public String toString() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + dump(new PrintStream(baos), ""); + return baos.toString(); + } + + /** */ + public String getDump(byte[] data) { + return StringUtil.getDump(data, offsetByte + 1, dataBytes); + } +} diff --git a/coreAPI/src/main/java/net/java/games/input/usb/parser/HidParser.java b/coreAPI/src/main/java/net/java/games/input/usb/parser/HidParser.java new file mode 100644 index 00000000..cbeed766 --- /dev/null +++ b/coreAPI/src/main/java/net/java/games/input/usb/parser/HidParser.java @@ -0,0 +1,591 @@ +/* + * Copyright (c) 2014, Kustaa Nyholm / SpareTimeLabs + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted 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 the Kustaa Nyholm or SpareTimeLabs nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * 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 HOLDER 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. + */ + +package net.java.games.input.usb.parser; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Deque; +import java.util.EnumSet; +import java.util.LinkedList; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + + +/** + * Mandatory items for REPORT + *

+ * Input (Output or Feature)
+ * Usage
+ *  Usage Page
+ *  Logical Minimum
+ *  Logical Maximum
+ *  Report Size
+ *  Report Count
+ * 
+ * + * @see "http://msdn.microsoft.com/en-us/library/windows/hardware/hh975383.aspx" + * @see "https://github.com/nyholku/purejavahidapi" + */ +public class HidParser { + + private static final Logger logger = Logger.getLogger(HidParser.class.getName()); + + private static final Collection rootCollection = new Collection(null, 0, 0xff); + private Collection topCollection; + private int collectionUsage; // TODO adhoc + private final Deque globalStack = new LinkedList<>(); + private int delimiterDepth; + private int parseIndex; + private byte[] descriptor; + private int descriptorLength; + private Local local; + private Global global; + private LinkedList reports; + + public final static int HID_MAX_FIELDS = 256; + + private final static int HID_MAX_IDS = 256; + private final static int HID_MAX_APPLICATIONS = 16; + private final static int HID_MAX_USAGES = 12288; + + private static final int HID_INPUT_REPORT = 0; + private static final int HID_OUTPUT_REPORT = 1; + private static final int HID_FEATURE_REPORT = 2; + + private static final int HID_COLLECTION_PHYSICAL = 0; + private static final int HID_COLLECTION_APPLICATION = 1; + private static final int HID_COLLECTION_LOGICAL = 2; + + public interface Tag { + void parse(HidParser context, Item item); + } + + public enum ItemType { // order import, do not change + MAIN { @Override Tag valueOf(int tag) { return MainTag.valueOf(tag); }}, + GLOBAL { @Override Tag valueOf(int tag) { return GlobalTag.valueOf(tag); }}, + LOCAL { @Override Tag valueOf(int tag) { return LocalTag.valueOf(tag); }}, + RESERVED { @Override Tag valueOf(int tag) { throw new UnsupportedOperationException(); }}, + LONG { @Override Tag valueOf(int tag) { throw new UnsupportedOperationException(); }}; + abstract Tag valueOf(int tag); + } + + public enum MainTag implements Tag { // order import, do not change + PADDING_0 { @Override public void parse(HidParser context, Item item) {}}, + PADDING_1 { @Override public void parse(HidParser context, Item item) {}}, + PADDING_2 { @Override public void parse(HidParser context, Item item) {}}, + PADDING_3 { @Override public void parse(HidParser context, Item item) {}}, + PADDING_4 { @Override public void parse(HidParser context, Item item) {}}, + PADDING_5 { @Override public void parse(HidParser context, Item item) {}}, + PADDING_6 { @Override public void parse(HidParser context, Item item) {}}, + PADDING_7 { @Override public void parse(HidParser context, Item item) {}}, + INPUT { + @Override public void parse(HidParser context, Item item) { + context.addField(HID_INPUT_REPORT, item.uValue); + context.local.reset(); + } + }, + OUTPUT { + @Override public void parse(HidParser context, Item item) { + context.addField(HID_OUTPUT_REPORT, item.uValue); + context.local.reset(); + } + }, + COLLECTION { + @Override public void parse(HidParser context, Item item) { + context.topCollection = new Collection(context.topCollection, context.collectionUsage, item.uValue & 3); + } + }, + FEATURE { + @Override public void parse(HidParser context, Item item) { + context.addField(HID_FEATURE_REPORT, item.uValue); + context.local.reset(); + } + }, + ENDCOLLECTION { + @Override public void parse(HidParser context, Item item) { + if (context.topCollection.getParent() == null) + throw new IllegalStateException("collection stack underflow"); + context.topCollection = context.topCollection.getParent(); + context.local.reset(); + } + }; + static Tag valueOf(int tag) { + if (tag < 8 || tag >= values().length) + throw new IllegalStateException(String.format("illegal/unsupported main tag %d", tag)); + return values()[tag]; + } + } + + public enum LocalTag implements Tag { // order import, do not change + USAGE { + @Override public void parse(HidParser context, Item item) { + if (item.size == 0) + throw new IllegalStateException("item data expected for local item"); + + int usage = item.uValue; + if (item.size <= 2) { // FIXME is this in the spec? + usage = (context.global.usagePage << 16) + usage; +logger.finer(String.format("item.size <= 2: %08x", usage)); + } + + if (context.topCollection == rootCollection) { // TODO adhoc + // rootCollection usage ignored + context.collectionUsage = usage; +logger.finer(String.format("topCollection is rootCollection: %08x", usage)); + return; + } + if (context.local.delimiterBranch > 1) { + // alternative usage ignored +logger.finer(String.format("context.local.delimiterBranch > 1: %08x", usage)); + return; + } + context.addUsage(usage); + } + }, + USAGE_MINIMUM { + @Override public void parse(HidParser context, Item item) { +logger.finer("USAGE_MAXIMUM: " + item.uValue + ", " + context.local.delimiterBranch); + context.local.usageMinimum = item.uValue; + } + }, + USAGE_MAXIMUM { + @Override public void parse(HidParser context, Item item) { +logger.finer("USAGE_MAXIMUM: " + item.uValue + ", " + context.local.delimiterBranch); + for (int n = context.local.usageMinimum; n <= item.uValue; n++) { +logger.finer("USAGE_MAXIMUM: " + n); + context.addUsage(context.global.usagePage << 16 | n); + } + } + }, + DESIGNATOR_INDEX { @Override public void parse(HidParser context, Item item) {}}, + DESIGNATOR_MINIMUM { @Override public void parse(HidParser context, Item item) {}}, + DESIGNATOR_MAXIMUM { @Override public void parse(HidParser context, Item item) {}}, + STRING_INDEX { @Override public void parse(HidParser context, Item item) {}}, + STRING_MINIMUM { @Override public void parse(HidParser context, Item item) {}}, + STRING_MAXIMUM { @Override public void parse(HidParser context, Item item) {}}, + DELIMITER { + @Override public void parse(HidParser context, Item item) { + if (item.uValue > 0) { + if (context.local.delimiterDepth != 0) + throw new IllegalStateException("nested delimiters"); + context.local.delimiterDepth++; + context.local.delimiterBranch++; + } else { + if (context.local.delimiterDepth < 1) + throw new IllegalStateException("extra delimiters"); + context.local.delimiterDepth--; + } + } + }; + static Tag valueOf(int tag) { + if (tag < 0 || tag >= values().length) + throw new IllegalStateException(String.format("illegal/unsupported local tag %d", tag)); + return values()[tag]; + } + } + + public enum GlobalTag implements Tag { + USAGE_PAGE { + @Override public void parse(HidParser context, Item item) { + context.global.usagePage = item.uValue; + } + }, + LOGICAL_MINIMUM { + @Override public void parse(HidParser context, Item item) { + context.global.logicalMinimum = item.sValue; + } + }, + LOGICAL_MAXIMUM { + @Override public void parse(HidParser context, Item item) { + context.global.logicalMaximum = item.sValue; + } + }, + PHYSICAL_MINIMUM { + @Override public void parse(HidParser context, Item item) { + context.global.physicalMinimum = item.sValue; + } + }, + PHYSICAL_MAXIMUM { + @Override public void parse(HidParser context, Item item) { + context.global.physicalMaximum = item.sValue; +logger.finer("global.physicalMaximum " + context.global.physicalMaximum); + } + }, + UNIT_EXPONENT { + @Override public void parse(HidParser context, Item item) { + context.global.unitExponent = item.sValue; + } + }, + UNIT { + @Override public void parse(HidParser context, Item item) { + context.global.unit = item.uValue; + } + }, + REPORT_SIZE { + @Override public void parse(HidParser context, Item item) { + if (item.uValue < 0 || item.uValue > 32) + throw new IllegalStateException(String.format("invalid report size %d", item.uValue)); + context.global.reportSize = item.uValue; + } + }, + REPORT_ID { + @Override public void parse(HidParser context, Item item) { + if (item.uValue == 0) + throw new IllegalStateException("report_id 0 is invalid"); + context.global.reportId = item.uValue; + } + }, + REPORT_COUNT { + @Override public void parse(HidParser context, Item item) { + if (item.uValue < 0 || item.uValue > HID_MAX_USAGES) + throw new IllegalStateException(String.format("invalid report count %d", item.uValue)); + context.global.reportCount = item.uValue; + } + }, + PUSH { + @Override public void parse(HidParser context, Item item) { + context.globalStack.push((Global) context.global.clone()); + } + }, + POP { + @Override public void parse(HidParser context, Item item) { + if (context.globalStack.isEmpty()) + throw new IllegalStateException("global environment stack underflow"); + context.global = context.globalStack.pop(); + } + }; + static Tag valueOf(int tag) { + if (tag < 0 || tag >= values().length) + throw new IllegalStateException(String.format("illegal/unsupported global tag %d", tag)); + return values()[tag]; + } + } + + public enum Feature { + CONSTANT("Constant", "Data"), + VARIABLE("Variable", "Array"), + RELATIVE("Relative", "Absolute"), + WRAP("Wrap", "No Wrap"), + NONLINEAR("Non Linear", "Linear"), + NO_PREFERRED("No Preferred State", "Preferred State"), + NULL_STATE("Null State", "No Null Position"), + VOLATILE("Volatile", "Non Volatile"), + BUFFERED_BYTE("Buffered Bytes", "Bitfield"); + final int mask; + final String on; + final String off; + Feature(String on, String off) { + this.on = on; + this.off = off; + this.mask = 0x001 << ordinal(); + } + static EnumSet valueOf(int v) { + return Arrays.stream(values()).filter(e -> (v & e.mask) != 0).collect(Collectors.toCollection(() -> EnumSet.noneOf(Feature.class))); + } + static String asString(EnumSet es) { + return Arrays.stream(values()).map(e -> es.contains(e) ? e.on : e.off).collect(Collectors.joining(", ")); + } + public static boolean containsIn(Feature e, int v) { + return valueOf(v).contains(e); + } + } + + private static final int HID_LONG_ITEM_PREFIX = 0xfe; + + static PrintStream out = System.out; + + private static final class Local { + + public int[] usages = new int[HID_MAX_USAGES]; + public int[] collectionIndex = new int[HID_MAX_USAGES]; + public int usageIndex; + public int usageMinimum; + public int delimiterDepth; + public int delimiterBranch; + + void reset() { + usageIndex = 0; + usageMinimum = 0; + delimiterDepth = 0; + delimiterBranch = 0; + Arrays.fill(usages, 0); + Arrays.fill(collectionIndex, 0); + } + } + + public static final class Global implements Cloneable { + + int usagePage; + int logicalMinimum; + int logicalMaximum; + int physicalMinimum; + int physicalMaximum; + int unitExponent; + int unit; + int reportId; + int reportSize; + int reportCount; + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(System.err); + return null; + } + } + } + + public static class EORException extends IllegalStateException {} + + public static final class Item { + + int size; + ItemType type; + Tag tag; + int uValue; + int sValue; + + Item(int size, ItemType type, int tag, int value) { + this.size = size; + this.type = type; + this.tag = type.valueOf(tag); + + uValue = value; + sValue = value; + switch (size) { // for long items 'size' is not valid, but they are no supported anyway and have value==0 + case 1: + if ((value & 0xffff_ff80) != 0) + sValue |= 0xffff_ff00; + break; + case 2: + if ((value & 0xffff_8000) != 0) + sValue |= 0xffff_0000; + break; + default: + break; + } + } + + /** */ + static boolean processNext(HidParser context) { + if (context.parseIndex >= context.descriptorLength) { +logger.finer("EOD"); + return false; + } + Item item; + int at = context.parseIndex; + int prev = context.descriptor[context.parseIndex++] & 0xff; + + if (prev == HID_LONG_ITEM_PREFIX) { + if (context.parseIndex >= context.descriptorLength) + throw new IllegalStateException("unexpected end of data white fetching long item size"); + + int size = context.descriptor[context.parseIndex++] & 0xff; + if (context.parseIndex >= context.descriptorLength) + throw new IllegalStateException("unexpected end of data white fetching long item tag"); + int tag = context.descriptor[context.parseIndex++] & 0xff; + + if (context.parseIndex + size - 1 >= context.descriptorLength) + throw new IllegalStateException("unexpected end of data white fetching long item"); + context.parseIndex += size; + item = new Item(size, ItemType.LONG, tag, 0); + } else { + int type = (prev >> 2) & 3; + int tag = (prev >> 4) & 15; + int size = prev & 3; + int value = 0; + switch (size) { + case 0: + break; + + case 1: + if (context.parseIndex >= context.descriptorLength) + throw new IllegalStateException("unexpected end of data white fetching item size==1"); + value = context.descriptor[context.parseIndex++] & 0xFF; + break; + + case 2: + if (context.parseIndex + 1 >= context.descriptorLength) + throw new IllegalStateException("unexpected end of data white fetching item size==1"); + value = (context.descriptor[context.parseIndex++] & 0xFF) | + ((context.descriptor[context.parseIndex++] & 0xFF) << 8); + break; + + case 3: + size++; // 3 means 4 bytes + if (context.parseIndex + 1 >= context.descriptorLength) + throw new IllegalStateException("unexpected end of data white fetching item size==1"); + value = (context.descriptor[context.parseIndex++] & 0xFF) | + ((context.descriptor[context.parseIndex++] & 0xFF) << 8) | + ((context.descriptor[context.parseIndex++] & 0xFF) << 16) | + (context.descriptor[context.parseIndex++] << 24); + } + + if (tag == 0 && type == 0) throw new EORException(); + if (type >= ItemType.values().length) + throw new IllegalStateException(String.format("illegal/unsupported type %d", type)); + item = new Item(size, ItemType.values()[type], tag, value); + } + + if (logger.isLoggable(Level.FINEST)) { + String tags = "?"; + if (item.tag != null) + tags = item.tag.toString(); + out.printf("[%3d] = 0x%02X: size %d type %-8s tag %-20s value 0x%6$08X (%6$d)\n", at, prev, item.size, item.type, tags, item.sValue); + } + item.tag.parse(context, item); + return true; + } + } + + private Report registerReport(int type, int id) { + for (Report r : reports) + if (r.type == type && r.id == id) + return r; + Report r = new Report(type, id, topCollection); + reports.add(r); + return r; + } + + private Field registerField(Report report, int values) { + if (report.maxField == HID_MAX_FIELDS) + throw new IllegalStateException("too many fields in report"); + + Field field = new Field(topCollection); + report.fields[report.maxField++] = field; + field.report = report; + + return field; + } + + private int lookUpCollection(int type) { + for (Collection c = topCollection; c.getParent() != null; c = c.getParent()) { + if (c.getType() == type) + return c.getUsagePair(); + } + return 0; + } + + private void addUsage(int usagePair) { + if (local.usageIndex >= local.usages.length) + throw new IllegalStateException("usage index exceeded"); +logger.finer(String.format("usage: %08x", usagePair)); + local.usages[local.usageIndex++] = usagePair; + } + + private void addField(int reportType, int flags) { + Report report = registerReport(reportType, global.reportId); + +// if ((global.logicalMinimum < 0 && +// global.logicalMaximum < global.logicalMinimum) || +// (global.logicalMinimum >= 0 && +// global.logicalMaximum < global.logicalMinimum)) { +//Debug.printf("logical range invalid 0x%x 0x%x", +// global.logicalMinimum, +// global.logicalMaximum); +// return; +// } + +logger.finer(String.format("ADD FIELD: global: %d", global.reportCount)); + int j = 0; + for (int i = 0; i < global.reportCount; i++) { + if (i < local.usageIndex) + j = i; + int offset = report.size; + report.size += global.reportSize; + Field field = registerField(report, global.reportCount); + + field.physical = lookUpCollection(HID_COLLECTION_PHYSICAL); + field.logical = lookUpCollection(HID_COLLECTION_LOGICAL); + field.application = lookUpCollection(HID_COLLECTION_APPLICATION); + + field.usage = local.usages[j]; + field.flags = flags; + field.reportOffset = offset; + field.reportType = reportType; + field.reportSize = global.reportSize; + field.logicalMinimum = global.logicalMinimum; + field.logicalMaximum = global.logicalMaximum; + field.physicalMinimum = global.physicalMinimum; + field.physicalMaximum = global.physicalMaximum; + field.unitExponent = global.unitExponent; + field.unit = global.unit; + field.init(); +logger.finer(String.format("ADD FIELD(%d): %08x (%d)", i, field.usage, j)); + } + } + + private void reset() { + rootCollection.reset(); + topCollection = rootCollection; + collectionUsage = 0; + globalStack.clear(); + delimiterDepth = 0; + parseIndex = 0; + descriptor = null; + descriptorLength = 0; + local = new Local(); + global = new Global(); + reports = new LinkedList<>(); + } + + /** entry point */ + public Collection parse(byte[] descriptor, int length) { + reset(); + this.descriptor = descriptor; + this.descriptorLength = length; + try { + while (Item.processNext(this)); + } catch (EORException e) { +logger.finer("end of report"); + } + if (topCollection.getParent() != null) + throw new IllegalStateException("unbalanced collection at end of report description"); + + if (delimiterDepth > 0) + throw new IllegalStateException("unbalanced delimiter at end of report description"); + + return rootCollection; + } + + public void dump() { +logger.finer("rootCollection: c:" + rootCollection.getChildren().size() + ", f:" + rootCollection.getFields().size()); + rootCollection.dump(out, ""); + +//logger.finer("reports:"); +// for (Report r : reports) { +// r.dump(out, ""); +// } + } +} diff --git a/coreAPI/src/main/java/net/java/games/input/usb/parser/Report.java b/coreAPI/src/main/java/net/java/games/input/usb/parser/Report.java new file mode 100644 index 00000000..2f118d5e --- /dev/null +++ b/coreAPI/src/main/java/net/java/games/input/usb/parser/Report.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2014, Kustaa Nyholm / SpareTimeLabs + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted 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 the Kustaa Nyholm or SpareTimeLabs nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * 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 HOLDER 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. + */ + +package net.java.games.input.usb.parser; + +import java.io.PrintStream; + + +/** + * Represents a hid device descriptor. + * + * @see "https://github.com/nyholku/purejavahidapi" + */ +public final class Report { + + int id; + int type; + Collection collection; + Field[] fields = new Field[HidParser.HID_MAX_FIELDS]; + int maxField; + int size; + + Report(int type, int id, Collection collection) { + this.type = type; + this.id = id; + this.collection = collection; + } + + void dump(PrintStream out, String tab) { + HidParser.out.printf(tab + "REPORT-------------------------\n"); + HidParser.out.printf(tab + " type: %s\n", new String[] {"input", "output", "feature"}[type]); + HidParser.out.printf(tab + " id: 0x%02X\n", id); + HidParser.out.printf(tab + " size: %d\n", size); + for (int i = 0; i < maxField; i++) { + fields[i].dump(out, tab + " "); + } + HidParser.out.printf(tab + "-------------------------------\n"); + } +} \ No newline at end of file diff --git a/coreAPI/src/test/java/net/java/games/input/usb/parser/HidParserTest.java b/coreAPI/src/test/java/net/java/games/input/usb/parser/HidParserTest.java new file mode 100644 index 00000000..34e40ecb --- /dev/null +++ b/coreAPI/src/test/java/net/java/games/input/usb/parser/HidParserTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 by Naohide Sano, All rights reserved. + * + * Programmed by Naohide Sano + */ + +package net.java.games.input.usb.parser; + + +import java.io.IOException; +import java.util.EnumSet; +import java.util.logging.Level; + +import net.java.games.input.plugin.DualShock4PluginBase; +import net.java.games.input.usb.UsagePage; +import net.java.games.input.usb.parser.HidParser.Feature; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import vavi.util.Debug; +import vavi.util.StringUtil; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/** + * HidParserTest. + * + * @author Naohide Sano (nsano) + * @version 0.00 2023-12-27 nsano initial version
+ */ +class HidParserTest { + + @Test + void test1() { + int[] testData = HidParserTestData.cheapGamepad; + byte[] descriptor = new byte[testData.length]; + for (int i = 0; i < descriptor.length; i++) + descriptor[i] = (byte) testData[i]; + HidParser parser = new HidParser(); + parser.parse(descriptor, descriptor.length); + parser.dump(); + } + + @Test + void test2() { + EnumSet feature = HidParser.Feature.valueOf(4); +Debug.println("enumSet: " + Feature.asString(feature)); + assertEquals("Data, Array, Relative, No Wrap, Linear, Preferred State, No Null Position, Non Volatile, Bitfield", Feature.asString(feature)); + } + + @Test + @DisplayName("get/setValue") + void test3() { + Field field = new Field(10, 3); +Debug.printf("offsetByte: %d, startBit: %d", field.offsetByte, field.startBit); +Debug.printf("%s, 0x%02x, %s", field.toBit("*", "_"), field.mask, StringUtil.toBits(field.mask)); + assertEquals("__***___", field.toBit("*", "_")); + assertEquals(1, field.offsetByte); + assertEquals(0x1c, field.mask); + + // the first byte is report id + byte[] data = { 0x01, 0x00, 0x56 }; // _X*_*_X_ .X.*.*X. + // ~~~ ~~~ + // ~~~ +--- StringUtil#toBits() ... MSB <- LSB + // +------------- Field#toBit() ... LSB -> MSB + + int v = field.getValue(data); + assertEquals(5, v); + + field.setValue(data, (byte) 0x03); // _X**__X_ .X..**X. + // ~~~ ~~~ + assertEquals(0x4e, data[2]); // index value 2 means offset byte (1) + report id size (1) + } + + @Test + @DisplayName("toBit") + void test4() { + // s != 0, l < + Field field = new Field(1, 3); + assertEquals("_***____", field.toBit("*", "_")); + + field = new Field(6, 2); + assertEquals("______**", field.toBit("*", "_")); + + field = new Field(5, 6); + assertEquals("_____******_____", field.toBit("*", "_")); + + field = new Field(4, 12); + assertEquals("____************", field.toBit("*", "_")); + } + + // TODO wip + @Test + void test5() throws IOException { + byte[] desk = HidParserTest.class.getResourceAsStream("/ds4_ir_desc.dat").readAllBytes(); + int r = desk.length; + + byte[] ir = HidParserTest.class.getResourceAsStream("/ds4_ir.dat").readAllBytes(); + DualShock4PluginBase.display(ir, System.out); + + HidParser parser = new HidParser(); +Debug.println(Level.FINER, "getFields: " + parser.parse(desk, r).enumerateFields().size()); + parser.parse(desk, r).enumerateFields().forEach(f -> { + if (UsagePage.map(f.getUsagePage()) != null) { + switch (UsagePage.map(f.getUsagePage())) { + case GENERIC_DESKTOP -> { + System.out.println(f); + } + case BUTTON -> { + System.out.println(f); + } + default -> { + } + } + } + }); + } +} \ No newline at end of file diff --git a/coreAPI/src/test/java/net/java/games/input/usb/parser/HidParserTestData.java b/coreAPI/src/test/java/net/java/games/input/usb/parser/HidParserTestData.java new file mode 100644 index 00000000..500cef76 --- /dev/null +++ b/coreAPI/src/test/java/net/java/games/input/usb/parser/HidParserTestData.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2014, Kustaa Nyholm / SpareTimeLabs + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted 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 the Kustaa Nyholm or SpareTimeLabs nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * 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 HOLDER 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. + */ + +package net.java.games.input.usb.parser; + + +public class HidParserTestData { + + static int[] standardMouseDescriptor = { // should be byte[] but initialization is easier when it is int[] + // This test descriptor from USB HID spec example + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x02, // USAGE (Mouse) + 0xa1, 0x01, // COLLECTION (Application) + 0x09, 0x01, // USAGE (Pointer) + 0xa1, 0x00, // COLLECTION (Physical) + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM (Button 1) + 0x29, 0x03, // USAGE_MAXIMUM (Button 3) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x95, 0x03, // REPORT_COUNT (3) + 0x75, 0x01, // REPORT_SIZE (1) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x05, // REPORT_SIZE (5) + 0x81, 0x03, // INPUT (Cnst,Var,Abs) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x15, 0x81, // LOGICAL_MINIMUM (-127) + 0x25, 0x7f, // LOGICAL_MAXIMUM (127) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x02, // REPORT_COUNT (2) + 0x81, 0x06, // INPUT (Data,Var,Rel) + 0xc0, // END_COLLECTION + 0xc0, // // END_COLLECTION + }; + + static int[] cheapGamepad = { // should be byte[] but initialization is easier when it is int[] + // this test data capture from VID = 0x0810 PID = 0x0005 Manufacturer = null Product = USB Gamepad Path = /sys/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1:1.0/0003:0810:0005.0002/hidraw/hidraw0 + 0x05, 0x01, // + 0x09, 0x04, // + 0xA1, 0x01, // + 0xA1, 0x02, // + 0x75, 0x08, // + 0x95, 0x05, // + 0x15, 0x00, // + 0x26, 0xFF, // + 0x00, 0x35, // + 0x00, 0x46, // + 0xFF, 0x00, // + 0x09, 0x30, // + 0x09, 0x30, // + 0x09, 0x30, // + 0x09, 0x30, // + 0x09, 0x31, // + 0x81, 0x02, // + 0x75, 0x04, // + 0x95, 0x01, // + 0x25, 0x07, // + 0x46, 0x3B, // + 0x01, 0x65, // + 0x14, 0x09, // + 0x00, 0x81, // + 0x42, 0x65, // + 0x00, 0x75, // + 0x01, 0x95, // + 0x0A, 0x25, // + 0x01, 0x45, // + 0x01, 0x05, // + 0x09, 0x19, // + 0x01, 0x29, // + 0x0A, 0x81, // + 0x02, 0x06, // + 0x00, 0xFF, // + 0x75, 0x01, // + 0x95, 0x0A, // + 0x25, 0x01, // + 0x45, 0x01, // + 0x09, 0x01, // + 0x81, 0x02, // + 0xC0, 0xA1, // + 0x02, 0x75, // + 0x08, 0x95, // + 0x04, 0x46, // + 0xFF, 0x00, // + 0x26, 0xFF, // + 0x00, 0x09, // + 0x02, 0x91, // + 0x02, // + 0xC0, // + 0xC0, // + }; + static int[] opticalMouse = { + // this test data capture from VID = 0x0461 PID = 0x4D22 Manufacturer = null Product = USB Optical Mouse Path = /sys/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1:1.0/0003:0461:4D22.0003/hidraw/hidraw1 + 0x05, 0x01, // + 0x09, 0x02, // + 0xA1, 0x01, // + 0x09, 0x01, // + 0xA1, 0x00, // + 0x05, 0x09, // + 0x19, 0x01, // + 0x29, 0x03, // + 0x15, 0x00, // + 0x25, 0x01, // + 0x75, 0x01, // + 0x95, 0x03, // + 0x81, 0x02, // + 0x75, 0x05, // + 0x95, 0x01, // + 0x81, 0x01, // + 0x05, 0x01, // + 0x09, 0x30, // + 0x09, 0x31, // + 0x09, 0x38, // + 0x15, 0x81, // + 0x25, 0x7F, // + 0x75, 0x08, // + 0x95, 0x03, // + 0x81, 0x06, // + 0xC0, // + 0xC0, // + }; +} diff --git a/coreAPI/src/test/resources/ds4_ir.dat b/coreAPI/src/test/resources/ds4_ir.dat new file mode 100644 index 00000000..caecf921 Binary files /dev/null and b/coreAPI/src/test/resources/ds4_ir.dat differ diff --git a/coreAPI/src/test/resources/ds4_ir_desc.dat b/coreAPI/src/test/resources/ds4_ir_desc.dat new file mode 100644 index 00000000..7faee86b Binary files /dev/null and b/coreAPI/src/test/resources/ds4_ir_desc.dat differ diff --git a/coreAPI/src/test/resources/logging.properties b/coreAPI/src/test/resources/logging.properties new file mode 100644 index 00000000..1328f0f9 --- /dev/null +++ b/coreAPI/src/test/resources/logging.properties @@ -0,0 +1,8 @@ +handlers=java.util.logging.ConsoleHandler +.level=INFO +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=vavi.util.logging.VaviFormatter + +vavi.util.level=FINE +net.java.games.input.level=FINE +logging.properties.usb.parser.level=FINE diff --git a/examples/pom.xml b/examples/pom.xml index 7f161d75..637f680b 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -5,7 +5,7 @@ net.java.jinput jinput-parent - 2.0.14v + 2.0.15v examples diff --git a/plugins/OSX/pom.xml b/plugins/OSX/pom.xml index 7251ff04..4adf1c11 100644 --- a/plugins/OSX/pom.xml +++ b/plugins/OSX/pom.xml @@ -5,7 +5,7 @@ net.java.jinput plugins - 2.0.14v + 2.0.15v osx-plugin @@ -96,4 +96,4 @@ test - \ No newline at end of file + diff --git a/plugins/OSX/src/main/java/net/java/games/input/osx/OSXRumbler.java b/plugins/OSX/src/main/java/net/java/games/input/osx/OSXRumbler.java index e5185523..ea3db5a0 100644 --- a/plugins/OSX/src/main/java/net/java/games/input/osx/OSXRumbler.java +++ b/plugins/OSX/src/main/java/net/java/games/input/osx/OSXRumbler.java @@ -25,12 +25,14 @@ public class OSXRumbler implements HidRumbler { private final OSXHIDDevice device; private final Component.Identifier id; private final OSXHIDElement element; + private final int reportId; /** * @param element element#cookieId is used as data offset */ - public OSXRumbler(OSXHIDDevice device, Component.Identifier id, OSXHIDElement element) { + public OSXRumbler(OSXHIDDevice device, int reportId, Component.Identifier id, OSXHIDElement element) { this.device = device; + this.reportId = reportId; this.id = id; this.element = element; } @@ -57,6 +59,11 @@ public Component.Identifier getOutputIdentifier() { return id; } + @Override + public int getReportId() { + return reportId; + } + @Override public void fill(byte[] data) { data[element.getCookie()] = (byte) value; diff --git a/plugins/OSX/src/main/java/net/java/games/input/osx/plugin/DualShock4Plugin.java b/plugins/OSX/src/main/java/net/java/games/input/osx/plugin/DualShock4Plugin.java index 5b90b7cc..0ae1f2b2 100644 --- a/plugins/OSX/src/main/java/net/java/games/input/osx/plugin/DualShock4Plugin.java +++ b/plugins/OSX/src/main/java/net/java/games/input/osx/plugin/DualShock4Plugin.java @@ -12,17 +12,15 @@ import net.java.games.input.Component; import net.java.games.input.Controller; -import net.java.games.input.DeviceSupportPlugin; import net.java.games.input.Rumbler; import net.java.games.input.osx.OSXHIDDevice; import net.java.games.input.osx.OSXHIDElement; import net.java.games.input.osx.OSXRumbler; +import net.java.games.input.plugin.DualShock4PluginBase; import net.java.games.input.usb.ElementType; import net.java.games.input.usb.GenericDesktopUsageId; -import net.java.games.input.usb.HidController; import net.java.games.input.usb.UsagePage; import net.java.games.input.usb.UsagePair; -import vavi.util.ByteUtil; import static net.java.games.input.osx.OSXHIDDevice.AXIS_DEFAULT_MAX_VALUE; import static net.java.games.input.osx.OSXHIDDevice.AXIS_DEFAULT_MIN_VALUE; @@ -31,10 +29,12 @@ /** * DualShock4Plugin. * + * TODO extract osx independent part + * * @author Naohide Sano (nsano) * @version 0.00 2023-10-24 nsano initial version
*/ -public class DualShock4Plugin implements DeviceSupportPlugin { +public class DualShock4Plugin extends DualShock4PluginBase { /** @param object OSXHIDDevice */ @Override @@ -66,115 +66,18 @@ public Collection getExtraChildControllers(Object object) { return Collections.emptyList(); } - private static final int BASE_OFFSET = 0; - @Override public Collection getExtraRumblers(Object object) { if (!(object instanceof OSXHIDDevice device)) return Collections.emptyList(); return List.of( - new OSXRumbler(device, Component.Identifier.Output.SMALL_RUMBLE, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 3, ElementType.OUTPUT, 0, 255, false)), - new OSXRumbler(device, Component.Identifier.Output.BIG_RUMBLE, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 4, ElementType.OUTPUT, 0, 255, false)), - new OSXRumbler(device, Component.Identifier.Output.LED_RED, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 5, ElementType.OUTPUT, 0, 255, false)), - new OSXRumbler(device, Component.Identifier.Output.LED_GREEN, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 6, ElementType.OUTPUT, 0, 255, false)), - new OSXRumbler(device, Component.Identifier.Output.LED_GREEN, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 7, ElementType.OUTPUT, 0, 255, false)), - new OSXRumbler(device, Component.Identifier.Output.FLASH_LED1, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 8, ElementType.OUTPUT, 0, 255, false)), - new OSXRumbler(device, Component.Identifier.Output.FLASH_LED2, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 9, ElementType.OUTPUT, 0, 255, false)) + new OSXRumbler(device, 5, DualShock4Output.SMALL_RUMBLE, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 3, ElementType.OUTPUT, 0, 255, false)), + new OSXRumbler(device, 5, DualShock4Output.BIG_RUMBLE, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 4, ElementType.OUTPUT, 0, 255, false)), + new OSXRumbler(device, 5, DualShock4Output.LED_RED, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 5, ElementType.OUTPUT, 0, 255, false)), + new OSXRumbler(device, 5, DualShock4Output.LED_GREEN, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 6, ElementType.OUTPUT, 0, 255, false)), + new OSXRumbler(device, 5, DualShock4Output.LED_GREEN, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 7, ElementType.OUTPUT, 0, 255, false)), + new OSXRumbler(device, 5, DualShock4Output.FLASH_LED1, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 8, ElementType.OUTPUT, 0, 255, false)), + new OSXRumbler(device, 5, DualShock4Output.FLASH_LED2, new OSXHIDElement(device, new UsagePair(UsagePage.GAME, GenericDesktopUsageId.GAME_PAD), BASE_OFFSET + 9, ElementType.OUTPUT, 0, 255, false)) ); } - - /** */ - public static class Report5 implements HidController.HidReport { - private final byte[] data = new byte[31]; - - public int smallRumble; - public int bigRumble; - public int ledRed; - public int ledGreen; - public int ledBlue; - public int flashLed1; - public int flashLed2; - - public Report5() { - data[0] = (byte) 255; - } - - @Override public int getReportId() { - return 5; - } - - @Override public byte[] getData() { - return data; - } - - @Override public void cascadeTo(Rumbler[] rumblers) { - for (Rumbler rumbler : rumblers) { - float value = switch ((Component.Identifier.Output) rumbler.getOutputIdentifier()) { - case SMALL_RUMBLE -> smallRumble; - case BIG_RUMBLE -> bigRumble; - case LED_RED -> ledRed; - case LED_GREEN -> ledGreen; - case LED_BLUE -> ledBlue; - case FLASH_LED1 -> flashLed1; - case FLASH_LED2 -> flashLed2; - }; - rumbler.setValue(value); - } - } - } - - /** @see "https://www.psdevwiki.com/ps4/DS4-USB" */ - public static void display(byte[] data) { - int l3x = data[1] & 0xff; - int l3y = data[2] & 0xff; - int r3x = data[3] & 0xff; - int r3y = data[4] & 0xff; - - boolean tri = (data[5] & 0x80) != 0; - boolean cir = (data[5] & 0x40) != 0; - boolean x = (data[5] & 0x20) != 0; - boolean sqr = (data[5] & 0x10) != 0; - int dPad = data[5] & 0x0f; - - enum Hat { - N("↑"), NE("↗"), E("→"), SE("↘"), S("↓"), SW("↙"), W("←"), NW("↖"), Released(" "); final String s; Hat(String s) { this.s = s; } - } - - boolean r3 = (data[6] & 0x80) != 0; - boolean l3 = (data[6] & 0x40) != 0; - boolean opt = (data[6] & 0x20) != 0; - boolean share = (data[6] & 0x10) != 0; - boolean r2 = (data[6] & 0x08) != 0; - boolean l2 = (data[6] & 0x04) != 0; - boolean r1 = (data[6] & 0x02) != 0; - boolean l1 = (data[6] & 0x01) != 0; - - int counter = (data[7] >> 2) & 0x3f; - boolean tPad = (data[7] & 0x02) != 0; - boolean ps = (data[7] & 0x01) != 0; - - int lTrigger = data[8] & 0xff; - int rTrigger = data[9] & 0xff; - - int timeStump = ByteUtil.readLeShort(data, 10) & 0xffff; - int batteryLevel = data[12] & 0xff; - - int gyroX = ByteUtil.readLeShort(data, 13) & 0xffff; - int gyroY = ByteUtil.readLeShort(data, 15) & 0xffff; - int gyroZ = ByteUtil.readLeShort(data, 17) & 0xffff; - - int accelX = ByteUtil.readLeShort(data, 19) & 0xffff; - int accelY = ByteUtil.readLeShort(data, 21) & 0xffff; - int accelZ = ByteUtil.readLeShort(data, 23) & 0xffff; - - boolean headphone = (data[30] & 0x40) != 0; - boolean mic = (data[30] & 0x20) != 0; - - int touchX = (data[36] & 0xff) | ((data[37] & 0x0f) << 8); - int touchY = ((data[38] & 0xff) << 4) | (data[37] & 0xf); - - System.out.printf("L3 x:%02x y:%02x R3 x:%02x y:%02x (%d,%d)%n", l3x, l3y, r3x, r3y, counter, timeStump); - System.out.printf("%3s %3s %3s %3s %5s %2s %s %s %s%n", tri ? "▲" : "", cir ? "●" : "", x ? "✖" : "", sqr ? "■" : "", tPad ? "T-PAD" : "", ps ? "PS" : "", Hat.values()[dPad].s, headphone ? "\uD83C\uDFA7" : "", mic ? "🎤" : ""); - System.out.printf("touch x:%d y:%d gyro x:%04x y:%04x z:%04x, accel x:%04x y:%04x z:%04x%n%n", touchX, touchY, gyroX, gyroY, gyroZ, accelX, accelY, accelZ); - } } diff --git a/plugins/OSX/src/test/java/net/java/games/input/osx/OSXPluginTest.java b/plugins/OSX/src/test/java/net/java/games/input/osx/OSXPluginTest.java index 1b84deff..28ce46b2 100644 --- a/plugins/OSX/src/test/java/net/java/games/input/osx/OSXPluginTest.java +++ b/plugins/OSX/src/test/java/net/java/games/input/osx/OSXPluginTest.java @@ -11,7 +11,7 @@ import java.util.Arrays; import net.java.games.input.ControllerEnvironment; -import net.java.games.input.osx.plugin.DualShock4Plugin.Report5; +import net.java.games.input.plugin.DualShock4PluginBase.Report5; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/plugins/awt/pom.xml b/plugins/awt/pom.xml index 23f7e8b2..c533d9f5 100644 --- a/plugins/awt/pom.xml +++ b/plugins/awt/pom.xml @@ -5,7 +5,7 @@ net.java.jinput plugins - 2.0.14v + 2.0.15v awt-plugin diff --git a/plugins/linux/pom.xml b/plugins/linux/pom.xml index f62ff6b8..23557423 100644 --- a/plugins/linux/pom.xml +++ b/plugins/linux/pom.xml @@ -5,7 +5,7 @@ net.java.jinput plugins - 2.0.14v + 2.0.15v linux-plugin diff --git a/plugins/linux/src/main/java/net/java/games/input/linux/LinuxForceFeedbackEffect.java b/plugins/linux/src/main/java/net/java/games/input/linux/LinuxForceFeedbackEffect.java index aad79819..aae9cc32 100644 --- a/plugins/linux/src/main/java/net/java/games/input/linux/LinuxForceFeedbackEffect.java +++ b/plugins/linux/src/main/java/net/java/games/input/linux/LinuxForceFeedbackEffect.java @@ -40,6 +40,21 @@ abstract class LinuxForceFeedbackEffect implements Rumbler { private static final Logger log = Logger.getLogger(LinuxForceFeedbackEffect.class.getName()); + enum ForceFeedbackEffectOutput implements Component.Identifier.Output { + ForceFeedbackEffect("forceFeedbackEffect"); + + final String name; + + @Override + public String getName() { + return name; + } + + ForceFeedbackEffectOutput(String name) { + this.name = name; + } + } + private final LinuxEventDevice device; private final int ffId; private final WriteTask writeTask = new WriteTask(); @@ -87,12 +102,12 @@ synchronized final void rumble() { @Override public final String getOutputName() { - return Component.Identifier.Output.BIG_RUMBLE.getName(); + return ForceFeedbackEffectOutput.ForceFeedbackEffect.getName(); } @Override public final Component.Identifier getOutputIdentifier() { - return Component.Identifier.Output.BIG_RUMBLE; + return ForceFeedbackEffectOutput.ForceFeedbackEffect; } private final class UploadTask extends LinuxDeviceTask { diff --git a/plugins/pom.xml b/plugins/pom.xml index 554cc110..6fc84cd4 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -5,7 +5,7 @@ net.java.jinput jinput-parent - 2.0.14v + 2.0.15v plugins diff --git a/plugins/windows/pom.xml b/plugins/windows/pom.xml index 8a98433e..b970cbc6 100644 --- a/plugins/windows/pom.xml +++ b/plugins/windows/pom.xml @@ -5,7 +5,7 @@ net.java.jinput plugins - 2.0.14v + 2.0.15v windows-plugin diff --git a/plugins/windows/src/main/java/net/java/games/input/windows/IDirectInputEffect.java b/plugins/windows/src/main/java/net/java/games/input/windows/IDirectInputEffect.java index 0c29de40..dbf800dc 100644 --- a/plugins/windows/src/main/java/net/java/games/input/windows/IDirectInputEffect.java +++ b/plugins/windows/src/main/java/net/java/games/input/windows/IDirectInputEffect.java @@ -53,6 +53,21 @@ final class IDirectInputEffect implements Rumbler { private static final Logger log = Logger.getLogger(IDirectInputEffect.class.getName()); + enum DirectInputEffectOutput implements Component.Identifier.Output { + DirectInputEffect("directInputEffect"); + + final String name; + + @Override + public String getName() { + return name; + } + + DirectInputEffectOutput(String name) { + this.name = name; + } + } + private final Pointer address; private final DIEffectInfo info; private boolean released; @@ -85,12 +100,12 @@ synchronized void rumble() { @Override public Component.Identifier getOutputIdentifier() { - return Component.Identifier.Output.BIG_RUMBLE; + return DirectInputEffectOutput.DirectInputEffect; } @Override public String getOutputName() { - return Component.Identifier.Output.BIG_RUMBLE.getName(); + return DirectInputEffectOutput.DirectInputEffect.getName(); } public synchronized void release() { diff --git a/plugins/wintab/pom.xml b/plugins/wintab/pom.xml index 251709a9..6f9b8762 100644 --- a/plugins/wintab/pom.xml +++ b/plugins/wintab/pom.xml @@ -5,7 +5,7 @@ net.java.jinput plugins - 2.0.14v + 2.0.15v ../ diff --git a/pom.xml b/pom.xml index fd97c67d..b787092c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ net.java.jinput jinput-parent - 2.0.14v + 2.0.15v pom JInput diff --git a/tests/pom.xml b/tests/pom.xml index dadca73d..8e6b9996 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -5,7 +5,7 @@ net.java.jinput jinput-parent - 2.0.14v + 2.0.15v tests