Blogger の旧ブログをデザインそのまま残して、同じドメインで新ブログを始める

この作業は Claude(Anthropic の AI)とやりとりしながら進めた。 コマンドとスクリプトはほぼ書いてもらっている。どう残したいかを決めたのと、画面を見て確かめたのは自分。

ブログを作り替えたい

このブログは元々「トンネルポエマー」という名前で、2017 年から 2023 年まで、撮った電車の写真に一言添えるだけのブログだった。全部で 425 記事。サブタイトルは「怪文書」。

しばらく放置していたが、最近電子工作をやるようになって、作ったものの記録を残す場所が欲しくなった。

別のブログを新しく作ってもよかったが、このドメインは手放したくない。使わなくなったドメインは、いずれ期限が切れて誰かに取られ、全然関係ないサイトになってしまうことがある。

かといって、昔の記事を消すのも嫌だった。

  • 自分がトンネルポエマーを気に入っていて、今でもときどき見返している
  • 当時、気に入ってくれていた人がいた
  • コミケで頒布したトンネルポエマー本に、このブログのリンクを載せている

本を手に取った人がいつかリンクを開いたときに、ちゃんとトンネルポエマーにたどり着けるようにしておきたい。

だから同じドメインで新しいブログを始めつつ、昔の記事はデザインも含めてそのまま残すことにした。

URL を壊したくない

一番簡単なのは、今のブログの記事を全部消して書き始めることだ。でもそうすると、昔の記事の URL(/2023/03/ktk.html みたいなもの)が全部 404 になる。どこかに貼られているリンクも、検索結果も、本に載せたリンクの先も死ぬ。

そこでこういう形にした。

www.tunnelpoemer.com   → Blogger の新しいブログ(トンネル備忘録)
old.tunnelpoemer.com   → 旧ブログの静的コピー(GitHub Pages)

www の昔の記事 URL に来た人は、old の同じ URL に転送する

旧ブログはただの HTML ファイルの束にして、GitHub Pages に置く。サーバーもデータベースも要らないので、一度置けばほぼ永久に残る。

丸ごと落とす

Blogger には記事を XML で書き出すバックアップ機能がある。最初はそれで済むと思ったが、中身は本文とタイトルと日付だけだった。デザインは入っていないし、画像も URL が書いてあるだけで実体はない。Blogger に読み込み直して初めて記事に戻る形式だ。

「見た目ごと残す」なら、ブラウザで見ている状態をそのまま保存するしかない。wget でサイトをミラーする。

まず記事の URL 一覧を作る。Blogger のフィードから取れるのだが、1 回に 150 件までしか返ってこない。425 件あるので、開始位置をずらして 3 回取る。

for i in $(seq 1 150 601); do
  curl -s "https://www.tunnelpoemer.com/feeds/posts/summary?alt=json&max-results=150&start-index=$i" \
   | jq -r '.feed.entry[]?.link[]|select(.rel=="alternate").href'
  sleep 1
done | sort -u > urls.txt

サイドバーの年別件数を足すと 425。一致した。

あとはこの一覧を wget に渡す。-p で画像や CSS も一緒に取り、-k でリンクを手元のファイルを指すように書き換え、-H で画像サーバー(blogger.googleusercontent.com)にも取りに行かせる。

作業は Ubuntu の PC に SSH で入ってやっていたのだが、30 分くらいかかる処理の途中でSSH が切れて、wget ごと死んだ。

tmux の中で動かし直した。接続が切れても処理は続くので、長いコマンドはこれで回すのが正解だった。

結果、記事 425 件、画像 851 ファイル(337MB)。サムネイルだけでなく、クリックで開く原寸の画像まで揃った。

手元で見てみる

保存したフォルダで Python の簡易サーバーを立てる。

python3 -m http.server 8000

Ubuntu の PC には Tailscale を入れてあるので、iPhone からそのアドレスを開けば外出先でも確認できる。

本文も写真もちゃんと出た。でも背景の空の写真がない。

原因は自分たちだった。wget のログに壊れた URL が出ていて、それを避けるためにテーマ画像のサーバー(themes.googleusercontent.com)を丸ごと除外していた。背景画像もそこにあった。CSS から URL を拾って取り直したら戻った。

「ホーム」や年別アーカイブのページ(/2019/ など)は Blogger がその場で作っているページなので、静的なコピーでは再現できない。代わりに 425 件全部の目次ページを作った。

スマホ版がない

Blogger はスマホで開くと、同じ URL の後ろに ?m=1 を付けて別のデザインを出している。記事が縦に並んで、スマホだと読みやすかった。

静的なコピーだとこれがなくなって、スマホでも PC 版の小さい画面が出る。

なので、保存した 425 記事から日付・タイトル・サムネイル・本文を抜き出して、スマホ版風のトップページを作り直した。スマホ版の記事ページも別に落として置いてある。

最初はサムネイルを押すと PC 版の記事に飛んでいたので、その場で写真が拡大表示されるように変えた。外部のライブラリは使わず、ページの中に直接書いてある。

Blogger に頼らない

旧ブログは最終的に非公開にするつもりだった。そうなると、コピーの中に Blogger 側の画像を読みに行っているところが残っていたら困る。

調べると、本文の画像は全部手元のファイルを指していた。残っていたのは SNS でリンクを貼ったときに出るプレビュー画像(og:image)の指定だけ。表示には関係ないが、どうせならこれも手元に持つことにした。

プレビュー用の 1200×630 の画像を 424 枚ダウンロードして(66MB)、指定を old.tunnelpoemer.com のものに書き換えた。原寸の写真を指す手もあったが、10MB を超えるものがあって SNS によっては大きすぎて出ない。

これでコピーは完全に自分のファイルだけで完結した。

old.tunnelpoemer.com で公開

GitHub にリポジトリを作って push し、GitHub Pages で公開した。公開リポジトリなら費用はかからない。サイト全体の上限が 1GB で、今回は画像込みで余裕で収まった。

独自ドメインを使うには、DNS に 1 行足す。

種類: CNAME   名前: old   データ: (GitHubのユーザー名).github.io

ここで少し迷った。ドメインは昔 Blogger の画面から買ったものだが、DNS の設定は Blogger にはない。Blogger から買ったドメインは Google Domains で登録されていて、今はその事業を引き継いだ Squarespace で管理されている。同じ Google アカウントでログインしたら、ちゃんとあった。

名前の欄は old だけ。old.tunnelpoemer.com と全部書くと二重になる。

既にある www と @ のレコードは今のブログ用なので触らない。

途中で GitHub のユーザー名を変えたくなって変えた。リポジトリの URL は自動で転送されるが、github.io の URL は転送されない。さっきの CNAME も書き換えが必要になる。やるなら公開前にやった方がいい。

ドメインを新ブログへ

Blogger で新しいブログを作って体裁を整えたら、ドメインの付け替え。

  1. 旧ブログの 設定 → 公開 → カスタムドメイン を削除
  2. 新ブログの 設定 → 公開 → カスタムドメイン に www.tunnelpoemer.com を入れる
  3. 「カスタム ドメインにリダイレクトする」をオン

1 と 2 の間はどちらのブログにもドメインが付いていないので、続けてやる。

直後に開いたら、「安全な接続ができませんでした」が出た。

設定の問題ではなく、新ブログ用の HTTPS の証明書がまだ発行されていないだけだった。待っていたら表示された。

次に、昔の記事の URL に来た人を old へ転送する。新ブログのテーマの HTML を編集して、<head> の直後にこれを入れた。

<b:if cond='data:view.isError'>
<script>location.replace('https://old.tunnelpoemer.com' + location.pathname + location.search);</script>
</b:if>

新ブログに存在しないページ、つまり 404 になるページを開いたら、old の同じパスに飛ばす。昔の記事の URL は新ブログには存在しないので、全部 old に流れる。

ドメインを付け替える前に、新ブログの blogspot.com のアドレスで試しておける。(新ブログ).blogspot.com/2023/03/ktk.html を開いて old の記事に飛べば OK。

入口を置く

最後に、新ブログのトップに旧ブログへのリンクを置いた。

HTML/JavaScript のガジェットを置いたら、PC では出たがスマホでは出なかった。このテーマはスマホ用の表示が別になっていて、ガジェットごとに「スマホでも出す」印が付いていないと表示されない。テーマの HTML でガジェットの行を探して、mobile='yes' を書き足した。

<b:widget id='HTML1' ... type='HTML' mobile='yes' version='1' ...>

最初は白い箱で大きく出して浮いていたので、テーマの色に合わせた小さい枠付きのリンクにした。

旧ブログは「投稿者のみ」に切り替えて非公開にする。コピーはもう Blogger 側に何も頼っていないので、閉じても old は壊れない。削除はせずに、元データの保険として置いておく。

完成

旧トンネルポエマーは、デザインそのままで old.tunnelpoemer.com に残った。昔のリンクから来ても、ちゃんとそこに着く。

そしてこのブログ、トンネル備忘録が始まった。

6 年分の写真を 1 枚ずつ見返すことになったのは想定外だった。怪文書は怪文書のまま置いておく。

この記事について

一連の作業は Claude(Anthropic の AI)とやりとりしながら進めた。

wget やフィードのコマンド、目次ページやスマホ版トップの生成、写真の拡大表示、og:image の取り直し、GitHub Pages への公開の段取りは、ほぼ Claude に書いてもらった。フィードが 150 件で切れること、XML のバックアップでは画像もデザインも残らないこと、Blogger から買ったドメインが Squarespace にあることを教えてくれたのも Claude だ。

一方、どう残したいかを決めたのは自分だ。デザインごと残す、URL は壊さない、スマホ版のトップも欲しい、写真は拡大で見たい。iPhone で開いて「背景がない」「画像を押すと PC 版に飛ぶ」「スマホにリンクが出ない」「不恰好」と気付いたのも、DNS や Blogger の設定画面を操作したのも自分。

間違いもあった。背景が抜けたのは、Claude が勧めた wget の除外設定のせいだった。スマホに出ないガジェットを最初に勧めてきたり、新ブログのテーマを取り違えて「HTML に id='main' があるはず」と言ってきたりもした。こちらも、コマンドの見本に書いてあった「リポジトリ名」をそのまま貼り付けて設定したりしている。

結局、画面を見て「ここがおかしい」と言える人間がいないと先に進まない。そういう分担だった。

PS1 の電車でGO!コントローラを 4 つのエミュレータで使えるようにする

エミュレータを使う前に

BIOS もゲームの ISO も、必ず自分のPS1・PS2・64・Dreamcast本体・自分のディスク・カセットから用意すること。
ネットで拾ってくるのは論外。ダウンロードも配布も違法。
エミュレータ本体(DuckStation / PCSX2 / Flycast / ares)自体は合法で公式配布されている。問題になるのは常にデータの出どころのほう。

なお自分の持ち物から吸い出す場合も、日本の著作権法には技術的保護手段の回避に関する規定がある。 適法かどうかは各自で確認を。この記事は法的な保証をするものではない。
この作業は Claude(Anthropic の AI)とやりとりしながら進めた。 コードはほぼ書いてもらっている。実機での計測と検証は自分。
PS1 の電車でGO!コントローラでエミュを運転しているところ

ノッチが飛ぶ

PS1 の電車でGO!用コントローラ、SLPH-00051。二ハンドルのやつだ。

SLPH-00051 と PS1/PS2-USBコンバーター

これを零環さんの PS1/PS2-USBコンバーター [LAY-002-01] 経由で PC に挿して、DuckStation で遊んでいた。

動かした。

マスコンの P2 が飛ぶ。

ブレーキもいくつか飛ぶ。PCSX2 でも同じだった。最初はスティックの閾値の問題だと思って調整したが、どう弄っても直らない。これはエミュレータ側の問題じゃないか、というのが出発点だ。

プラグインでは直せなかった

エミュ側の問題なら、入力プラグインを書いて直せばいい。そう思って調べたら、道が塞がっていた。

DuckStation はそもそもプラグイン方式を否定して作られた統合型。PCSX2 も 1.7 以降でプラグイン機構を廃止している。どちらにも入力プラグインの API が存在しない。

エミュを触れないなら、手前のコンバーター側で直すしかない。幸い LAY-002-01 の中身は ProMicro 互換で、ファームを自分で書き換えられる。

電GOコントローラの中身

MarcRiera/ddgo-controller-docs に書いてある。要点は3つ。

普通のデジタルパッドとまったく同じプロトコルで喋っている。 特別な通信は何もしていない。

ハンドルはボタンの組み合わせで段を表す。 力行が3ビット、ブレーキが4ビット。

十字キーの上と下が常時押しっぱなし。 ゲームはこの「普通はありえない組み合わせ」で電GOコントローラだと見分けている。

つまり軸はどこにも出てこない。閾値もデッドゾーンも本来関係ない。

それなのに段が飛ぶのは、コンバーターやエミュのどこかで、ボタンの組み合わせを軸やハットスイッチに変換していて、そこで情報が落ちているからだ。HID のハットスイッチは上と下を同時に表現できない。識別用の上下同時押しが、まずここで消える。

なら、16 ビットをそのまま 16 個の独立したボタンとして PC に流せばいい。解釈する層がなくなれば、段は飛びようがない。

Windows のゲームコントローラー画面(joy.cpl)を見ながら、全部の段を一段ずつ読んだ。

joy.cpl でボタンの点灯を確認しているところ

常時点灯の 5 と 7(上下)を除くとこうなる。

マスコン  点灯          ブレーキ  点灯
N         6, 8          解除      9, 10, 12
P1        6, 13         B1        10, 11, 12
P2        6             B2        10, 12
P3        8, 13         B3        9, 10, 11
P4        8             B4        9, 10
P5        13            B5        10, 11
                        B6        10
                        B7        9, 11, 12
                        B8        9, 12
                        非常      全部消灯

15 状態、全部違う組み合わせ。重複はひとつもない。

飛んでいた P2 も、ここではちゃんと「6 だけ」という固有の値を出している。コントローラもコンバーターも正常。犯人はエミュ側の変換で確定した。

ちなみに非常の位置で値がパラパラ変わるのは、汽車でGO! の自動帯のせいだ。ハンドルの不調ではない。

ファームを書く

やることは単純で、PS1 から来た 16 ビットを、軸もハットも通さずに 16 個の HID ボタンへ順番に流すだけ。

ベースは零環さんが公開しているスケッチ(GPLv3)。ATT ピンと SPI の構成をそのまま引き継いだので、配線は触っていない。

困ったのは割り当て画面だ。上下が常時押されているので、エミュの割り当て画面で「ボタンを押してください」と出た瞬間に、上か下が勝手に拾われる。他のボタンが一切割り当てられない。

そこで、SELECT と START を押しながら挿すと上下だけ出力を止めるモードを入れた。LED が赤く光ったらこのモード。

PS1 用のワンハンドル機 TCPP-20001 も試した。

点滅する。

ハンドルを動かすと一瞬ボタンが光るだけで、安定しない。PsxNewLib で TCPP-20001 が動かないという報告は前からあるが、原因は書かれていない。

観察すると、止めた位置で同じ組み合わせが点いたり消えたりしていた。読み取りが時々失敗して、そのたびに全ボタンが離されている。

なので、読み取りに失敗した回は前の値を保持するようにした。100 回連続で失敗したときだけ、本当に外れたと見なす。

止まった。

ビット配置は SLPH-00051 と完全に同じだった。ワンハンドルなのに二ハンドルと同じ表で読める。ファームも設定も共用できる。

書き込めない

本筋ではないが、一番時間を食ったのはここだ。

Switch 向けに作った Zuiki マスコン用のボード定義を選んだまま書き込もうとしていた。あれは CDC を切ってあるので Serial が存在せず、リンクで落ちる。

しかも CDC を切ったファームが載っている間は、COM ポートが現れない。IDE が自動リセットをかける相手がいない。リセットボタンを押せばブートローダーが 8 秒だけ起動するが、手動では間に合わなかった。

結局、PowerShell でポートの出現を 50ms ごとに見張って、出た瞬間に avrdude を叩いた。

while ($true) {
  if ([System.IO.Ports.SerialPort]::getportnames() -contains "COM9") {
    & "$a\bin\avrdude.exe" -C"$a\etc\avrdude.conf" -patmega32u4 -cavr109 `
      -PCOM9 -b57600 -D -Uflash:w:"$hex":i
    break
  }
  Start-Sleep -Milliseconds 50
}

もうひとつ引っかかったのが COM 番号。Pro Micro は、スケッチが動いているときとブートローダーのときで COM 番号が違う。

IDE で選ぶのはスケッチが動いているほう。そっちを選んでおけば、IDE が勝手にリセットをかけて書き込んでくれる。ブートローダー側の番号を選んでいたから「開けない」と言われていた。

CDC ありのボード定義(SparkFun Pro Micro / 3.3V 8MHz)で焼き直してからは、普通の Arduino と同じ感覚で書き込めるようになった。

DuckStation

パッドをデジタルコントローラーにして、16 個を 1:1 で割り当てる。

段は複数ボタンが同時に点くので、GUI で割り当てるのは難しい。設定ファイルに直接書いた。

[Pad1]
Type = DigitalController
Select = SDL-0/Button0
L3 = SDL-0/Button1
R3 = SDL-0/Button2
Start = SDL-0/Button3
Up = SDL-0/Button4
Right = SDL-0/Button5
Down = SDL-0/Button6
Left = SDL-0/Button7
L2 = SDL-0/Button8
R2 = SDL-0/Button9
L1 = SDL-0/Button10
R1 = SDL-0/Button11
Triangle = SDL-0/Button12
Circle = SDL-0/Button13
Cross = SDL-0/Button14
Square = SDL-0/Button15

ハマったのは設定ファイルの場所。新しめの DuckStation は %LOCALAPPDATA%\DuckStation に保存している。ドキュメント側にも古い settings.ini が残っていて、そっちを何度書き換えても画面に反映されなかった。

DuckStation で電車でGO!をプレイしている画面

P2 が飛ばなくなった。最初の見立てはこれで裏付けが取れた。

PCSX2

電車でGO!3 は PS1 のコントローラが使える。同じやり方で行けると思ったら、ボタンは効くのにハンドルだけ効かない。

PCSX2 のパッドは DualShock2 しか選べない。本物の SLPH-00051 は DualShock2 向けの設定コマンドに応答しないので、ゲームはそこで「これは電GOコントローラだ」と判断しているようだ。エミュの DualShock2 は応答してしまうので、普通のパッド扱いになる。

そこで方針を変えて、USB の「Train Controller(Type 2)」として見せることにした。

ファームに X 軸 = 力行、Y 軸 = ブレーキの出力を足す。値は、PCSX2 が Type 2 として段に区切る範囲のちょうど真ん中に置いた。16 ボタンの出力はそのまま残してあるので、DuckStation の設定には響かない。

コントローラー端子 1   未接続
USB ポート 1           Train Controller / Type 2
Axes Passthrough       オフ

ついでに2つ足した。

START だけ押しながら挿すと、ボタンを出さず軸だけ出す。 軸を割り当てるときに、段ごとボタンが拾われないようにするためだ。LED が青になる。

入力プロファイルを分ける。 電GO3 にだけ当てておけば、他のゲームには影響しない。

ひとつ引っかかったのは書式。PCSX2 は生のボタンを SDL-0/JoyButton13 と書く。DuckStation と同じ Button13 で書くと、閉じたときに勝手に別の名前へ書き換えられてしまう。

PCSX2 で電車でGO!3 をプレイしている画面

Flycast

次は DC 版の電車でGO!2。

Flycast には「電車でGO!」コントローラがちゃんと実装されている。ソースを見ると、ボタンのビットをそのままゲームに渡す作りだった。

PS1 と DC の電GOコントローラは、同じビットの組み合わせを、機種ごとのボタン名に載せているだけ。なので並べ替えれば済む。軸の変換はいらない。

役割                  ファームの番号     DC のボタン
SELECT                0                  D
START                 3                  Start
A / B / C             15 / 14 / 13       A / B / C
力行 1 / 2 / 3        12 / 7 / 5         Z / Y / X
ブレーキ 1 / 2 / 3 / 4  10 / 8 / 11 / 9   上 / 下 / 左 / 右

常時押しの上下(4 と 6)は割り当てない。DC ではブレーキが十字キーに載っているので、割り当てるとブレーキが壊れる。

[digital]
bind0 = 0:btn_d
bind1 = 3:btn_start
bind2 = 15:btn_a
bind3 = 14:btn_b
bind4 = 13:btn_c
bind5 = 12:btn_z
bind6 = 7:btn_y
bind7 = 5:btn_x
bind8 = 10:btn_dpad1_up
bind9 = 8:btn_dpad1_down
bind10 = 11:btn_dpad1_left
bind11 = 9:btn_dpad1_right

[analog] は空にしておく。PCSX2 用に足した軸が、アナログスティックとして拾われてしまうからだ。

困ったのはメニュー。ブレーキを解除に入れていると、十字キーの下・左・右が押しっぱなし扱いになって、Flycast のメニューでカーソルが流れ続ける。

メニューを触るときはブレーキを非常に入れておく。非常は 4 ビット全部オフなので、十字キーが何も押されていない状態になる。

Flycast で電車でGO!2 をプレイしている画面

ares

最後に N64 の電車でGO!64。

ares に電GO専用のコントローラはない。ただ N64 版もボタンの組み合わせ方式なので、普通のゲームパッドとして並べ替えればいい。

気になったのは、ares が十字キーの上下・左右の同時押しを打ち消す作りになっていること。調べると C ボタンは打ち消していない。N64 版はブレーキが C ボタン、力行が十字キーの右・上と Z なので、反対方向を同時に押す場面がない。問題なかった。

役割                  ファームの番号     N64 のボタン
SELECT                0                  R
START                 3                  Start
A / B / C             15 / 14 / 13       B / A / L
力行 1 / 2 / 3        12 / 7 / 5         十字右 / 十字上 / Z
ブレーキ 1 / 2 / 3 / 4  10 / 8 / 11 / 9   C右 / C左 / C下 / C上

電GO64 はコントローラをコネクタ 3 に挿す前提なので、Controller Port 3 に割り当てる。

そして起動直後のコントローラ選択画面で詰んだ。

電車でGO!64 のコントローラ選択画面

「コネクタ 1 のコントローラで選択」と書いてある。ポート 1 に何も割り当てていないと、ここから動けない。ポート 1 の左右と A をキーボードに割り当てておくこと。

あと、N64 には L + R + Start の同時押しでリセット扱いになる仕様がある。この並べ方だと C + SELECT + START がそれに当たる。同時に押さないように。

ares で電車でGO!64 をプレイしている画面

完成

DuckStation   PS1 版各種          16 ボタンを 1:1
PCSX2         電車でGO!3          軸にして USB の Type 2 へ
Flycast       DC 版 電車でGO!2    DC の電GO配置に並べ替え
ares          電車でGO!64         ポート 3 に N64 配置で並べ替え

ファームは 1 本のまま。変えたのはエミュ側の割り当てだけだ。

4 つのエミュレータで電車でGO!を動かしたところ

このスクショ撮るの大変でした汗

最初は「DuckStation で段が飛ばなければいいや」くらいの気持ちだったのに、気付いたら 4 つのエミュのソースを読んでいた。

一番おもしろかったのは、軸を通さないほど素直に動くということ。エミュに解釈させる余地をなくすのが正解で、遠回りに見えた「生のビットを 1:1」が、結局どのエミュでも通用した。

この記事について

一連の作業は Claude(Anthropic の AI)とやりとりしながら進めた。隠すことでもないので書いておく。

ファームのコードと設定ファイルの書き換えは、ほぼ Claude に書いてもらった。PCSX2、Flycast、ares の入力まわりのソースを読んで「ビットをそのまま渡している」「C ボタンは打ち消されない」と判断材料を出してきたのも、「閾値じゃない気がする」という自分の勘に、資料で「そもそも軸が存在しない」と裏付けを付けてきたのも Claude だ。自分だけなら何日かかったか分からない。

一方、実機を触るところは全部こちらの仕事だった。joy.cpl を見ながら全部の段を読んで表を作ったのも、TCPP-20001 で「同じ組み合わせが点滅している」と観察して伝えたのも自分だ。非常位置で値が変わるのを「汽車でGO! の自動帯だ」と見抜いたのもこちらで、Claude はハンドルの不調を疑っていた。

間違いもあった。PCSX2 の機能を実在しない名前で説明してきたり、「パススルー機能で直る」と勧めてきたものが、実は動かないままマージされていたり。書き込みで詰まったときは次々に別の手を出してきたが、ボードの選び間違いや COM 番号の取り違えといった、もっと単純な原因を先に潰すべき場面が何度かあった。

AI に投げれば完成する、という話ではない。実機を持っていて、症状を正確に伝えられて、言われたことをその場で試せる人間がいないと一歩も進まない。そういう分担だった。

TYPE2 の車内信号ランプを PCSX2 で光らせる【Arduino で変換アダプター自作 その3】

エミュレータを使う前に

BIOS もゲームの ISO も、必ず自分の PS2 本体・自分のディスクから用意すること。
ネットで拾ってくるのは論外。ダウンロードも配布も違法。
エミュレータ本体(PCSX2)自体は合法で公式配布されている。問題になるのは常にデータの出どころのほう。

なお自分の持ち物から吸い出す場合も、日本の著作権法には技術的保護手段の回避に関する規定がある。 適法かどうかは各自で確認を。この記事は法的な保証をするものではない。
この作業は Claude(Anthropic の AI)とやりとりしながら進めた。 コードとパッチはほぼ書いてもらっている。実機での計測と検証は自分。
電車でGO!を TYPE2 で運転しているところ

ランプが光らない

TYPE2 には車内信号のランプが付いている。運転台の速度制限表示だ。実機の電車でGO!では、これが走行中に光ったり消えたりする。

車内信号ランプが消えている TYPE2

その1で PCSX2 対応にしたが、このランプは死んだままだった。振動も来ない。

考えてみれば当然で、その1でやったのは「TYPE2 を読んで HID として出す」という一方通行の話だ。逆向き、つまりゲームからコントローラへ何かを送る経路は作っていない。

まず実機の出力プロトコルを調べる

ランプを光らせるには、実機に何を送ればいいのか。

MarcRiera/ddgo-controller-docs に書いてある。制御転送で2バイト送る。

bmRequestType 0x41   (Host→Device | Vendor | Interface)
bRequest      0x09
wValue        0x0201
wLength       2
データ        [0] = Status     0x00 停止 / 0x01 作動
              [1] = Function   0x01 左振動 / 0x02 右振動 / 0x03 車内信号ランプ

これをそのまま実装して、Function を 0x01 から順に、1.5秒 ON / 0.5秒 OFF で回すテストスケッチを書いた。

動かした。

振動が止まらない。

ON にはなる。ずっと振動する。OFF を送っても止まらない。

公式ドキュメントが間違っていた

バイト順を疑った。

仮に順序が逆で、実機が [Function, Status] の順で読んでいるとする。すると「OFF を送る」つもりで出した [0x00, 0x01] は、実機には「Function 0x00 を Status 0x01 で実行」と読める。Function 0x00 は未定義。OFF は永遠に届かない。逆に ON のつもりの [0x01, 0x01] は「Function 0x01 を ON」で、たまたま正しく動く。ON だけ効いて OFF が効かないという症状と完全に一致する。

テストスケッチに、実行中にバイト順を入れ替えられるコマンドを仕込んで、押してみた。

止まった。

つまり、正しいのはこう。

データ [0] = Function   0x01 左振動 / 0x02 右振動 / 0x03 車内信号ランプ
       [1] = Status     0x00 停止 / 0x01 作動

ドキュメントとは逆である。

ちゃんと 1.5秒 ON / 0.5秒 OFF で気持ちよく動くようになった。実機を持っていないと気付けない類の間違いなので、そのうち報告しておきたい。

PCSX2 は出力を捨てていた

実機の喋り方は分かった。あとは PCSX2 からその指示をもらえばいい。

……と思って PCSX2 のソースを開いたら、こうなっていた。

pcsx2/USB/usb-pad/usb-train.cpp

static void train_handle_control(USBDevice* dev, USBPacket* p, int request, int value,
    int index, int length, uint8_t* data)
{
    if (usb_desc_handle_control(dev, p, request, value, index, length, data) < 0)
        p->status = USB_RET_STALL;
}

usb_desc_handle_control が処理するのは標準のディスクリプタ要求だけ。ベンダー独自の要求は全部 < 0 を返して、STALL(拒否)して捨てられる。

つまり、ゲームが出したランプと振動の指示は、エミュレータの中で握りつぶされて消えていた。設定項目もプラグイン機構もない。

ソースを直すしかない。

PCSX2 をビルドしてパッチを当てる

腹をくくってビルド環境を作る。Visual Studio と、PCSX2 のソース、依存ライブラリ一式。初回ビルドは30分くらいかかった。

パッチは単純で、STALL する前に横取りするだけ。

static void train_handle_control(USBDevice* dev, USBPacket* p, int request, int value,
    int index, int length, uint8_t* data)
{
    // ゲームが出す振動・ランプの指示を横取りする
    if (DdgoHandleOutput(request, value, length, data))
    {
        p->actual_length = 0;
        return;
    }

    if (usb_desc_handle_control(dev, p, request, value, index, length, data) < 0)
        p->status = USB_RET_STALL;
}

DdgoHandleOutput の中では、bmRequestType 0x41 / bRequest 0x09 / wValue 0x0201 に一致したら状態を記録して、外に書き出す。

ゲームは本当に信号を送っているのか

ここで初めて、そもそもゲームがランプの信号を出しているのかという疑問に答えが出る。出していなければ全部無駄骨だ。

ビルドして、電車でGO!Pro 2 を起動して、走らせた。

TrainController out: func=0x02 status=0x00  (L=0 R=0 lamp=1)
TrainController out: func=0x01 status=0x00  (L=0 R=0 lamp=1)
TrainController out: func=0x03 status=0x01  (L=0 R=0 lamp=1)

出ていた。

func=0x03(車内信号ランプ)が status=0x01。ちゃんと送っている。しかも 13ms ごとに延々と。

これで確信が持てたので、あとは Arduino に流すだけ。

COM ポートに直接書き出す

最初はファイル経由でやった。PCSX2 がテキストファイルに状態を書き、それを監視するスクリプトがシリアルに転送する。

ランプは点いた。

車内信号ランプが点灯した TYPE2

ただ、これだと短い振動パルスを取りこぼす可能性がある。ファイルを見に行く間隔より短い変化は消えてしまう。

なので、PCSX2 から直接 COM ポートに書き込むようにした。中継役がいなくなるので取りこぼしようがない。

PCSX2_TRAIN_SERIAL=COM8

ついでに2つ直した。

変化したときだけ出力する。 ゲームは毎秒230回も同じ内容を投げてくる。全部書いていたらログもシリアルも溢れる。

COM ポートを自動検出する。 SetupAPI で Arduino(VID 0x2341)のポートを探す。環境変数すら要らなくなった。

Arduino 側も、変化した Function だけ制御転送を送るようにした。制御転送の間は入力のポーリングが止まるので、無駄撃ちは減らしたい。

完成

[  6.2946] TrainController: \\.\COM8 に出力します
[110.1850] TrainController out: L=0 R=0 lamp=0
[172.3643] TrainController out: L=0 R=0 lamp=1
[175.4005] TrainController out: L=1 R=0 lamp=1
[175.5671] TrainController out: L=0 R=0 lamp=1
[390.7480] TrainController out: L=0 R=1 lamp=1

走行中に車内信号が変わり、分岐を通過すれば左右の振動が交互に入る。

車内信号ランプが点灯する瞬間

最初は「PCSX2 で使えればいいや」くらいの気持ちだったのに、気付いたらエミュレータ本体にパッチを当てていた。ドキュメントの誤記まで見つけてしまった。

実機を引っ張り出してきた甲斐はあった。

おまけ:1本のファームで両対応にした

ここまでで、PCSX2 用とSwitch 用でファームが2本になっていた。使うたびに書き換えるのが面倒なので、1本にまとめた。

挿すときに押しているボタンでモードが決まる。

START を押しながら挿す   → PCSX2 モード   (ランプが2回点滅)
SELECT を押しながら挿す  → Switch モード  (ランプが1回点滅)
何も押さずに挿す          → 前回のモード

Arduino には USB につながる直前に呼ばれる initVariant() という関数があるので、そこで TYPE2 のボタンを読んでからUSBに顔を出す。VID/PID や名乗る名前は、HID の部品を自前で持てば実行時に切り替えられた。

ただし Switch は COM ポートがあると受け付けないので、両モードとも COM ポートはなくした。そのかわりランプの指示は HID の出力レポートで受け取る。DENSYASX2(今回パッチを当てた PCSX2 に付けた名前)も、COM ポートではなくこちらに送るように変えた。

この記事について

一連の作業は Claude(Anthropic の AI)とやりとりしながら進めた。隠すことでもないので書いておく。

ファームウェアのコードと PCSX2 のパッチは、ほぼ Claude に書いてもらった。USB Host Shield ライブラリのクラスフィルタの件も、HID の軸の静止位置の件も、症状を伝えたら原因を突き止めてくれた。自分だけなら何日かかったか分からない。

一方、実機を触るところは全部こちらの仕事だった。ノッチごとの生バイトを取ったのも、出力テストを回して振動が止まらないことに気付いたのも、バイト順を入れ替えて「止まった」と確認したのも自分だ。ドキュメントの誤記については、「順序が逆では」という仮説が Claude、実機での確定が自分、という分担になる。

間違いもあった。振動がほとんど来ないのを「取りこぼしでは」と言われて PCSX2 を作り直したが、実際はゲーム側の振動設定をオフにしていただけだった。作り直し自体は結果的に良くなったので損はしていない。

AI に投げれば完成する、という話ではない。実機を持っていて、症状を正確に伝えられて、言われたことをその場で試せる人間がいないと一歩も進まない。そういう分担だった。

TYPE2 を ZUIKI マスコンに化けさせて Switch 2 で遊ぶ【Arduino で変換アダプター自作 その2】

この作業は Claude(Anthropic の AI)とやりとりしながら進めた。 コードとパッチはほぼ書いてもらっている。実機での計測と検証は自分。
Nintendo Switch 2 と電車でGO!コントローラ TYPE2

ズイキのマスコンになりきる

その1で作ったアダプターは、そのままでは Switch では動かない。Switch は素性の知れないコントローラを受け付けないからだ。

そこで、ZUIKI のワンハンドルマスコン(ZKNS-001)になりきらせる。Switch 版の電車でGO!が公式対応しているコントローラだ。

必要なのは、

  • VID 0x0F0D / PID 0x00C1
  • 製品名 One Handle MasCon for Nintendo Switch
  • ZKNS-001 と同じ HID レポートディスクリプタ(94バイト)
  • CDC(仮想 COM ポート)を無効にすること

ディスクリプタの中身は MarcRiera/ddgo-controller-docs にある。ありがたい。

最後の CDC 無効化が地味に効く。Leonardo は標準だと「HID + 仮想シリアルポート」の複合デバイスとして見える。Switch はこれを嫌がるので、HID 単体にしないと受け付けてもらえない。

Switch 2 のコントローラ設定でプロコンとして認識されている

Switch 側からはプロコンとして認識された。

VID/PID を変えるだけでは動かない

boards.txt をいじって VID/PID を変え、-DCDC_DISABLED を付けてビルドした。書き込んだ。

何も出ない。

不明な USB デバイス (デバイス記述子要求の失敗)
USB\VID_0000&PID_0002

Switch どころか PC でも認識されない。VID が 0000 ということは、そもそも記述子を返せていない。

原因は2つあった。順に潰していく。

boards.txt にボードを丸ごと追加する

1つ目。設定が効いていなかった。

Arduino IDE の「ツール」メニューに選択肢を足すには、boards.txt にこう書く。

menu.usbid=USB Identity
leonardo.menu.usbid.zuiki=Zuiki Mascon (ZKNS-001)
leonardo.menu.usbid.zuiki.build.vid=0x0F0D
...

これで「ツール → USB Identity」が出るはず……なのだが出ない。IDE を再起動しても出ない。ファイルには確かに書けている(grep で確認した)。

不思議なことに、先に入れてあった menu.usbcdc(USB CDC の Enabled/Disabled)はちゃんと出ている。同じファイルの同じ書式なのに、2つ目だけ出ない。Arduino IDE 2.3.10 の話。原因は分からなかった。

粘っても仕方ないので、独立したボード定義として丸ごと追加することにした。leonardo.* の行を全部コピーして zuikimascon.* にリネームし、必要なところだけ書き換える。

zuikimascon.name=Zuiki Mascon (ZKNS-001)
zuikimascon.build.vid=0x0F0D
zuikimascon.build.pid=0x00C1
zuikimascon.build.usb_product="One Handle MasCon for Nintendo Switch"
zuikimascon.build.extra_flags={build.usb_flags} -DCDC_DISABLED

40行ちょっと。これで「ツール → ボード → Arduino AVR Boards」に Zuiki Mascon (ZKNS-001) が出るようになった。メニューで切り替えるよりむしろ分かりやすい。

Arduino IDE のボード選択に Zuiki Mascon (ZKNS-001) が出ている
boards.txt は AVR ボードパッケージを更新すると上書きされて消える。バックアップ必須。

HID.cpp の罠

2つ目。これは完全に自分のせいだった。

以前、別件(Switch 2 用のマウスリピーター)で Arduino の HID.cpp を書き換えていた。インターフェースディスクリプタの SubClass と Protocol をマウス用に固定するパッチだ。マウスリピーターについては、また書こうと思う。

D_INTERFACE(..., HID_SUBCLASS_BOOT_INTERFACE, HID_PROTOCOL_MOUSE)

ZKNS-001 は SubClass / Protocol とも 0x00 でなければならない。これが残っていたせいで、記述子の内容が食い違って弾かれていた。

標準に戻したら、あっさり認識された。

D_INTERFACE(..., HID_SUBCLASS_NONE, HID_PROTOCOL_NONE)

こういうグローバルな書き換えは、どのプロジェクトにも効いてしまうので危ない。HID.cpp.mouse と HID.cpp.bak を両方残して、切り替えて使うことにした。

二軸を一軸に変換する

ZKNS-001 はワンハンドル、TYPE2 はツーハンドル。段数の表現が違うので変換が要る。

ZKNS-001 は 0〜14 の一本の値で表す。

0        非常
1〜8     ブレーキ B8〜B1
9        N
10〜14   力行 P1〜P5

TYPE2 側は二本あるので、単純にこうした。

  • ブレーキが効いていれば、ブレーキ段を採用
  • ブレーキが解除なら、マスコン段を採用

実機の運転と同じで、両方同時に入れることはないので、これで破綻しない。

ボタンは位置で合わせた。PlayStation と Nintendo は X と Y が逆なので、□×○△ → Y B A X になる。

HOME ボタンが欲しい

TYPE2 には HOME ボタンがない。ゲームを抜けるのにいちいち Joy-Con を取りに行くのは面倒だ。

SELECT + START の同時押しに割り当てた。

厄介なのは「同時押し」の判定で、素直に書くと先に押したほうが単独で発火してしまう。なので、どちらかが押されたら 200ms 待ってから判定するようにした。

  • 200ms 以内にもう一方も押された → HOME を出す
  • 200ms 経っても片方だけ → 普通に - か + を出す

体感では遅延は分からない。

SELECT と START の同時押しで HOME 画面を出したところ

書き込めなくなる問題

CDC を無効にした副作用として、COM ポートが出なくなる。つまり Arduino IDE の書き込みボタンが使えない。

Skipping 1200-bps touch reset: no serial port selected!
Failed uploading: no upload port provided

ブートローダに入れば書けるのだが、手順がシビアだ。

  1. 基板の RESET を素早く2回押す
  2. ブートローダが起動して、一時的に COM ポートが現れる
  3. 8秒以内に書き込みを開始する

この8秒に手で間に合わせるのが難しい。何度も失敗した。

……というのは、実はマウスリピーターのときに一度通った道だった。そのとき作った、COM ポートが現れるのを待ち構えるスクリプトを汎用化して使う。PowerShell で、新しいポートが生えたら即座に avrdude を叩く。

.\flash_zuiki.ps1 -Sketch ddgo_type2_zuiki

Arduino IDE ではコンパイルだけ済ませておいて(検証ボタン)、書き込みはこのスクリプトに任せる。これで一発で決まるようになった。

Switch 2 の電車でGO!を TYPE2 で運転しているところ

無事に動いた。

次は 車内信号のランプを光らせる。ここからが本番。→ その3へ

電車でGO!コントローラ TYPE2 を PCSX2 で使う【Arduino で変換アダプター自作 その1】

エミュレータを使う前に

BIOS もゲームの ISO も、必ず自分の PS2 本体・自分のディスクから用意すること。
ネットで拾ってくるのは論外。ダウンロードも配布も違法。
エミュレータ本体(PCSX2)自体は合法で公式配布されている。問題になるのは常にデータの出どころのほう。

なお自分の持ち物から吸い出す場合も、日本の著作権法には技術的保護手段の回避に関する規定がある。 適法かどうかは各自で確認を。この記事は法的な保証をするものではない。
この作業は Claude(Anthropic の AI)とやりとりしながら進めた。 コードとパッチはほぼ書いてもらっている。実機での計測と検証は自分。

何がしたかったのか

PS2 の電車でGO!用コントローラ、TCPP-20009(通称 TYPE2)。二軸のやつだ。

電車でGO!コントローラ TYPE2 (TCPP-20009)

これを PCSX2 で使いたい。ただそれだけ。

PCSX2 には最初から「Train Controller / Type 2」というデバイスが用意されていて、エミュレータ側は完璧に実機のふりをしてくれる。問題は実機の TYPE2 を PC に挿しても認識されないことだ。PS2 用の独自プロトコルで喋っているので、Windows からは正体不明の USB デバイスにしか見えない。

既存の変換ソフトでは反応が間に合わなかった

まず定番の DenConv を試した。Zadig でドライバを当てて、キーボード出力させて、vJoy に橋渡しして……とやったのだが、反応が間に合わなかった。

段数は合う。合うのだが、マスコンを N から P5 まで一気に入れると反映まで1秒くらいかかる。実際に運転していると致命的で、加速のタイミングが全部ズレる。

内部を追ってみると、画面のラベルを読んで段数を判定する作りになっていた。しかも Ver4 系ではそのラベルが独自描画で、外から読めない。アドレス解析からやり直しになる。

割に合わないので、ハードで作ることにした。

※ 当方の環境・Ver4.17 での話。設定や環境次第では結果が違うかもしれない。

Arduino Leonardo + USB Host Shield で作る

方針はこう。

TYPE2 → USB Host Shield 2.0 → Arduino Leonardo → 普通のゲームパッドとして PC へ

Leonardo は ATmega32U4 を積んでいて、自分から USB デバイスになれる。だからキーボードにもマウスにもゲームパッドにもなれる。USB Host Shield を乗せれば、逆に USB ホストとして他のデバイスを繋げられる。つまり両方できる。

Arduino Leonardo と USB Host Shield 2.0

やることは単純で、TYPE2 からノッチの値を読んで、標準的な HID ジョイスティックとして出し直すだけ。PCSX2 からは普通のコントローラに見えるので、あとは設定画面で割り当てれば済む。

TYPE2 が吐いているデータを読む

シリアルに生バイトを流して、レバーを一段ずつ動かしながら記録した。8バイト固定。

[0] 0x01 固定
[1] ブレーキ  解除 79 / B1-B8 8A 94 9A A2 A8 AF B2 B5 / 非常 B9
[2] マスコン  N 81 / P1 6D / P2 54 / P3 3F / P4 21 / P5 00
[3] 警笛      0xFF(ペダル。今回は使わない)
[4] 十字      上 00 / 右 02 / 下 04 / 左 06 / 中立 08
[5] ボタン    B:01 A:02 C:04 D:08 SELECT:10 START:20
[6][7] 0x00

面白いのは、この値が PCSX2 のソースにある dct01_brake / dct01_power のテーブルと完全に一致したこと。PCSX2 の中の人もちゃんと実機を解析していたわけだ。

一点だけ罠がある。更新がないフレームではレバーのバイトに 0xFF が返ってくる。素直に受け取ると一瞬 P5 に入ったことになって暴れる。値域の内側のときだけ取り込んで、それ以外は直前の値を保持するようにした。

ハマりどころ1:デバイスが見つからない

最初、interrupt IN endpoint が見つかりません で止まった。

原因は USB Host Shield ライブラリの ConfigDescParser の使い方。サンプルを真似して

ConfigDescParser<USB_CLASS_USE_CLASS_INFO, 0, 0, CP_MASK_COMPARE_CLASS>

と書いていたのだが、これはインターフェースのクラスが 0x00 のものしか拾わない。TYPE2 はベンダー独自クラス(0xFF)なので、一生マッチしない。

Xbox 用のドライバのソースを見たら、フィルタなしで書いてあった。VID/PID で既に確認済みなんだから絞る必要がない、という理屈。同じにしたら通った。

ConfigDescParser<0, 0, 0, 0>

ハマりどころ2:ノッチが振り切ったまま動かない

HID としては出るようになった。joy.cpl でもちゃんと動く。なのに PCSX2 の割り当て画面がおかしい。

マスコンを一往復させる前に勝手に割り当てが完了してしまう。しかも確定した後はブレーキが一切効かず、マスコンは P5 に入りっぱなし。

原因は軸の静止位置だった。

HID の 0-255 の軸は 0x80 が中央という約束になっている。最初は実機の値をそのまま出していたので、静止位置が 0x00(端)だった。PCSX2 から見ると「開いた瞬間から全力で振り切っているスティック」なので、即座に割り当てが確定してゴミが入る。

なので、静止位置を 0x80 にして、片側半分だけを使うようにした。普通のスティックを中央から倒すのと同じ動きになる。

マスコン  N 0x80 → P5 0xFF
ブレーキ  解除 0x80 → 非常 0xFF

値は PCSX2 側のノッチ判定の境界の内側を狙って決めてある。これで一発で通った。

完成

PCSX2 のコントローラ設定画面

PCSX2 側の設定は、

  • USB デバイス:Train Controller / Type 2
  • Axes Passthrough:OFF(こちらで既にノッチ値に丸めているため)

遅延は体感でゼロ。ノッチを入れた瞬間に反映される。

TYPE2 で PCSX2 の電車でGO!を運転しているところ

次はこれを Nintendo Switch 2 でも使えるようにする。→ その2へ

Blogger の旧ブログをデザインそのまま残して、同じドメインで新ブログを始める

この作業は Claude(Anthropic の AI)とやりとりしながら進めた。 コマンドとスクリプトはほぼ書いてもらっている。どう残したいかを決めたのと、画面を見て確かめたのは自分。 目次 ブログを作り替えたい URL を壊したくない 丸ごと落とす ...