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