Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

KenColle (県これ)

English | 日本語

Tap a Japanese Individual Number Card (マイナンバーカード) on a PC/SC reader and the matching prefecture fills in on a map of Japan. The name is short for 「県これくしょん」, a collection of prefectures.

  • A prefecture not yet seen → blown up on screen and filled in (NEW!)
  • One already filled → blown up and flagged as a duplicate (重複)
  • An expired card → 有効期限切れ blown up, and nothing is filled in (even for an already filled prefecture, this takes precedence over the duplicate message)

Every outcome has its own sound. Errors such as an unplugged reader or a failed read are shown on screen.

The user-facing interface is in Japanese; code comments are in English.

How it works

The app reads EF 0001 of the card's common card AP (AID D3921000310001010100).

SELECT DF (AID)  00 A4 04 0C 0A D3 92 10 00 31 00 01 01 01 00
SELECT EF (FID)  00 A4 02 0C 02 00 01
READ RECORD      00 B2 01 0C 00

The response is 01 1C (tag=0x01, len=28) followed by 28 ASCII digits, laid out from the right as:

... card serial | municipality code (5) | expiry date (8)

The prefecture code is the first two digits of the municipality code. This AP needs no PIN, so nobody has to type anything. A card stays valid through its expiry date and lapses the day after.

The municipality code is rewritten when the holder registers a move, so it names the municipality they currently live in.

Note

The app never touches the text-entry-assistance AP (券面事項入力補助AP) or the My Number itself.

Privacy

Only the prefecture code, the time it was first filled, and a hit count are stored. The card serial, which could identify a card uniquely, never leaves app/common_card_ap.py and is never logged. The expiry date is not sent to the browser either: it is never displayed, and it narrows down the holder's birth date.

State lives in a SQLite database with a single table, one row per prefecture. Reading it back needs the sqlite3 command-line tool, which is not otherwise required:

$ sudo apt install sqlite3
$ sqlite3 state.db 'SELECT * FROM filled ORDER BY filled_at'
13|2026-07-10T12:44:07+09:00|2
27|2026-07-10T12:51:33+09:00|1

Setup

You need pcscd and a CCID driver that covers your reader.

sudo apt install pcscd pcsc-tools
pcsc_scan -r          # check that the reader shows up

uv sync               # add --no-dev to leave the test dependencies out

Dependencies live in pyproject.toml and are pinned in uv.lock, which is committed: uv sync reproduces the exact environment. Without uv, pip install -e . --group dev works too, but resolves fresh rather than from the lock.

Running

uv run kencolle                      # real reader
uv run kencolle --fake --port 8000   # try the UI with no card

uv run python -m app does the same thing.

Open http://127.0.0.1:8000/ and go full screen. One click starts it: browsers will not play audio before a user gesture, so that click is what enables the sounds.

  • r — reset every filled prefecture
  • m — mute or unmute the sounds
  • --state PATH — where to keep the state database (defaults to state.db)

Under --fake you can drive the card and the reader from outside.

curl -X POST 'http://127.0.0.1:8000/api/debug/tap/13'               # read Tokyo
curl -X POST 'http://127.0.0.1:8000/api/debug/tap/13?expired=true'  # an expired card
curl -X POST 'http://127.0.0.1:8000/api/debug/reader/false'         # reader unplugged

Sounds

Synthesised with Web Audio, with no external assets (app/static/sounds.js).

Outcome Sound
new A rising arpeggio (C5 E5 G5 C6)
dup The same pitch twice over
expired A falling arpeggio (B4 G#4 F4)
error A dirty buzzer. Also fires when the reader is unplugged

If nothing is audible, check Sounds.state() in the browser console. uninitialized means the page has not been clicked yet; suspended means the browser is holding playback back.

Layout

File Role
app/common_card_ap.py APDU exchange, record parsing, expiry check
app/reader.py The PC/SC polling loop, on its own thread
app/prefectures.py Prefecture codes and names
app/map_shapes.py The outline, label anchor and type size of each prefecture
app/store.py Persistence to the state.db SQLite database
app/clock.py Japan Standard Time
app/main.py FastAPI and the WebSocket
app/static/ The deformed map UI and the sounds

Card detection

disconnect() defaults to the SCARD_UNPOWER_CARD disposition, which cuts power to a contactless card on every disconnect. That makes the presence check immediately afterwards flaky, and a single tap gets read twice, so the app uses SCARD_LEAVE_CARD instead. On top of that it will not read again until it has observed "no card" three times in a row (ABSENT_CONFIRM). A card already resting on the reader at startup is ignored until it is lifted, and unplugging the reader disarms the loop too, so the card has to be presented again.

tests/test_reader.py pins these transitions down without touching PC/SC.

Tests

uv run pytest -q

Verified on

  • Ubuntu, pcsc-lite 2.4.1, the ccid driver
  • SONY FeliCa RC-S300/P (PaSoRi 4.0)
  • Python 3.14

About

Tap a Japanese Individual Number Card (マイナンバーカード) on a PC/SC reader and the matching prefecture fills in on a map of Japan.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages