Skip to content

HID Communication Protocol

AI-generated

This page was generated by AI from the hinata-rs and hinata_go sources. It may contain inaccuracies or omissions — corrections against the source code and real hardware are welcome.

This page describes the HID layer communication protocol, device discovery, frame structure, command bytes, and subscription/distribution model for the HINATA card reader, intended for developers who wish to implement their own host software or integration.

Reference implementations:

  • hinata-rs — Desktop Rust, based on hidapi.
  • hinata_go — Flutter, bridging WebHID (desktop/web) and Android quick_usb.

Both implementations are protocol-layer identical; differences exist only in the underlying HID channel encapsulation.

1. Device Identification

FieldValue
Vendor ID (VID)0xF822 (decimal 63522)
Product ID (PID)Not fixed (returned by device, serves as machine type identifier)
Manufacturer StringNERI (used by Linux udev rules for matching)
HID Report Size64 bytes
HID Report ID (write)0x01
HID Report ID (CardIO input)0x02 (8 bytes only, special purpose)

1.1 Platform-Specific Lookup Logic

PlatformLookup / Matching MethodEndpoint Structure
Windows / Linuxhidapi enumeration: VID=0xF822, differentiate by usage_pageDual interface: usage_page == 0x01READ, usage_page == 0x06WRITE
macOShidapi enumeration: VID=0xF822, take only usage_page == 0x06Single interface (read/write shared)
Androidquick_usb enumeration: VID=0xF822, scan all interfaces (skip class 2 CDC, class 10 CDC-Data), claim and split by endpoint direction into IN/OUTBulk IN + Bulk OUT endpoints
Webnavigator.hid (neo_web_hid), filter { vendorId: 0xF822 }Browser WebHID API

Windows device instance-id is obtained by splitting the hidapi path on # (used to pair the read/write interfaces of the same device). Windows also crawls up via SetupAPI / CM_Get_Parent, then CM_Get_Child to find sibling nodes with ClassGuid Ports (used to map the corresponding COMx).

1.2 Linux udev Rule

ATTRS{manufacturer}=="NERI", MODE="0666"

1.3 Android USB Filter

xml
<usb-device vendor-id="63522" />   <!-- 0xF822 -->

2. HID I/O Model

mermaid
graph LR
    APP[Application Code] -->|cmd+payload| FRAME[Add ReportID=1 prefix]
    FRAME -->|HID OUT 64B| READER[HINATA]
    READER -->|HID IN 64B| ROUTER{Route by<br/>first byte}
    ROUTER -->|reportId=2| CARDIO[CardIO 8B stream]
    ROUTER -->|reportId=1, header byte| SUB[Subscription table _subscriptions / mpsc]

2.1 Write Frame (Host → Reader)

[ReportID=0x01] [CMD] [PAYLOAD ...]   total length ≤ 64
  • hidapi/WebHID: device.write([0x01, cmd, ...payload]) or sendReport(1, payload_with_cmd_at_offset_0)
  • Android Bulk OUT: bulkTransferOut(write_ep, [0x01, cmd, ...payload])

2.2 Read Frame (Reader → Host)

64-byte HID input report, first byte = ReportID:

ReportIDMeaningParsing
0x01Normal command responsedata[1] = response header (generally equals sent CMD, exceptions below), data[2..] = payload
0x02CardIO input8-byte card ID data, independent callback stream

Exception: When sending CMD=0x01 (GetFirmwareTimestamp), response header = 0x32.

rust
// builder.rs
if data[1] == 1 { 50 } else { data[1] }
dart
// hinata_device.dart
if (command == 1) responseHeader = 0x32;

Reference: builder.rs, hinata_device.dart.

2.3 Why a Subscription Mechanism Is Needed

The HINATA main protocol (0x01 ReportID channel) has no packet-level sequence numbers / transaction IDs / correlation fields—unlike the Sega protocol where the SEQ byte can bind a response back to a request one-to-one. The only field in a response frame that can distinguish "whose reply is this" is the first byte (the header, ≈ original CMD). This creates several problems that must be handled:

  1. Concurrent requests collide: If two GetConfig(0xD4) commands are sent simultaneously, both responses have header 0xD4, making it impossible to distinguish their order by protocol fields alone; the upper layer must serialize them.
  2. Asynchronous pushes and synchronous responses share the same channel: The firmware may actively push data (subscription-style data, e.g., ACK + actual response in PN532 passthrough, CardIO streams), so not all IN frames are "the response to the previous OUT".
  3. Multi-frame responses / intermediate frames need to be discarded (typical: PN532 first returns ACK 00 00 FF 00 FF 00, then returns the actual data frame)—a naive "send one, receive one" model will get the wrong frame.
  4. Response header does not always equal request CMD: The reply to CMD=0x01 has header 0x32; the subscription must explicitly declare the expected header.

Both reference implementations adopt the same approach: route by header byte + each subscription carries a "when to remove" policy. This is equivalent to adding a transaction manager in the host software layer to compensate for the lack of protocol-level SEQ. Comparison:

DimensionSega Protocol (0xE0)HINATA Main Protocol (other CMDs)
Packet contains SEQ number✅ Increments per packet, precise pairing❌ None
Response-to-request relationshipBound directly by SEQMatched only by header byte
Multi-frame / ACK frame handlingRarely occurs in protocolMust be filtered by subscription policy
Unsolicited data pushesAlmost nonePresent (CardIO, PN532 async, etc.)
Host-side implementationOne frame received = pairing completeRegister subscription → route → remove by policy

In other words: the subscription mechanism is a patch for "HINATA main protocol has no SEQ"—treat the header as a weak correlation key, then use policy to express "when should this transaction end" (once, never, specific byte matches/doesn't match).

2.4 Subscription / Unsubscription Policies

The main thread registers (header_byte → Subscription) in a map. The I/O thread reads a frame, dispatches it to the corresponding subscription by data[1], and uses the "policy" to decide whether to remove it:

PolicyRemoval Trigger
Count(n)After receiving n frames
NeverNever (for continuous streams)
SpecificIsOn(idx, byte)When data[idx] == byte
SpecificNotOn(idx, byte)When data[idx] != byte

PN532 wrapped commands use SpecificNotOn(4, 0) (to filter out PN532's ACK frame 00 00 FF 00 FF 00, waiting for the actual response frame).

3. Firmware Layer Command Packet (HINATA Native Protocol)

All commands are sent via [0x01][CMD][PAYLOAD] HID OUT. The CMD in the table below is a single byte.

CMDNamePayload (Host→Reader)Response (Reader→Host, data[2..])Notes
0x01GetFirmwareTimestampEmpty10 ASCII bytes, e.g., "2025051301", convert to u32Response header is 0x32
0x07SetLed[R, G, B]None (fire-and-forget)Set LED immediately
0xD0SetStorage[index, value]NoneWrite persistent storage (NVM/EEPROM)
0xD1GetStorage[index][value] (data[2])Read persistent storage
0xD3SetConfig[index, value]NoneWrite runtime config
0xD4GetConfig[index][value] (data[2])Read runtime config
0xE0SegaTransportSega protocol frame (see §5)Sega protocol framePassthrough to Sega submodule
0xE2PN532TransportPN532 frame (see §4)PN532 framePassthrough to PN532
0xE3GetMainLoopStateEmpty[state] (data[2])Read firmware state machine
0xE5GetCommitHashEmpty4-byte commit hash (data[2..6])Firmware ≥ 2025051301 only
0xE6GetChipIdEmpty4-byte chip id (data[2..6])Firmware ≥ 2025051301 only
0xE8ResetStateMachineEmptyNoneReset main state machine
0xE9ReloadConfigEmptyNoneReload storage into runtime config
0xEAResetLedEmptyNoneLED restore to default
0xF0EnterBootloaderEmptyNoneDFU

3.1 Config / Storage Index (ConfigIndex)

0  segaBrightness   1  config0   2  config1
3  idleR  4  idleG  5  idleB
6  busyR  7  busyG  8  busyB

config0 is a bitfield:

bitMeaning
0isFirstLaunch
1cardioDisableIso14443a
2cardioIso14443aStartWithE004
3enableLedRainbow
4serialDescriptorUnique
5segaHwFw
6segaFastRead
7isNotFirstLaunch

3.2 Frame Examples

SetLed(255,0,0):    01 07 FF 00 00
GetFirmware:        01 01           → resp: 02 32 32 30 32 35 30 35 31 33 30 31 ...  ("2025051301", header 0x32)
GetStorage(idleR):  01 D1 03         → resp: 02 D1 <value>
EnterBootloader:    01 F0

4. PN532 Passthrough (CMD = 0xE2)

HINATA has a built-in PN532. Pass a standard PN532 information frame directly into the payload.

4.1 PN532 Information Frame

00 00 FF  LEN  LCS  TFI CMD [DATA...]  DCS  00
  • LEN = len(TFI + CMD + DATA) = data.len() + 2
  • LCS = (~LEN) + 1, such that LEN + LCS == 0
  • TFI = 0xD4 (Host→PN532) / 0xD5 (PN532→Host)
  • Response CMD byte increments by 1 (i.e., host_cmd + 1)
  • DCS = (~Σ(TFI..DATA)) + 1, such that Σ + DCS == 0
  • One preamble/postamble byte = 0x00 at each end

4.2 ACK Frame (to be ignored)

00 00 FF 00 FF 00

PN532 returns an ACK (LEN=0, LCS=0xFF) before the actual response. The subscription policy SpecificNotOn(4, 0) is used to skip the ACK and wait for the proper response frame where data[4] != 0.

4.3 PN532 Command Enumeration

CMDNameCMDName
0x00Diagnose0x46InJumpForPsl
0x02GetFirmwareVersion0x4AInListPassiveTarget
0x04GetGeneralStatus0x4EInPsl
0x06ReadRegister0x50InAtr
0x08WriteRegister0x52InRelease
0x0CReadGpio0x54InSelect
0x0EWriteGpio0x56InJumpForDep
0x10SetSerialBaudRate0x58RfRegulationTest
0x12SetParameters0x60InAutoPoll
0x14SamConfiguration0x86TgGetData
0x16PowerDown0x88TgGetInitiatorCommand
0x32RfConfiguration0x8ATgGetTargetStatus
0x40InDataExchange0x8CTgInitAsTarget
0x42InCommunicateThru0x8ETgSetData
0x44InDeselect0x90TgResponseToInitiator
0x92TgSetGeneralBytes
0x94TgSetMetadata

4.4 Common PN532 Command Payloads

InListPassiveTarget (0x4A) — Card Detection

  • Payload: [max_tg, brty, initial_data...]

    • brty = 0x00 → ISO14443A
    • brty = 0x01 / 0x02 → FeliCa 212 / 424kbps (initial_data generated via gen_felica_poll_initial_data)
  • FeliCa polling initial data:

    [0x00, sysCodeHi, sysCodeLo, requestCodeLo, 0x00]

    Typical polling parameters: sysCode = 0xFFFF, requestCode = 0x0001; see pn532.dart::genFelicaPollInitialData.

  • Response parsing:

    • [NbTg, Tg, ...]
    • Type A: ATQA(2 BE) SAK(1) UID_LEN(1) UID(N)
    • FeliCa: LEN(1) ResCode(1) IDm(8) PMm(8) [SystemCode(2) ...], number of SystemCode = (LEN-18)/2

InDataExchange (0x40) — Generic APDU/Mifare/FeliCa Command

  • Payload: [Tg, CMD, DATA...]
  • Response first byte is PN532 error code (see §6), followed by data.

Mifare Classic Auth (via InDataExchange)

  • CMD = 0x60 (KeyA) or 0x61 (KeyB)
  • DATA = [block_num, key(6B), uid(4B)]

Mifare Classic Read (via InDataExchange)

  • CMD = 0x30, DATA = [block_num]
  • Response: [status, 16B block]

Mifare Classic Write (via InDataExchange)

  • CMD = 0xA0, DATA = [block_num, 16B data]

Mifare Ultralight Write

  • CMD = 0xA2

FeliCa Read Without Encryption (via InDataExchange)

  • CMD = len(input)+1 (CMD field in PN532 InDataExchange is repurposed as length here)
  • DATA = [0x06, IDm(8), N_svc, svc[i](2 BE)..., N_blk, blk[i](2 BE)...]

Mifare Command Bytes

AuthA=0x60  AuthB=0x61  Read=0x30  Write=0xA0
Transfer=0xB0  Decrement=0xC0  Increment=0xC1  Store=0xC2
UltralightWrite=0xA2

FeliCa Command Bytes

Polling=0x00  RequestService=0x02  RequestResponse=0x04
ReadWithoutEncryption=0x06  WriteWithoutEncryption=0x08
RequestSystemCode=0x0C

InRelease (0x52) / InSelect (0x54)

  • Payload: [Tg], response first byte = PN532 error code.

4.5 Complete Packet Example

Get PN532 firmware version (GetFirmwareVersion):

HID OUT (64B, pad with zeros):
  01 E2  00 00 FF 02 FE D4 02 2A 00
  └Hdr  └PN532 frame─────────────────

HID IN (header=E2):
  E2  00 00 FF 00 FF 00                                  ← ACK, subscription skips
  E2  00 00 FF 06 FA D5 03 IC VER REV SUP DCS 00         ← Actual response

InListPassiveTarget(brty=1, FeliCa, max=1):

01 E2  00 00 FF 04 FC D4 4A 01 00 E1 00

5. Sega Protocol Passthrough (CMD = 0xE0, Sega-mode machines only)

Sega subboard protocol encapsulation is passed through the main frame 0xE0, with subscription policy Count(1). Complete command parsing is in sega_protocol.dart; hinata-rs provides only the raw 0xE0 channel without parsing internal fields.

5.1 Frame Format

[LEN] [ADDR=0x00] [SEQ] [CMD] [PLEN] [PAYLOAD ...]
LEN = PLEN + 5

SEQ is a monotonically increasing packet sequence number (increments by 1 per packet).

5.2 NFC Commands

CMDNameCMDName
0x30GetFwVersion0x60ToUpdaterMode
0x32GetHwVersion0x61SendHexData
0x40StartPolling0x62ToNormalMode
0x41StopPolling0x63SendBinDataInit
0x42CardDetect0x64SendBinDataExec
0x43CardSelect0x70FelicaPush
0x44CardHalt0x71NfcThrough
0x50MifareKeySetA0x80ExtBoardLed
0x51MifareAuthorizeA0x81ExtBoardLedRgb
0x52MifareRead0x82ExtBoardLedThinca
0x53MifareWrite0xF0ExtBoardInfo
0x54MifareKeySetB0xF2ExtFirmSum
0x55MifareAuthorizeB0xF3ExtSendHexData
0xF4ExtToBootMode
0xF5ExtToNormalMode

5.3 Response Codes

0x00 ok          0x01 cardError       0x02 noAccept
0x03 invalidCmd  0x04 invalidData     0x05 sumError
0x06 asicError   0x07 hexError        0x08 sendFin
0x10 isNewReader 0x20 isNewReader3    0xFF unknown

5.4 CardDetect Response Parsing

res[7] = cardNum
if cardNum == 1:
  res[8] = cardType    0x10 = FeliCa, 0x20 = ISO14443A
  res[9] = idLen
  FeliCa : IDm = res[10..18], PMm = res[18..26]
  ISO14A : UID = res[10..10+idLen]

5.5 Frame Example (StartPolling)

05 00 01 40 00
└LEN └ADDR └SEQ=1 └CMD=0x40 (start) └PLEN=0

Packed into HINATA main frame: 01 E0 05 00 01 40 00

6. PN532 Error Codes (Response First Byte)

0x00 None              0x01 Timeout            0x02 CRC
0x03 Parity            0x04 CollisionBitCount  0x05 MifareFraming
0x06 CollisionBitColl  0x07 NoBufs             0x09 RfNoBufs
0x0A ActiveTooSlow     0x0B RfProto            0x0D TooHot
0x0E InternalNoBufs    0x10 Inval              0x12 DepInvalidCmd
0x13 DepBadData        0x14 MifareAuth         0x18 NoSecure
0x19 I2cBusy           0x23 UidChecksum        0x25 DepState
0x26 HciInval          0x27 Context            0x29 Released
0x2A CardSwapped       0x2B NoCard             0x2C Mismatch
0x2D Overcurrent       0x2E NoNad

7. Connection / Send-Receive Complete Flow

mermaid
sequenceDiagram
    participant App
    participant HID as HID Channel
    participant Dev as HINATA
    participant PN as PN532

    App->>HID: enumerate(VID=0xF822)
    HID-->>App: device list (split by usage_page for read/write)
    App->>HID: open(read+write)
    App->>HID: write [01 01]                 (GetFirmwareTimestamp)
    HID->>Dev: HID OUT
    Dev-->>HID: HID IN [01][32][...]         (note header=0x32)
    HID-->>App: subscribers[0x32].fire

    App->>HID: write [01 E6]                 (GetChipId, fw≥2025051301)
    Dev-->>HID: [01 E6 chipId(4)]

    Note over App,PN: Search for FeliCa card
    App->>HID: write [01 E2 + InListPassiveTarget(brty=1, FeliCa initial)]
    Dev->>PN: forward
    PN-->>Dev: ACK
    Dev-->>HID: [E2 + ACK frame]            (subscription SpecificNotOn(4,0), discard)
    PN-->>Dev: response
    Dev-->>HID: [E2 + PN532 response]
    HID-->>App: parse IDm/PMm

7.1 Key Code Locations

FunctionRust ReferenceDart Reference
Device discoverybuilder.rs (find_devices_inner)hid_bridge_native.dart, hid_bridge_web.dart
I/O loopbuilder.rs (io_loop)hinata_device.dart (_onInputReport)
Subscription / dispatchmessage.rssubscription.dart
HINATA commandsdevice.rshinata_device.dart
PN532 frame codecpn532.rs (Pn532Packet)pn532.dart
Sega passthroughNot implemented (raw 0xE0 only)sega_protocol.dart
Windows COM port detectionutils/com.rs
Linux udev rules10-hinata.rules

7.2 Frame Encoding Pseudocode

rust
// HINATA main frame
fn send(cmd: u8, payload: &[u8]) {
    let mut frame = vec![0x01, cmd];      // ReportID + CMD
    frame.extend_from_slice(payload);
    hid_write(&frame);                     // 64B HID OUT
}

// PN532 frame (nested in cmd=0xE2 payload)
fn pn532_frame(cmd: u8, data: &[u8]) -> Vec<u8> {
    let len = (data.len() + 2) as u8;
    let lcs = (!len).wrapping_add(1);
    let tfi = 0xD4;
    let mut sum = tfi.wrapping_add(cmd);
    for &b in data { sum = sum.wrapping_add(b); }
    let dcs = (!sum).wrapping_add(1);

    let mut f = vec![0x00, 0x00, 0xFF, len, lcs, tfi, cmd];
    f.extend_from_slice(data);
    f.push(dcs);
    f.push(0x00);
    f
}

8. Quick Reference Summary

  • VID 0xF822, HID Report ID 0x01, 64-byte fixed-length frame.
  • Main frame structure: [0x01][CMD][PAYLOAD...].
  • Responses routed by first byte (except CMD 0x010x32; otherwise matches CMD).
  • 0xE2 forwards to PN532 (with ACK filtering); 0xE0 forwards to Sega subboard.
  • Platform differences exist only in HID endpoint discovery: Win/Linux dual interface (usage_page 1/6), macOS single interface (usage_page 6), Android quick_usb Bulk IN/OUT, Web uses navigator.hid.