ミッキーマジックペン(2013年仕様)の教材データを分析する

World Familyのディズニー英語システムの教材として販売されているミッキーマジックペンについて、2013年から2016年に販売された背面ボタンが1つのタイプを分析した。
このタイプは保証が切れているものの、良質な中古が大量かつ安価で手に入る上、修理が容易である。

私はこの分析結果を活用することによって、独自の教材ファイルを定義し、レーザープリンタで印刷したドットコードによって動作させることに成功した。

## 留意事項

年代によってペンの仕様は変更されている。
本分析結果は2013年仕様のものである。

最古の2ボタンのタイプではまた少々仕様が異なるため適用できない。また、最新版(2017年以降版)ではストレージとして見えない領域に教材ファイルが配置されているため、本手法は適用できない。ドットコードの仕様も2017年を境に変更されていることにも注意が必要である。

参考 : [ミニーマジックペン　年代による違い](https://www.xn--dck3bubs9v0906b9hl.biz/mickeymagicpen.html)

また、本仕様についてWorld Family社やSonix社に問い合わせることは避けていただきたい。

## 基本動作

ミッキーマジックペンは、USB mini B端子によって充電するものであるが、PCと接続すると(若干の遅延のあと)ストレージとして見える。
BOOKフォルダには各教材に対応するファイルが置かれている。ファイルをここからなくすと教材に反応しなくなることから、このファイルを直接利用して動作していることがわかる。

## 教材データファイル

具体的に置かれている教材データファイルは下記の通り。

- BOOK/AC_20130426.0BFC.OU
- BOOK/FAA1_20130426.0BF1.OU
- BOOK/FAA2_20130426.0BF2.OU
- BOOK/FAA3_20130426.0BF3.OU
- BOOK/FAA4_20130426.0BF4.OU
- BOOK/FAG1_20130426.0BF5.OU
- BOOK/FAG2_20130426.0BF6.OU
- BOOK/FAG3_20130426.0BF7.OU
- BOOK/FAG4_20130426.0BF8.OU
- BOOK/FWW1_20130426.0BEB.OU
- BOOK/FWW2_20130426.0BEC.OU
- BOOK/FWW3_20130426.0BED.OU
- BOOK/FWW4_20130426.0BEE.OU
- BOOK/GC_20130426.0BF0.OU
- BOOK/MBB_20130426.0BEF.OU

それぞれ下記の教材に対応している。

- AC : Activity Cards
- FAA : Fun and Adventure
- FAG : Fun and Games
- FWW : Fun with Words
- GC : Game Cards
- MBB : My Big Book of Words

これらのバイナリファイルを眺めてみると、MP3ファイルのヘッダが繰り返されている。これは結論から述べると、読み込み専用のファイルシステムのイメージファイルである。

## 教材データファイル仕様

ファイル名およびバイナリファイルの先頭に記載されたコード番号のドットコードが読み取られると、ファイルシステムに格納されている外挿プログラムが読み出され、教材に特化したドットコードの待受状態に推移する。

具体的な仕様は下記の通り。

### ファイル名

`[任意のファイル名].[16進表記の教材ドットコード番号].OU`

### ファイル構造

組み込み機器らしく、古典的かつ素直なバイナリファイルである。
このバイナリファイルを直接デバイスのアドレスにマップし、ファイルシステム様のインデックスとして振る舞っていると考えられる。最小限のメモリ構成で任意の長さの音声を再生できるよう工夫されている。

教材選定に伴う初期化に必要な情報を含んだヘッダ部から始まり、固定長インデックスを起点にファイル名→ファイル実態とたどる構成となっている。

実物との対応を理解しやすいように先頭部分を掲載する。

```bash
$ hexdump -C FAA1_20130426.0BF1.OU | head 
00000000  4f 55 45 00 f1 0b 00 00  c3 0b 00 00 da f9 09 00  |OUE.............|
00000010  3c 8d 00 00 19 00 00 00  b4 2b 0a 00 55 8d 00 00  |<........+..U...|
00000020  1b 00 00 00 6c 9e 14 00  70 8d 00 00 19 00 00 00  |....l...p.......|
00000030  c7 25 38 00 89 8d 00 00  1a 00 00 00 b7 98 39 00  |.%8...........9.|
00000040  a3 8d 00 00 14 00 00 00  55 cf 57 00 b7 8d 00 00  |........U.W.....|
00000050  18 00 00 00 1d d5 63 00  cf 8d 00 00 18 00 00 00  |......c.........|
00000060  44 aa 6f 00 e7 8d 00 00  1a 00 00 00 b4 b4 7d 00  |D.o...........}.|
00000070  01 8e 00 00 19 00 00 00  09 7f 8a 00 1a 8e 00 00  |................|
00000080  19 00 00 00 24 88 c5 00  33 8e 00 00 18 00 00 00  |....$...3.......|
00000090  11 b8 c5 00 4b 8e 00 00  18 00 00 00 ee 0c ca 00  |....K...........|
```

1. ヘッダ部 (12バイト)
    - `4f 55 45 00`
        - 決め打ちヘッダ
    - `f1 0b 00 00`
        - 教材番号
        - `0x0bf1` == 3057番
        - ファイル名と一致する必要あり
    - `c3 0b 00 00`
        - ファイルシステムに格納されているファイルエントリ数
        - `0x0bc3` == 3011個
        - 音声ファイルや外挿プログラム等
2. ファイル名インデックス部 (固定長)
    - 12バイト単位でファイルエントリ数分繰り返し
        - `da f9 09 00`
            - ファイル名から算出される探索用ハッシュ (後述)
            - `0x09f9da` == 653786
        - `3c 8d 00 00`
            - ファイル名および実態へのポインタが格納されている先頭バイト数
            - `0x00008d3c` == 36156バイト
        - `19 00 00 00`
            - `0x00000019` == 25バイト
        - ...繰り返し
3. 区切り12バイト
    - 無視されるのでなんでもよい模様
4. ファイル名および実態へのポインタ定義部 (可変長)
    - (４バイト + 4バイト + ファイル名のバイト数)単位で繰り返し
        - `b0 39 02 00`
            - ファイル実態へのポインタ (ファイルからの先頭バイト数)
            - `0x000239b0` == 145840バイト
        - `a8 98 00 00`
            - ファイル実態の長さ
            - `0x000098a8` == 39080バイト
        - `4c 45 58 31 50 32 32 5f 31 30 39 34 5f 41 2e 6d 70 33 00`
            - ファイル名
            - `LEX1P22_1094_A.mp3\0` (Null文字を含む)
5. ファイル実態
    - ファイル数分繰り返し 
        - ファイルのバイナリべた書き

### ファイル名から算出される探索用ハッシュ

仕様は公開されていないが、UART端子をつないでダンプしたファームウェア `system.run` を解析すると、ファイル名から固定長のハッシュ値を求める関数が見つかった。ChatGPTの助けを借りてPythonで実装すると下記のようであった。

```python
def calculate_hash(target_string):
    hash = 0
    for char in target_string:
        index = hash >> 24
        hash_value = hash_table[index]
        next_hash = (ord(char) | (hash << 8)) & 0xFFFFFFFF
        next_hash ^= hash_value
        hash = next_hash
    return hash
```

なお、`hash_table`は標準的なCRC32のハッシュテーブルである。下記によって求められる。

```python
POLYNOMIAL = 0xEDB88320
hash_table = []

for i in range(256):
    crc = i
    for j in range(8):
        if crc & 1:
            crc = (crc >> 1) ^ POLYNOMIAL
        else:
            crc >>= 1
    hash_table.append(crc)
```

## ドットコード

このペンには[台湾Sonix社](https://www.sonix.com.tw/)の[SN9P701](https://www.datasheet4u.com/share_search.php?sWord=SN9P701-00X)が用いられている。
仕様の詳細はチップを購入した開発者にだけ公開されている模様で、公式の資料は見つけられなかった。

しかしながら、同種のチップを用いる[ドイツのTip Toiの解析プロジェクト tip-toi-reveng](https://github.com/entropia/tip-toi-reveng)の解析結果を流用可能である。
教材と同等のドットコードはコマンド `tttool oid-code [番号]` で生成可能であった。

[コードの仕様の解説](https://github.com/entropia/tip-toi-reveng/wiki/PEN-Optical-ID-and-codes)には、ビットパターンやJavaScriptベースのサンプルも存在する。
なお、ドットの並びから計算される番号と実際の番号は意図的にずれたものになっている。解析や教材のコピーを困難にさせるためだと考えられる。上記プロジェクトでは[ずれを補正するテーブルを力技で実装](https://github.com/entropia/tip-toi-reveng/blob/master/src/KnownCodes.hs)している。

印刷にはレーザープリンタが必要であった。インクジェットプリンタではドットを正確に打てないためにペンでの読み取りが困難であった。

## 教材自作

著作権の問題があるため、活用中のデータは公開不能である。
`FWW1_20130426.0BEB` の仕様が、テキストファイルにドットコードの番号と対応するMP3ファイルを列挙すればよいものであったため、こちらを流用して独自の音声ファイルを注入した。

## 最新版(2017年以降版)への対応は厳しい

半年程度片手間で解析したが、3つの理由で難しい。

- ドットコードの仕様が解析されていない
    - 旧来の正方形のパターンから、縦長平行四辺形に変更された
    - コードの空間が16bitから大きく広がっているため法則が見つけられていない
    - ドットの法則が見つかったとしても、確実に存在するであろう意図的なずれの補正調査がしんどい
- 教材データの置き場所が不可視領域に移動したため試行錯誤がしにくい
    - 非破壊的なハックはできる見込みだが、都度UARTでつないで書き換える必要がある
- ペンが高価である

## 執筆後記

全般的に試行錯誤や経緯は述べず、結論だけ書いたが、実際には相当な試行錯誤が必要であった。

分解するとUART端子が見えたため、シリアルケーブルを接続してファームウェアのデバッグ入出力を観察することでドットコードを読んだときの振る舞いを直接知ることができた。また、ファームウェアのダンプにも成功した。
ここで得られたバイナリやアセンブリの断片をChatGPTに問い合わせたことによって劇的に調査が進んだ。ChatGPTすげぇ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60150/10eda5a0-92af-1f26-3b69-94561f0a158a.png)


この解析結果を経た結果、どんな本でもどんな音でも鳴らせるようになるため、家族は大喜びである。