Skip to content

Instantly share code, notes, and snippets.

@niw
Last active February 25, 2026 11:15
Show Gist options
  • Select an option

  • Save niw/3885b22d502bb1e145984d41568f202d to your computer and use it in GitHub Desktop.

Select an option

Save niw/3885b22d502bb1e145984d41568f202d to your computer and use it in GitHub Desktop.
400x300 4-Color e-Paper NFC Protocol

NFC で書き換えられる電子ペーパーのプロトコル

いま巷で話題の NFC で書き換えできる名札サイズの電子ペーパーのプロトコルを調べました。 @alt-coreさんの追加の調査も参考にしています。

実装

NFCの種類、通信方式と画面の種類

  • ISO 14443-A (Type A)
  • Application Identifier (AID): D2760000850101
  • ISO7816 APDU を使う
サイズ 色数 (ビット数) 解像度 色インデックス 画像データの方向
2.9-inch 2 (1-bit) 296 x 128 0x00: Black
0x01: White
90度回転
2.9-inch 4 (2-bit) 296 x 128 0x00: Black
0x01: White
0x02: Yellow
0x03: Red
90度回転
4.2-inch 2 (1-bit) 400 x 300 0x00: Black
0x01: White
左右反転
4.2-inch 4 (2-bit) 400 x 300 0x00: Black
0x01: White
0x02: Yellow
0x03: Red
そのまま

APDU コマンド

認証

送信

0x00, 0x20, 0x00, 0x01, 0x04, 0x20, 0x09, 0x12, 0x10

受信ステータス

  • 成功: 0x90, 0x00
  • 失敗: その他

デバイス情報取得

送信

0x00, 0xD1, 0x00, 0x00, 0x00

受信ステータス

  • 成功: 0x90, 0x00
  • 失敗: その他

受信データ

TLV形式 (タグ 1byte, データ長 1byte, データ…, の繰り返し)

Tag: 0xA0
Length: 7
Data: {
  unknown: UInt8,
  numberOfColors: UInt8,
  unknown: UInt8,
  heightInBits: UInt16 (Big-Endian)
  width: UInt16 (Big-Endian)
}

// numberOfColors
0x01, 0x47: 2-Color, 1bpp (bits per pixel)
0x07: 4-Color, 2bpp

// size
(width, heightInBits / bpp)

Tag: 0xA1
Length: 7
Data: {
  imageOrientation: UInt8
  unknown: UInt8[6]
}

// imageOrientation (未確定)
0x00: 90度回転
0x01: そのまま

Tag: 0xC0
Length: 10
Data: {
  name: Char[10]
}

Tag: 0xC1
Length: 4
Data: {
  uid: UInt32 (Big-Endian)
}

Warning

imageOrientation フィールドについては推定で不確定です。特に 2.9-inch 4色と 4.2-inch 2色については不明です。

画像データ送信

送信

0xF0, 0xD3, 0x00, P2, Lc, Data...

詳細は後述

受信ステータス

  • 成功: 0x90, 0x00
  • 失敗: その他

画面更新

送信

0xF0, 0xD4, 0x85, P2, 0x00

// P2
0x00: 画面更新を待って受信 (ブロック), 後述の画面更新の確認は不要
0x80: 画面更新を待たずに受信

ドキュメントには記載がないが、iOS の Core NFC はタグのコネクションが connect(to:) を呼んでから20秒に制限されている。ブロックせずに画面更新をポーリングする場合、20秒を越えることがあり、画面更新が完了せずに終了することがある。ブロックする場合は結果を受信するまで20秒を超えてコネクションが維持される。

受信ステータス

  • 成功: 0x90, 0x00
  • 失敗: その他

画面更新の確認

送信

0xF0, 0xDE, 0x00, 0x00, 0x01
  • 例えば 500ms 間隔でポーリングする

受信ステータス

  • 成功: 0x90, 0x00
  • 失敗: その他

受信データ

- 0x00: 更新完了
- 0x01: 更新中

最小限の更新手順

画面更新をコマンドで待つ場合

  1. 認証コマンド 0x00, 0x20, ... → 成功 0x90, 0x00
  2. 画像データ送信 0xF0, 0xD3, ... → 成功 0x90, 0x00 を複数回
  3. 画面更新 0xF0, 0xD4, 0x85, 0x00, 0x00 → 成功 0x90, 0x00

画面更新をポーリングする場合

  1. 認証コマンド 0x00, 0x20, ... → 成功 0x90, 0x00
  2. 画像データ送信 0xF0, 0xD3, ... → 成功 0x90, 0x00 を複数回
  3. 画面更新 0xF0, 0xD4, 0x85, 0x80, 0x00 → 成功 0x90, 0x00
  4. 画面更新の確認 0xF0, 0xDE, ... → 成功 0x90, 0x00, データが完了0x01 になるま繰り返す

画像データの形式と送信データの形式

画像データ

  • デバイス情報取得コマンドの結果を使うか、手動で選択して色数とサイズを決定。
  • 色インデックスを使ったビットマップデータ UInt8[width * height] を用意。
  • 2.9-inchのデバイスは画像データを90度回転する
  • 4.2-inch 4-Colorsのデバイスは画像データを左右反転する

横方向でパックする

画像データをそれぞれの行の右から左に 1-byte に 8-pixel (2-color) か 4-pixel (4-color) をパックする

// 画像それぞれの行の右から左に以下の色インデックスで並んでいる場合
p0, p1, p2, p3, p4, p5, p6, p7 ...
// 2色なら
byte = p0 | (p1 << 1) | (p2 << 2) | (p3 << 3) | (p4 << 4) | ...
// 4色なら
byte = p0 | (p1 << 2) | (p2 << 4) | (p3 << 6)

パックしたデータをブロックに分割する

  • パックしたデータを 2,000-byte ごとに分割してブロックにする。
  • 2.9-inch の場合、最後のブロックは 2,000-byte 未満になる。

ブロックを圧縮する

  • ブロックごとに LZO1X-1 (lzo1x_1_compress) で圧縮する

圧縮したブロックをフラグメントにする

  • 圧縮したブロックを 250-byte ごとに分割してフラグメントにする。
  • 最後のフラグメントは 250-byte 以下になる。

フラグメントごとに画像データ送信コマンドで送信する

画像データ送信コマンド 0xF0, 0xD3, ... を複数回使いフラグメントを送信する。

0xF0, 0xD3, 0x00, P2, Lc, Data...

// P2
- 0x00: ブロック途中のフラグメント
- 0x01: ブロック最後のフラグメント

// Lc
フラグメントの長さ + 2

// Data
{
  blockIndex: UInt8
  fragmentIndex: UInt8
  fragment: UInt8[]
}

Protocol for NFC-Rewritable e-Paper

This document describes the protocol of a name-badge-sized electronic paper device that can be rewritten via NFC. This document also based on @alt-core’s additional researh.

Implementations

NFC Type, Communication Method, and Display Variants

  • ISO 14443-A (Type A)
  • Application Identifier (AID): D2760000850101
  • ISO 7816 APDU is used
Size Number of Colors (Bit Depth) Resolution Color Index Image Orientation
2.9-inch 2 (1-bit) 296 × 128 0x00: Black
0x01: White
Rotated 90°
2.9-inch 4 (2-bit) 296 × 128 0x00: Black
0x01: White
0x02: Yellow
0x03: Red
Rotated 90°
4.2-inch 2 (1-bit) 400 × 300 0x00: Black
0x01: White
Horizontally flipped
4.2-inch 4 (2-bit) 400 × 300 0x00: Black
0x01: White
0x02: Yellow
0x03: Red
Normal

APDU Commands

Authentication

Command

0x00, 0x20, 0x00, 0x01, 0x04, 0x20, 0x09, 0x12, 0x10

Response Status

  • Success: 0x90, 0x00
  • Failure: Any other value

Get Device Information

Command

0x00, 0xD1, 0x00, 0x00, 0x00

Response Status

  • Success: 0x90, 0x00
  • Failure: Any other value

Response Data

TLV format (1-byte tag, 1-byte length, data…, repeated)

Tag: 0xA0
Length: 7
Data: {
  unknown: UInt8,
  numberOfColors: UInt8,
  unknown: UInt8,
  heightInBits: UInt16 (Big-Endian),
  width: UInt16 (Big-Endian)
}

// numberOfColors

- 0x01, 0x47: 2-color, 1 bpp (bits per pixel)
- 0x07: 4-color, 2 bpp

// Display Size

- (width, heightInBits / bpp)

Tag: 0xA1
Length: 7
Data: {
  imageOrientation: UInt8,
  unknown: UInt8[6]
}

// imageOrientation

- 0x00: Rotated 90°
- 0x01: Normal

Tag: 0xC0
Length: 10
Data: {
  name: Char[10]
}

Tag: 0xC1
Length: 4
Data: {
  uid: UInt32 (Big-Endian)
}

Warning

imageOrientation field is estimation, especially I didn’t see 2.9-inch 4-Color or 4.2-inch 2-Color devices.

Send Image Data

Command

0xF0, 0xD3, 0x00, P2, Lc, Data...

Details are described later.

Response Status

  • Success: 0x90, 0x00
  • Failure: Any other value

Refresh Display

Command

0xF0, 0xD4, 0x85, P2, 0x00

// P2
0x00: Wait for display refresh to complete (blocking); no need to confirm refresh status separately
0x80: Do not wait for display refresh (non-blocking)

Although not documented, iOS Core NFC limits the tag connection to 20 seconds after calling connect(to:). When using non-blocking mode and polling for refresh completion, the operation may exceed 20 seconds, potentially terminating before refresh completes. In blocking mode, the connection is maintained beyond 20 seconds until a response is received.

Response Status

  • Success: 0x90, 0x00
  • Failure: Any other value

Confirm Display Refresh

Command

0xF0, 0xDE, 0x00, 0x00, 0x01
  • For example, poll at 500 ms intervals.

Response Status

  • Success: 0x90, 0x00
  • Failure: Any other value

Response Data

0x00: Refresh complete
0x01: Refresh in progress

Minimal Update Procedure

When Waiting for Display Refresh (Blocking)

  1. Send authentication command 0x00, 0x20, ... → Success 0x90, 0x00
  2. Send image data 0xF0, 0xD3, ... → Success 0x90, 0x00 (multiple times)
  3. Refresh display 0xF0, 0xD4, 0x85, 0x00, 0x00 → Success 0x90, 0x00

When Polling for Display Refresh (Non-Blocking)

  1. Send authentication command 0x00, 0x20, ... → Success 0x90, 0x00
  2. Send image data 0xF0, 0xD3, ... → Success 0x90, 0x00 (multiple times)
  3. Refresh display 0xF0, 0xD4, 0x85, 0x80, 0x00 → Success 0x90, 0x00
  4. Confirm refresh 0xF0, 0xDE, ... → Success 0x90, 0x00; repeat until response data becomes 0x00

Image Data Format and Transmission Format

Image Data Preparation

  • Determine the number of colors and display size either from the Get Device Information command or by manual selection.
  • Prepare bitmap data using color indices: UInt8[width × height].
  • For 2.9-inch devices, rotate the image data by 90°.
  • For 4.2-inch 2-Color device, flip the image data horizonally.

Horizontal Packing

Pack the image data row by row, from right to left, into 1 byte per 8 pixels (2-color) or 4 pixels (4-color).

// If pixels in a row from right to left are:
p0, p1, p2, p3, p4, p5, p6, p7 ...

// For 2-color (1 bpp):
byte = p0 | (p1 << 1) | (p2 << 2) | (p3 << 3) | (p4 << 4) | ...

// For 4-color (2 bpp):
byte = p0 | (p1 << 2) | (p2 << 4) | (p3 << 6)

Split Packed Data into Blocks

  • Divide the packed data into 2,000-byte blocks.
  • For 2.9-inch devices, the final block is less than 2,000 bytes.

Compress Each Block

  • Compress each block using LZO1X-1 (lzo1x_1_compress).

Split Compressed Blocks into Fragments

  • Divide each compressed block into 250-byte fragments.
  • The final fragment may be less than 250 bytes.

Transmit Each Fragment Using the Image Data Command

Use the image data command 0xF0, 0xD3, ... multiple times to send fragments.

0xF0, 0xD3, 0x00, P2, Lc, Data...

// P2
0x00: Intermediate fragment of a block
0x01: Final fragment of a block

// Lc
Length of fragment + 2

// Data
{
  blockIndex: UInt8,
  fragmentIndex: UInt8,
  fragment: UInt8[]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment