xeij/ZKeyLEDPort.java
//========================================================================================
//  ZKeyLEDPort.java
//    en:Z keyboard LED port
//    ja:ZキーボードLEDポート
//  Copyright (C) 2003-2025 Makoto Kamada
//
//  This file is part of the XEiJ (X68000 Emulator in Java).
//  You can use, modify and redistribute the XEiJ if the conditions are met.
//  Read the XEiJ License for more details.
//  https://stdkmd.net/xeij/
//========================================================================================

package xeij;

import java.io.*;
import java.lang.foreign.*;  //Arena,FunctionDescriptor,Linker,MemorySegment,SymbolLookup,ValueLayout
import java.lang.invoke.*;  //MethodHandle
import java.nio.charset.*;  //StandardCharsets
import java.util.*;  //NoSuchElementException

public final class ZKeyLEDPort implements AutoCloseable {

  private boolean debugFlag;
  public void setDebugFlag (boolean debugFlag) {
    this.debugFlag = debugFlag;
  }

  //構造体

  //GUID構造体
  //  https://learn.microsoft.com/ja-jp/windows/win32/api/guiddef/ns-guiddef-guid
  private static final MemoryLayout GUID = MemoryLayout.structLayout (
    ValueLayout.JAVA_INT.withName ("Data1"),  //0 unsigned long Data1
    ValueLayout.JAVA_SHORT.withName ("Data2"),  //4 unsigned short Data2
    ValueLayout.JAVA_SHORT.withName ("Data3"),  //6 unsigned short Data3
    MemoryLayout.sequenceLayout (
      8,
      ValueLayout.JAVA_BYTE).withName ("Data4")  //8 unsigned char Data4[8]
    //16
    );

  //HIDD_ATTRIBUTES構造体
  //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/ns-hidsdi-_hidd_attributes
  private static final MemoryLayout HIDD_ATTRIBUTES = MemoryLayout.structLayout (
    ValueLayout.JAVA_INT.withName ("Size"),  //0 ULONG Size
    ValueLayout.JAVA_SHORT.withName ("VendorID"),  //4 USHORT VendorID
    ValueLayout.JAVA_SHORT.withName ("ProductID"),  //6 USHORT ProductID
    ValueLayout.JAVA_SHORT.withName ("VersionNumber"),  //8 USHORT VersionNumber
    MemoryLayout.paddingLayout (2)  //10
    //12
    );

  //HIDP_CAPS構造体
  //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidpi/ns-hidpi-_hidp_caps
  private static final MemoryLayout HIDP_CAPS = MemoryLayout.structLayout (
    ValueLayout.JAVA_SHORT.withName ("Usage"),  //0 USAGE Usage  USAGEはUSHORT。hidusage.hで定義されている
    ValueLayout.JAVA_SHORT.withName ("UsagePage"),  //2 USAGE UsagePage
    ValueLayout.JAVA_SHORT.withName ("InputReportByteLength"),  //4 USHORT InputReportByteLength
    ValueLayout.JAVA_SHORT.withName ("OutputReportByteLength"),  //6 USHORT OutputReportByteLength
    ValueLayout.JAVA_SHORT.withName ("FeatureReportByteLength"),  //8 USHORT FeatureReportByteLength
    MemoryLayout.sequenceLayout (
      17,
      ValueLayout.JAVA_SHORT).withName ("Reserved"),  //10 USHORT Reserved[17]
    ValueLayout.JAVA_SHORT.withName ("NumberLinkCollectionNodes"),  //44 USHORT NumberLinkCollectionNodes
    ValueLayout.JAVA_SHORT.withName ("NumberInputButtonCaps"),  //46 USHORT NumberInputButtonCaps
    ValueLayout.JAVA_SHORT.withName ("NumberInputValueCaps"),  //48 USHORT NumberInputValueCaps
    ValueLayout.JAVA_SHORT.withName ("NumberInputDataIndices"),  //50 USHORT NumberInputDataIndices
    ValueLayout.JAVA_SHORT.withName ("NumberOutputButtonCaps"),  //52 USHORT NumberOutputButtonCaps
    ValueLayout.JAVA_SHORT.withName ("NumberOutputValueCaps"),  //54 USHORT NumberOutputValueCaps
    ValueLayout.JAVA_SHORT.withName ("NumberOutputDataIndices"),  //56 USHORT NumberOutputDataIndices
    ValueLayout.JAVA_SHORT.withName ("NumberFeatureButtonCaps"),  //58 USHORT NumberFeatureButtonCaps
    ValueLayout.JAVA_SHORT.withName ("NumberFeatureValueCaps"),  //60 USHORT NumberFeatureValueCaps
    ValueLayout.JAVA_SHORT.withName ("NumberFeatureDataIndices")  //62 USHORT NumberFeatureDataIndices
    //64
    );

  //INPUT構造体
  //  https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/ns-winuser-input
  private static final int INPUT_KEYBOARD = 1;
  private static final int KEYEVENTF_KEYUP = 0x0002;
  private static final MemoryLayout INPUT = MemoryLayout.structLayout (
    ValueLayout.JAVA_INT.withName ("type"),  //0 DWORD type
    MemoryLayout.paddingLayout (4),  //4
    MemoryLayout.unionLayout (
      //MOUSEINPUT構造体
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/ns-winuser-mouseinput
      MemoryLayout.structLayout (
        ValueLayout.JAVA_INT.withName ("dx"),  //8 LONG dx
        ValueLayout.JAVA_INT.withName ("dy"),  //12 LONG dy
        ValueLayout.JAVA_INT.withName ("mouseData"),  //16 DWORD mouseData
        ValueLayout.JAVA_INT.withName ("dwFlags"),  //20 DWORD dwFlags
        ValueLayout.JAVA_INT.withName ("time"),  //24 DWORD time
        MemoryLayout.paddingLayout (4),  //28
        ValueLayout.ADDRESS.withName ("dwExtraInfo")  //32 ULONG_PTR dwExtraInfo
        //40
        ).withName ("mi"),  //8
      //KEYBDINPUT構造体
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/ns-winuser-keybdinput
      MemoryLayout.structLayout (
        ValueLayout.JAVA_SHORT.withName ("wVk"),  //8 WORD wVk
        ValueLayout.JAVA_SHORT.withName ("wScan"),  //10 WORD wScan
        ValueLayout.JAVA_INT.withName ("dwFlags"),  //12 DWORD dwFlags
        ValueLayout.JAVA_INT.withName ("time"),  //16 DWORD time
        MemoryLayout.paddingLayout (4),  //20
        ValueLayout.ADDRESS.withName ("dwExtraInfo")  //24 ULONG_PTR dwExtraInfo
        //32
        ).withName ("ki"),  //8
      //HARDWAREINPUT構造体
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/ns-winuser-hardwareinput
      MemoryLayout.structLayout (
        ValueLayout.JAVA_INT.withName ("uMsg"),  //8 DWORD uMsg
        ValueLayout.JAVA_SHORT.withName ("wParamL"),  //12 WORD wParamL
        ValueLayout.JAVA_SHORT.withName ("wParamH")  //14 WORD wParamH
        //16
        ).withName ("hi")  //8
      ).withName ("DUMMYUNIONNAME")
    //40
    );

  //SP_DEVICE_INTERFACE_DATA構造体
  //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/ns-setupapi-sp_device_interface_data
  private static final MemoryLayout SP_DEVICE_INTERFACE_DATA = MemoryLayout.structLayout (
    ValueLayout.JAVA_INT.withName ("cbSize"),  //0 DWORD cbSize
    GUID.withName ("InterfaceClassGuid"),  //4 GUID InterfaceClassGuid
    ValueLayout.JAVA_INT.withName ("Flags"),  //20 DWORD Flags
    ValueLayout.ADDRESS.withName ("Reserved")  //24 ULONG_PTR Reserved
    //32
    );

  //SP_DEVICE_INTERFACE_DETAIL_DATA_W構造体
  //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/ns-setupapi-sp_device_interface_detail_data_w
  private static final int ANYSIZE_ARRAY = 1;
  private static final MemoryLayout SP_DEVICE_INTERFACE_DETAIL_DATA_W = MemoryLayout.structLayout (
    ValueLayout.JAVA_INT.withName ("cbSize"),  //0 DWORD cbSize
    MemoryLayout.sequenceLayout (
      ANYSIZE_ARRAY,
      ValueLayout.JAVA_SHORT).withName ("DevicePath"),  //4 WCHAR DevicePath[ANYSIZE_ARRAY]
    MemoryLayout.paddingLayout (2)  //6
    //8
    );

  //エラーコード
  //  https://learn.microsoft.com/ja-jp/windows/win32/debug/system-error-codes--0-499-
  private final int ERROR_INSUFFICIENT_BUFFER = 122;
  private final int ERROR_NO_MORE_ITEMS = 259;

  //リンカ
  private Linker linker;
  private MethodHandle downcallHandle (MemorySegment address, FunctionDescriptor function) {
    return linker.downcallHandle (address, function);
  }

  //アリーナ
  private Arena arena;

  //メソッド
  private MethodHandle CloseHandle;
  private static final int FILE_SHARE_READ = 0x00000001;
  private static final int FILE_SHARE_WRITE = 0x00000002;
  private static final int OPEN_EXISTING = 3;
  private static final long INVALID_HANDLE_VALUE = -1L;
  private MethodHandle CreateFileW;
  private MethodHandle GetKeyState;
  private MethodHandle GetLastError;
  private MethodHandle HidD_FreePreparsedData;
  private MethodHandle HidD_GetAttributes;
  private MethodHandle HidD_GetHidGuid;
  private MethodHandle HidD_GetPreparsedData;
  private MethodHandle HidD_SetFeature;
  private static final int HIDP_STATUS_SUCCESS = 0x0 << 28 | 0x11 << 16 | 0;
  private MethodHandle HidP_GetCaps;
  private MethodHandle SendInput;
  private MethodHandle SetupDiDestroyDeviceInfoList;
  private MethodHandle SetupDiEnumDeviceInterfaces;
  private static final int DIGCF_PRESENT = 0x00000002;
  private static final int DIGCF_DEVICEINTERFACE = 0x00000010;
  private MethodHandle SetupDiGetClassDevsA;
  private MethodHandle SetupDiGetDeviceInterfaceDetailW;

  //ハンドル
  private MemorySegment handle;

  //port = new ZKeyLEDPort (debugFlag)
  //  コンストラクタ
  public ZKeyLEDPort (boolean debugFlag) throws IOException {
    this.debugFlag = debugFlag;
    if (debugFlag) {
      System.out.println ("ZKeyLEDPort(\"" + debugFlag + "\")");
    }

    //リンカ
    linker = Linker.nativeLinker ();

    //アリーナ
    arena = Arena.ofAuto ();

    //ライブラリ
    SymbolLookup hid = SymbolLookup.libraryLookup ("hid", arena);
    SymbolLookup kernel32 = SymbolLookup.libraryLookup ("kernel32", arena);
    SymbolLookup setupapi = SymbolLookup.libraryLookup ("setupapi", arena);
    SymbolLookup user32 = SymbolLookup.libraryLookup ("user32", arena);

    //メソッド
    try {

      //CloseHandle関数
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/handleapi/nf-handleapi-closehandle
      CloseHandle = downcallHandle (
        kernel32.findOrThrow ("CloseHandle"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT,  //BOOL
          ValueLayout.ADDRESS));  //HANDLE hObject

      //CreateFileW関数
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/fileapi/nf-fileapi-createfilew
      CreateFileW = downcallHandle (
        kernel32.findOrThrow ("CreateFileW"),
        FunctionDescriptor.of (
          ValueLayout.ADDRESS,  //HANDLE
          ValueLayout.ADDRESS,  //LPCWSTR lpFileName
          ValueLayout.JAVA_INT,  //DWORD dwDesiredAccess
          ValueLayout.JAVA_INT,  //DWORD dwShareMode
          ValueLayout.ADDRESS,  //LPSECURITY_ATTRIBUTES lpSecurityAttributes
          ValueLayout.JAVA_INT,  //DWORD dwCreationDisposition
          ValueLayout.JAVA_INT,  //DWORD dwFlagsAndAttributes
          ValueLayout.ADDRESS));  //HANDLE hTemplateFile

      //GetKeyState関数
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getkeystate
      GetKeyState = downcallHandle (
        user32.findOrThrow ("GetKeyState"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_SHORT,  //SHORT
          ValueLayout.JAVA_INT));  //int nVirtKey

      //GetLastError関数
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror
      GetLastError = downcallHandle (
        kernel32.findOrThrow ("GetLastError"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT));  //DWORD

      //HidD_FreePreparsedData関数
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_freepreparseddata
      HidD_FreePreparsedData = downcallHandle (
        hid.findOrThrow ("HidD_FreePreparsedData"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT,  //BOOLEAN
          ValueLayout.ADDRESS));  //PHIDP_PREPARSED_DATA PreparsedData

      //HidD_GetAttributes関数
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_getattributes
      HidD_GetAttributes = downcallHandle (
        hid.findOrThrow ("HidD_GetAttributes"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT,  //BOOLEAN
          ValueLayout.ADDRESS,  //HANDLE HidDeviceObject
          ValueLayout.ADDRESS));  //PHIDD_ATTRIBUTES Attributes

      //HidD_GetHidGuid関数
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_gethidguid
      HidD_GetHidGuid = downcallHandle (
        hid.findOrThrow ("HidD_GetHidGuid"),
        FunctionDescriptor.ofVoid (
          ValueLayout.ADDRESS));  //LPGUID HidGuid

      //HidD_GetPreparsedData関数
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_getpreparseddata
      HidD_GetPreparsedData = downcallHandle (
        hid.findOrThrow ("HidD_GetPreparsedData"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT,  //BOOLEAN
          ValueLayout.ADDRESS,  //HANDLE HidDeviceObject
          ValueLayout.ADDRESS));  //PHIDP_PREPARSED_DATA *PreparsedData

      //HidD_SetFeature関数
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_setfeature
      HidD_SetFeature = downcallHandle (
        hid.findOrThrow ("HidD_SetFeature"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT,  //BOOLEAN
          ValueLayout.ADDRESS,  //HANDLE HidDeviceObject
          ValueLayout.ADDRESS,  //PVOID ReportBuffer
          ValueLayout.JAVA_INT));  //ULONG ReportBufferLength

      //HidP_GetCaps関数
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidpi/nf-hidpi-hidp_getcaps
      HidP_GetCaps = downcallHandle (
        hid.findOrThrow ("HidP_GetCaps"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT,  //NTSTATUS
          ValueLayout.ADDRESS,  //PHIDP_PREPARSED_DATA PreparsedData
          ValueLayout.ADDRESS));  //PHIDP_CAPS Capabilities

      //SendInput関数
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-sendinput
      SendInput = downcallHandle (
        user32.findOrThrow ("SendInput"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT,  //UINT
          ValueLayout.JAVA_INT,  //UINT cInputs
          ValueLayout.ADDRESS,  //LPINPUT pInputs
          ValueLayout.JAVA_INT));  //int cbSize

      //SetupDiDestroyDeviceInfoList関数
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/nf-setupapi-setupdidestroydeviceinfolist
      SetupDiDestroyDeviceInfoList = downcallHandle (
        setupapi.findOrThrow ("SetupDiDestroyDeviceInfoList"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT,  //BOOL
          ValueLayout.ADDRESS));  //HDEVINFO DeviceInfoSet

      //SetupDiEnumDeviceInterfaces関数
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/nf-setupapi-setupdienumdeviceinterfaces
      SetupDiEnumDeviceInterfaces = downcallHandle (
        setupapi.findOrThrow ("SetupDiEnumDeviceInterfaces"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT,  //BOOL
          ValueLayout.ADDRESS,  //HDEVINFO DeviceInfoSet
          ValueLayout.ADDRESS,  //PSP_DEVINFO_DATA DeviceInfoData
          ValueLayout.ADDRESS,  //GUID *InterfaceClassGuid
          ValueLayout.JAVA_INT,  //DWORD MemberIndex
          ValueLayout.ADDRESS));  //PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData

      //SetupDiGetClassDevsA関数
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/nf-setupapi-setupdigetclassdevsa
      SetupDiGetClassDevsA = downcallHandle (
        setupapi.findOrThrow ("SetupDiGetClassDevsA"),  //SetupDiGetClassDevsWは見つからない
        FunctionDescriptor.of (
          ValueLayout.ADDRESS,  //HDEVINFO
          ValueLayout.ADDRESS,  //GUID *ClassGuid
          ValueLayout.ADDRESS,  //PCSTR Enumerator
          ValueLayout.ADDRESS,  //HWND hwndParent
          ValueLayout.JAVA_INT));  //DWORD Flags

      //SetupDiGetDeviceInterfaceDetailW関数
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/nf-setupapi-setupdigetdeviceinterfacedetailw
      SetupDiGetDeviceInterfaceDetailW = downcallHandle (
        setupapi.findOrThrow ("SetupDiGetDeviceInterfaceDetailW"),
        FunctionDescriptor.of (
          ValueLayout.JAVA_INT,  //BOOL
          ValueLayout.ADDRESS,  //HDEVINFO DeviceInfoSet
          ValueLayout.ADDRESS,  //PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData
          ValueLayout.ADDRESS,  //PSP_DEVICE_INTERFACE_DETAIL_DATA_W DeviceInterfaceDetailData
          ValueLayout.JAVA_INT,  //DWORD DeviceInterfaceDetailDataSize
          ValueLayout.ADDRESS,  //PDWORD RequiredSize
          ValueLayout.ADDRESS));  //PSP_DEVINFO_DATA DeviceInfoData

    } catch (NoSuchElementException nsee) {
      nsee.printStackTrace ();
    }

    handle = null;
    open ();
  }

  //------------------------------------------------------------------------
  //ZKeyLEDPort
  //  close ()
  //  ポートを閉じる
  @Override public void close () {
    //閉じていたら何もしない
    if (handle == null || handle.address () == INVALID_HANDLE_VALUE) {
      return;
    }
    //ハンドルを閉じる
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/handleapi/nf-handleapi-closehandle
      if ((int) CloseHandle.invoke (
        handle) == 0 &&  //HANDLE hObject
          (error = (int) GetLastError.invoke ()) != -1) {
        if (debugFlag) {
          System.out.printf ("close: CloseHandle failed (%d)\n", error);
        }
        //handle = null;
      }
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("close: CloseHandle invocation failed\n");
      }
      //handle = null;
    }
    //ハンドルを消す
    handle = null;
  }  //close

  //------------------------------------------------------------------------
  //ZKeyLEDPort
  //  hitKey (vk)
  //  キーを叩く
  //  vk       キー
  public void hitKey (int vk) {
    MemorySegment pInputs = arena.allocate (INPUT.byteSize () * 2,
                                            INPUT.byteAlignment ());
    pInputs.fill ((byte) 0);
    pInputs.set (ValueLayout.JAVA_INT,
                 INPUT.byteOffset (MemoryLayout.PathElement.groupElement ("type")),
                 INPUT_KEYBOARD);
    pInputs.set (ValueLayout.JAVA_SHORT,
                 INPUT.byteOffset (MemoryLayout.PathElement.groupElement ("DUMMYUNIONNAME"),
                                   MemoryLayout.PathElement.groupElement ("ki"),
                                   MemoryLayout.PathElement.groupElement ("wVk")),
                 (short) vk);
    pInputs.set (ValueLayout.JAVA_INT,
                 INPUT.byteSize () +
                 INPUT.byteOffset (MemoryLayout.PathElement.groupElement ("type")),
                 INPUT_KEYBOARD);
    pInputs.set (ValueLayout.JAVA_SHORT,
                 INPUT.byteSize () +
                 INPUT.byteOffset (MemoryLayout.PathElement.groupElement ("DUMMYUNIONNAME"),
                                   MemoryLayout.PathElement.groupElement ("ki"),
                                   MemoryLayout.PathElement.groupElement ("wVk")),
                 (short) vk);
    pInputs.set (ValueLayout.JAVA_INT,
                 INPUT.byteSize () +
                 INPUT.byteOffset (MemoryLayout.PathElement.groupElement ("DUMMYUNIONNAME"),
                                   MemoryLayout.PathElement.groupElement ("ki"),
                                   MemoryLayout.PathElement.groupElement ("dwFlags")),
                 KEYEVENTF_KEYUP);
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-sendinput
      if ((int) SendInput.invoke (
        2,  //UINT cInputs
        pInputs,  //LPINPUT pInputs
        (int) INPUT.byteSize ()) == 0 &&  //int cbSize
          (error = (int) GetLastError.invoke ()) != -1) {
        if (debugFlag) {
          System.out.printf ("hitKey: SendInput failed (%d)\n", error);
        }
      }
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("hitKey: SendInput invocation failed\n");
      }
    }
  }  //hitKey

  //------------------------------------------------------------------------
  //ZKeyLEDPort
  //  pressed = isKeyPressed (vk)
  //  キーは押されているか
  //  vk       キー
  //  pressed  true   キーは押されている
  //           false  キーは離されている
  public boolean isKeyPressed (int vk) {
    try {
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getkeystate
      return ((short) GetKeyState.invoke (
        vk  //int nVirtKey
        ) & 128) != 0;
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("isKeyPressed: GetKeyState invocation failed\n");
      }
    }
    return false;
  }  //isKeyPressed

  //------------------------------------------------------------------------
  //ZKeyLEDPort
  //  toggled = isKeyToggled (vk)
  //  キーは点灯しているか
  //  vk       キー
  //  toggled  true   キーは点灯している
  //           false  キーは消灯している
  public boolean isKeyToggled (int vk) {
    try {
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getkeystate
      return ((short) GetKeyState.invoke (
        vk  //int nVirtKey
        ) & 1) != 0;
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("isKeyToggled: GetKeyState invocation failed\n");
      }
    }
    return false;
  }  //isKeyToggled

  private void printCaps (String devicePath) {
    System.out.printf ("--------------------------------------\n");
    System.out.printf ("%s\n", devicePath);
    System.out.printf ("--------------------------------------\n");
    MemorySegment handle;  //HANDLE handle
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/fileapi/nf-fileapi-createfilew
      if ((handle = (MemorySegment) CreateFileW.invoke (
        arena.allocateFrom (devicePath, StandardCharsets.UTF_16LE),  //LPCWSTR lpFileName
        0,  //DWORD dwDesiredAccess
        FILE_SHARE_READ | FILE_SHARE_WRITE,  //DWORD dwShareMode
        MemorySegment.NULL,  //LPSECURITY_ATTRIBUTES lpSecurityAttributes
        OPEN_EXISTING,  //DWORD dwCreationDisposition
        0,  //DWORD dwFlagsAndAttributes
        MemorySegment.NULL)  //HANDLE hTemplateFile
           ).address () == INVALID_HANDLE_VALUE &&
          (error = (int) GetLastError.invoke ()) != -1) {
        System.out.printf ("printCaps: CreateFileW %s failed (%d)\n", devicePath, error);
        return;
      }
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("printCaps: CreateFileW invocation failed\n");
      }
      return;
    }
    MemorySegment attributes = arena.allocate (HIDD_ATTRIBUTES);
    attributes.fill ((byte) 0);
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_getattributes
      if ((int) HidD_GetAttributes.invoke (
        handle,  //HANDLE HidDeviceObject
        attributes) == 0 &&  //PHIDD_ATTRIBUTES Attributes
          (error = (int) GetLastError.invoke ()) != -1) {
        System.out.printf ("printCaps: HidD_GetAttributes failed (%d)\n", error);
      } else {
        System.out.printf ("VendorID\t\t\t0x%04x\n",
                           0xffff & attributes.get (ValueLayout.JAVA_SHORT,
                                                    HIDD_ATTRIBUTES.byteOffset (MemoryLayout.PathElement.groupElement ("VendorID"))));
        System.out.printf ("ProductID\t\t\t0x%04x\n",
                           0xffff & attributes.get (ValueLayout.JAVA_SHORT,
                                                    HIDD_ATTRIBUTES.byteOffset (MemoryLayout.PathElement.groupElement ("ProductID"))));
        System.out.printf ("VersionNumber\t\t\t0x%04x\n",
                           0xffff & attributes.get (ValueLayout.JAVA_SHORT,
                                                    HIDD_ATTRIBUTES.byteOffset (MemoryLayout.PathElement.groupElement ("VersionNumber"))));
        System.out.printf ("--------------------------------------\n");
      }
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("printCaps: HidD_GetAttributes invocation failed\n");
      }
    }
    MemorySegment preparsedDataHandle = arena.allocate (ValueLayout.ADDRESS);  //PHIDP_PREPARSED_DATA
    preparsedDataHandle.fill ((byte) 0);
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_getpreparseddata
      if ((int) HidD_GetPreparsedData.invoke (
        handle,  //HANDLE HidDeviceObject
        preparsedDataHandle) == 0 &&  //PHIDP_PREPARSED_DATA *PreparsedData
          (error = (int) GetLastError.invoke ()) != -1) {
        System.out.printf ("printCaps: HidD_GetPreparsedData failed (%d)\n", error);
        return;
      }
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("printCaps: HidD_GetPreparsedData invocation failed\n");
      }
      return;
    }
    MemorySegment preparsedData = preparsedDataHandle.get (ValueLayout.ADDRESS, 0L);
    MemorySegment caps = arena.allocate (HIDP_CAPS);
    caps.fill ((byte) 0);
    try {
      int ntstatus;
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidpi/nf-hidpi-hidp_getcaps
      if ((ntstatus = (int) HidP_GetCaps.invoke (
        preparsedData,  //PHIDP_PREPARSED_DATA PreparsedData
        caps)  //PHIDP_CAPS Capabilities
           ) != HIDP_STATUS_SUCCESS) {
        System.out.printf ("printCaps: HidP_GetCaps failed (0x%08x)\n", ntstatus);
      } else {
        System.out.printf ("Usage\t\t\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("Usage"))));
        System.out.printf ("UsagePage\t\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("UsagePage"))));
        System.out.printf ("InputReportByteLength\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("InputReportByteLength"))));
        System.out.printf ("OutputReportByteLength\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("OutputReportByteLength"))));
        System.out.printf ("FeatureReportByteLength\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("FeatureReportByteLength"))));
        System.out.printf ("NumberLinkCollectionNodes\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("NumberLinkCollectionNodes"))));
        System.out.printf ("NumberInputButtonCaps\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("NumberInputButtonCaps"))));
        System.out.printf ("NumberInputValueCaps\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("NumberInputValueCaps"))));
        System.out.printf ("NumberInputDataIndices\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("NumberInputDataIndices"))));
        System.out.printf ("NumberOutputButtonCaps\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("NumberOutputButtonCaps"))));
        System.out.printf ("NumberOutputValueCaps\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("NumberOutputValueCaps"))));
        System.out.printf ("NumberOutputDataIndices\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("NumberOutputDataIndices"))));
        System.out.printf ("NumberFeatureButtonCaps\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("NumberFeatureButtonCaps"))));
        System.out.printf ("NumberFeatureValueCaps\t\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("NumberFeatureValueCaps"))));
        System.out.printf ("NumberFeatureDataIndices\t0x%04x\n",
                           0xffff & caps.get (ValueLayout.JAVA_SHORT,
                                              HIDP_CAPS.byteOffset (MemoryLayout.PathElement.groupElement ("NumberFeatureDataIndices"))));
        System.out.printf ("--------------------------------------\n");
      }
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("printCaps: HidP_GetCaps invocation failed\n");
      }
    }
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_freepreparseddata
      if ((int) HidD_FreePreparsedData.invoke (
        preparsedData) == 0 &&  //PHIDP_PREPARSED_DATA PreparsedData
          (error = (int) GetLastError.invoke ()) != -1) {
        System.out.printf ("printCaps: HidD_FreePreparsedData failed (%d)\n", error);
      }
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("printCaps: HidD_FreePreparsedData invocation failed\n");
      }
    }
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/handleapi/nf-handleapi-closehandle
      if ((int) CloseHandle.invoke (
        handle) == 0 &&  //HANDLE hObject
          (error = (int) GetLastError.invoke ()) != -1) {
        System.out.printf ("printCaps: CloseHandle failed (%d)\n", error);
      }
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("printCaps: CloseHandle invocation failed\n");
      }
    }
  }

  //------------------------------------------------------------------------
  //ZKeyLEDPort
  //  open ()
  //  ポートを開く
  public void open () throws IOException {
    //開いていたら失敗
    //  Javaは変数にゴミが入っていることはない
    if (handle != null && handle.address () != INVALID_HANDLE_VALUE) {
      if (debugFlag) {
        System.out.printf ("open: already open\n");
      }
      throw new IOException ("already open");
    }
    //デバイス情報セットを作る
    MemorySegment hidGuid = arena.allocate (GUID);
    hidGuid.fill ((byte) 0);
    try {
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_gethidguid
      HidD_GetHidGuid.invoke (
        hidGuid);  //LPGUID HidGuid
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("open: HidD_GetHidGuid invocation failed\n");
      }
    }
    MemorySegment infoSet = arena.allocate (ValueLayout.ADDRESS);  //HDEVINFO
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/nf-setupapi-setupdigetclassdevsw
      if ((infoSet = (MemorySegment) SetupDiGetClassDevsA.invoke (
        hidGuid,  //GUID *ClassGuid
        MemorySegment.NULL,  //PCSTR Enumerator
        MemorySegment.NULL,  //HWND hwndParent
        DIGCF_DEVICEINTERFACE | DIGCF_PRESENT)  //DWORD Flags
           ).address () == INVALID_HANDLE_VALUE &&
          (error = (int) GetLastError.invoke ()) != -1) {
        if (debugFlag) {
          System.out.printf ("open: SetupDiGetClassDevsA failed (%d)\n", error);
        }
        throw new IOException ("SetupDiGetClassDevsA failed");
      } else {
        if (debugFlag) {
          System.out.printf ("open: SetupDiGetClassDevsA success\n");
        }
      }
    } catch (IOException ioe) {
      throw ioe;
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("open: SetupDiGetClassDevsA invocation failed\n");
      }
      return;
    }
    String targetPath = "";
    //デバイスを探す
    MemorySegment interfaceData = arena.allocate (SP_DEVICE_INTERFACE_DATA);
    for (int index = 0; ; index++) {
      //デバイスインターフェイスを得る
      interfaceData.fill ((byte) 0);
      interfaceData.set (ValueLayout.JAVA_INT,
                         SP_DEVICE_INTERFACE_DATA.byteOffset (MemoryLayout.PathElement.groupElement ("cbSize")),
                         (int) SP_DEVICE_INTERFACE_DATA.byteSize ());
      try {
        int error;
        //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/nf-setupapi-setupdienumdeviceinterfaces
        if ((int) SetupDiEnumDeviceInterfaces.invoke (
          infoSet,  //HDEVINFO DeviceInfoSet
          MemorySegment.NULL,  //PSP_DEVINFO_DATA DeviceInfoData
          hidGuid,  //GUID *InterfaceClassGuid
          index,  //DWORD MemberIndex
          interfaceData) == 0 &&  //PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData
            (error = (int) GetLastError.invoke ()) != -1) {
          if (error != 0 &&
              error != ERROR_NO_MORE_ITEMS) {
            if (debugFlag) {
              System.out.printf ("open: index %d, SetupDiEnumDeviceInterfaces failed (%d)\n", index, error);
            }
          } else {
            if (debugFlag) {
              System.out.printf ("open: index %d, no more items\n", index);
            }
          }
          break;
        }
      } catch (Throwable e) {
        e.printStackTrace ();
        if (debugFlag) {
          System.out.printf ("open: index %d, SetupDiEnumDeviceInterfaces invocation failed\n", index);
        }
        break;
      }
      if (debugFlag) {
        System.out.printf ("open: index %d, SetupDiEnumDeviceInterfaces success\n", index);
      }
      //デバイスインターフェイスの詳細のサイズを得る
      MemorySegment detailSizeSegment = arena.allocate (ValueLayout.JAVA_INT);  //DWORD detailSize
      int detailSize;
      try {
        int error;
        //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/nf-setupapi-setupdigetdeviceinterfacedetailw
        if ((int) SetupDiGetDeviceInterfaceDetailW.invoke (
          infoSet,  //HDEVINFO DeviceInfoSet
          interfaceData,  //PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData
          MemorySegment.NULL,  //PSP_DEVICE_INTERFACE_DETAIL_DATA_W DeviceInterfaceDetailData
          0,  //DWORD DeviceInterfaceDetailDataSize
          detailSizeSegment,  //PDWORD RequiredSize
          MemorySegment.NULL) == 0 &&  //PSP_DEVINFO_DATA DeviceInfoData
            (error = (int) GetLastError.invoke ()) != -1) {
          if (error != 0 &&
              error != ERROR_INSUFFICIENT_BUFFER) {
            if (debugFlag) {
              System.out.printf ("open: index %d, SetupDiGetDeviceInterfaceDetail failed (%d)\n", index, error);
            }
            continue;
          } else {
            detailSize = detailSizeSegment.get (ValueLayout.JAVA_INT, 0L);
            if (debugFlag) {
              System.out.printf ("open: index %d, SetupDiGetDeviceInterfaceDetail insufficient buffer, detailSize %d\n", index, detailSize);
            }
          }
        } else {
          if (debugFlag) {
            System.out.printf ("open: index %d, SetupDiGetDeviceInterfaceDetail unexpected success\n", index);
          }
          continue;
        }
      } catch (Throwable e) {
        e.printStackTrace ();
        if (debugFlag) {
          System.out.printf ("open: index %d, SetupDiGetDeviceInterfaceDetailW invocation failed\n", index);
        }
        continue;
      }
      //デバイスインターフェイスの詳細を得る
      MemorySegment detailData = arena.allocate ((long) detailSize, 4L);  //SP_DEVICE_INTERFACE_DETAIL_DATA_W
      detailData.fill ((byte) 0);
      detailData.set (ValueLayout.JAVA_INT,
                      SP_DEVICE_INTERFACE_DETAIL_DATA_W.byteOffset (MemoryLayout.PathElement.groupElement ("cbSize")),
                      (int) SP_DEVICE_INTERFACE_DETAIL_DATA_W.byteSize ());  //detailSizeではない。可変部分のサイズを含まない
      try {
        int error;
        //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/nf-setupapi-setupdigetdeviceinterfacedetailw
        if ((int) SetupDiGetDeviceInterfaceDetailW.invoke (
          infoSet,  //HDEVINFO DeviceInfoSet
          interfaceData,  //PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData
          detailData,  //PSP_DEVICE_INTERFACE_DETAIL_DATA_W DeviceInterfaceDetailData
          detailSize,  //DWORD DeviceInterfaceDetailDataSize
          MemorySegment.NULL,  //PDWORD RequiredSize
          MemorySegment.NULL) == 0 &&  //PSP_DEVINFO_DATA DeviceInfoData
            (error = (int) GetLastError.invoke ()) != -1) {
          if (debugFlag) {
            System.out.printf ("open: index %d, SetupDiGetDeviceInterfaceDetail failed (%d)\n", index, error);
          }
          continue;
        } else {
          if (debugFlag) {
            System.out.printf ("open: index %d, SetupDiGetDeviceInterfaceDetail success\n", index);
          }
        }
      } catch (Throwable e) {
        e.printStackTrace ();
        if (debugFlag) {
          System.out.printf ("open: index %d, SetupDiGetDeviceInterfaceDetailW invocation failed\n", index);
        }
        continue;
      }
      //デバイスパスをコピーする
      String devicePath = detailData.getString (SP_DEVICE_INTERFACE_DETAIL_DATA_W.byteOffset (MemoryLayout.PathElement.groupElement ("DevicePath")),
                                                StandardCharsets.UTF_16LE);
      String lowerPath = devicePath.toLowerCase ();
      //情報を表示する
      if (debugFlag) {
        printCaps (devicePath);
      }
      //デバイスパスを比較する
      //  DevicePathはcase-insensitive
      if (lowerPath.indexOf ("vid_33dd&pid_0011&mi_01&col05") == -1) {
        if (debugFlag) {
          System.out.printf ("open: index %d, mismatch\n", index);
        }
        continue;
      } else {
        if (debugFlag) {
          System.out.printf ("open: index %d, match\n", index);
        }
      }
      targetPath = devicePath;
    }  //for index
    //デバイス情報セットを捨てる
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/setupapi/nf-setupapi-setupdidestroydeviceinfolist
      if ((int) SetupDiDestroyDeviceInfoList.invoke (
        infoSet) == 0 &&  //HDEVINFO DeviceInfoSet
          (error = (int) GetLastError.invoke ()) != -1) {
        if (debugFlag) {
          System.out.printf ("open: SetupDiDestroyDeviceInfoList failed (%d)\n", error);
        }
        throw new IOException ("SetupDiDestroyDeviceInfoList failed");
      } else {
        if (debugFlag) {
          System.out.printf ("open: SetupDiDestroyDeviceInfoList success\n");
        }
      }
    } catch (IOException ioe) {
      throw ioe;
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("open: SetupDiDestroyDeviceInfoList invocation failed\n");
      }
      return;
    }
    //見つからなかったら失敗
    if (targetPath.equals ("")) {
      if (debugFlag) {
        System.out.printf ("open: device not found\n");
      }
      throw new IOException ("device not found");
    } else {
      if (debugFlag) {
        System.out.printf ("open: device found\n");
      }
    }
    //デバイスを開く
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows/win32/api/fileapi/nf-fileapi-createfilew
      if ((handle = (MemorySegment) CreateFileW.invoke (
        arena.allocateFrom (targetPath, StandardCharsets.UTF_16LE),  //LPCWSTR lpFileName
        0,  //DWORD dwDesiredAccess
        FILE_SHARE_READ | FILE_SHARE_WRITE,  //DWORD dwShareMode
        MemorySegment.NULL,  //LPSECURITY_ATTRIBUTES lpSecurityAttributes
        OPEN_EXISTING,  //DWORD dwCreationDisposition
        0,  //DWORD dwFlagsAndAttributes
        MemorySegment.NULL)  //HANDLE hTemplateFile
           ).address () == INVALID_HANDLE_VALUE &&
          (error = (int) GetLastError.invoke ()) != -1) {
        if (debugFlag) {
          System.out.printf ("open: CreateFile %s failed (%d)\n", targetPath, error);
          System.out.printf ("open: device not available\n");
        }
        throw new IOException ("device not available");
      } else {
        if (debugFlag) {
          System.out.printf ("open: CreateFile %s success\n", targetPath);
        }
      }
    } catch (IOException ioe) {
      throw ioe;
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("open: CreateFileW invocation failed\n");
      }
      return;
    }
    //成功して終了
  }  //open

  //------------------------------------------------------------------------
  //ZKeyLEDPort
  //  success = send (data)
  //  LEDのデータを送る
  //  success  true   成功
  //           false  失敗
  //  data    LEDのデータ。0=消灯,…,32=暗い,…,64=やや暗い,…,128=やや明るい,…,255=明るい
  //          +----------+----------+----------+----------+----------+----------+----------+----------+
  //          |63      56|55      48|47      40|39      32|31      24|23      16|15       8|7        0|
  //          |          |   全角   | ひらがな |    INS   |   CAPS   |コード入力| ローマ字 |   かな   |
  //          +----------+----------+----------+----------+----------+----------+----------+----------+
  private static final int[] indexes = new int[] { 7, 8, 9, 10, 11, 13, 14 };
  public boolean send (long data) {
    //閉じていたら失敗
    if (handle == null || handle.address () == INVALID_HANDLE_VALUE) {
      return false;
    }
    //機能レポートを作る
    final int length = 65;
    MemorySegment report = arena.allocate (length);
    report.fill ((byte) 0);
    report.set (ValueLayout.JAVA_BYTE, 0L, (byte) 10);
    report.set (ValueLayout.JAVA_BYTE, 1L, (byte) 248);
    for (int i = 0; i < 7; i++) {
      report.set (ValueLayout.JAVA_BYTE, (long) indexes[i], (byte) (data >> (8 * i)));
    }
    //機能レポートを送る
    try {
      int error;
      //  https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_setfeature
      if ((int) HidD_SetFeature.invoke (
        handle,  //HANDLE HidDeviceObject
        report,  //PVOID ReportBuffer
        length) == 0 &&  //ULONG ReportBufferLength
          (error = (int) GetLastError.invoke ()) != -1) {
        if (debugFlag) {
          System.out.printf ("send: HidD_SetFeature failed (%d)\n", error);
        }
        return false;
      }
    } catch (Throwable e) {
      e.printStackTrace ();
      if (debugFlag) {
        System.out.printf ("send: HidD_SetFeature invocation failed\n");
      }
      return false;
    }
    return true;
  }  //send

}  //class ZKeyLEDPort