かつかれーのメモ帳

実験ノートか勉強記録

Python環境が訳わからなくならないための自分用メモ

WindowsLinux(CentOSUbuntu)でPython環境を壊さないためのメモをしておく。自分の使い方の場合、現時点ではこのやり方が一番良さそう。

まずpipについて

例えば普段pyコマンドでpythonを起動しているとする。この環境にモジュールを入れたいときは

py -m pip install numpy

のような記法が強く推奨される。pip, pip3, pip3.9など、どれがどれだかそのうちわからなくなるので。
所望のバージョンのpythonが正しく起動できている限り、これでモジュールが入れられるはず。

Windowsにインストール

Python標準のインストーラーを使ってインストール。新しいバージョンが出たら都度インストーラーを落としてきてインストールするので問題ない。
このときAdd Python 3.x to PATHすると混乱のもとになるのでしないように。
ストアアプリ版も変に競合するので入れない方が良い。
py.exeというランチャーがついてくるので、pyコマンドでpythonを起動する運用にする。バージョンは引数で選べるし、省略すれば自動的に最新のものが起動される。
今まではパスが通っていると思って使っていたモジュールたちは、以下のようにpyランチャー経由で呼べば良い。

py -m IPython
py -m jupyter lab

参考:
gammasoft.jp

Linuxにインストール

スパコンなど、自分がsudoersに入っていない場合の手順。pyenvを使うのが良い。

(WSL2 とかだと sudo できる && 必要なパッケージが揃っていないため以下をやっておく)

sudo apt update
sudo apt install build-essential libffi-dev libssl-dev zlib1g-dev liblzma-dev libbz2-dev libreadline-dev libsqlite3-dev libopencv-dev tk-dev git

pyenvのgithubページにある通り、

git clone https://github.com/pyenv/pyenv.git ~/.pyenv
cd ~/.pyenv && src/configure && make -C src
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n  eval "$(pyenv init -)"\nfi' >> ~/.bash_profile
source ~/.bash_profile

すればgit cloneしかしないのでsudo aptが不要で嬉しい。なお2行目はpyenvの高速化のためのもので、コケたら別にそれはそれでいいらしい。
さらに

pyenv install 3.9.2
pyenv global 3.9.2

これでpython3.9.2がインストールされ、pythonコマンドに紐付けられる。
他に何がinstall出来るか知りたいときは

pyenv install -l

で一覧が出せる。
現在何が入っていて、そのうちどれがデフォルトなのか知りたいときは

pyenv version

すれば良い。

機械学習を用いた音ゲーのクリア難度推定 (その1)

音ゲーのクリア難度推定なんかデータサイエンスの入門にいいんじゃないかな?とふと思い立ってやることにしました。3記事くらいに分けて公開します。初回はデータ収集のためのWebスクレイピングについて。

やること

  • LR2IRのデータをもとに、BMSの譜面のクリア難度を推定します。
  • 譜面とプレイヤーの相性が要因として大きいので、Eloレーティングのような1軸評価ではない手法について検討します。

データの準備

LR2IRにはプレー人数の非常に少ない譜面や、アクティブでないアカウントが多く存在します。
そこで、まず集計対象プレイヤーについて「BMS発狂難易度表の収録曲のうち125曲以上スコア登録済み」という基準を設けます。これはStairwayの基準を引用したものです。
そして、「集計対象プレイヤー(現在12000人くらい)のうち、100人以上がスコアをつけている」曲を集計対象曲とします。

import re
from urllib.request import urlopen
from bs4 import BeautifulSoup
#stairwayの集計対象プレイヤーのid一覧を取得
player_ids=[]
bs = BeautifulSoup(urlopen("http://stairway.sakura.ne.jp/bms/LunaticRave2/?contents=player&p=1"))
for player_link in bs.find_all("a", {"href" : re.compile("/?contents=player&page=\d")}):
    player_id = int(player_link["href"].split("=")[-1])
    player_ids.append(player_id)
#ファイルに保存
with open("playerid","w") as f:
    f.write(str(player_ids[0]))
    for player_id in player_ids[1:]:
        f.write(",")
        f.write(str(player_id))

全ユーザーに対してプレー履歴の全取得をするとちょっとサーバーに負荷をかけてしまいそうなので、ユーザーリストはStairwayから拝借しました。
aタグのうち、プレーヤーのmypageへのリンクの形になっているものを選び出し、splitすることでplayer idを得ています。
これは単一ページのパースなのですぐできると思います。
次に、譜面ごとにプレー人数を集計します。譜面ID(bmsid)をkey、プレー人数をvalにした辞書で数えていきます。keyがないとき勝手に0を入れてくれるdefaultdict(int)が便利。負荷をかけないようsleepを入れながら、丸一日くらいで取得しました。
このうち、100人以上がプレーした譜面を残します。意外なことに、10000譜面ちょっとありました。

import re
from collections import defaultdict
from urllib.request import urlopen
from bs4 import BeautifulSoup
#各playerのプレーしたことあるbmsidを全取得、bmsidごとにプレーした人数を数える
num_player = defaultdict(int) #key:val=(bmsid, プレー人数)
for player_id in player_ids:
    for i in range(1, 10000):
        bs=BeautifulSoup(urlopen(f"http://www.dream-pro.info/~lavalse/LR2IR/search.cgi?mode=mylist&playerid={player_id}&sort=recent&filter=7&page={i}"))
        findresult=bs.find_all("a", {"href" : re.compile("search.cgi\?mode=ranking&bmsid=\d")})
        if not findresult:
            break #空のページに至ったらbreak
        for bms_link in findresult:
            bms_id = int(bms_link["href"].split("=")[-1])
            num_player[bms_id]+=1
        time.sleep(0.01)
    print(f"player{player_id} done")
#プレー人数がmin_playcountより多い譜面だけ残してbms_idsにidを集計
bms_ids=[]
for k in num_player.keys():
    if num_player[k]<min_playcount:
        continue:
    else
        bms_ids.append(k)
#ファイルに保存
with open("bmsid","w") as f:
    f.write(str(bms_ids[0]))
    for bms_id in bms_ids[1:]:
        f.write(",")
        f.write(str(bms_id))

最後に、この集計対象譜面について全プレーヤーのスコアを取得し、集計対象ユーザーのものだけ残してファイルに保存します。1譜面1ファイルで書き出しています。
丸二日くらいかかりました。ファイルサイズは合計で770 MBでした。

import re
import time
import os
from collections import defaultdict
from urllib.request import urlopen
from bs4 import BeautifulSoup

with open("playerid","r") as f:
    player_ids=set([int(i) for i in f.read().split(",")])
with open("bmsid","r") as f:
    bms_ids=set([int(i) for i in f.read().split(",")])

for bms_id in bms_ids: #集計対象譜面についてループ
    if os.path.exists(str(bms_id)):
        print(f"bmsid {bms_id} ... skipped")
        continue
    with open(str(bms_id), "w") as f:
        print(f"bmsid {bms_id} ... ",end="")
        for i in range(1, 10000):
            bs=BeautifulSoup(urlopen(f"http://www.dream-pro.info/~lavalse/LR2IR/search.cgi?mode=ranking&page={i}&bmsid={bms_id}"))
            find_result=bs.find_all("a", {"href" : re.compile("search.cgi\?mode=mypage&playerid=\d")})
            if not find_result:
                break #空のページに至ったらbreak
            for player_link in find_result:
                player_id = int(player_link["href"].split("=")[-1])
                if player_id not in player_ids:
                    continue
                playdata = player_link.parent.parent.contents[3:]
                f.write(str(player_id))
                for item in playdata:
                    f.write(",")
                    f.write(item.get_text())
                f.write("\n")
            time.sleep(0.01)
        print("done")

例えばBMS界のAAこと星の器(bmsid=15)に対する出力ファイルの冒頭はこんな感じ。各行で一番左の列はplayer id、残りがプレーデータです。

37188,FULLCOMBO,AAA,4012/4012(100%),2006/2006,0,2006,0,0,0,2,難,乱,KB,LR2
83367,FULLCOMBO,AAA,4011/4012(99.97%),2006/2006,0,2005,1,0,0,0,難,乱,BM,LR2
202,★FULLCOMBO,AAA,4010/4012(99.95%),2006/2006,0,2004,2,0,0,0,難,乱,KB,LR2
105711,FULLCOMBO,AAA,4010/4012(99.95%),2006/2006,0,2004,2,0,0,0,易,乱,KB,LR2
120831,FULLCOMBO,AAA,4010/4012(99.95%),2006/2006,0,2004,2,0,0,0,難,乱,KB,LR2
160637,FULLCOMBO,AAA,4009/4012(99.92%),2006/2006,0,2003,3,0,0,2,難,乱,BM,LR2
15499,FULLCOMBO,AAA,4008/4012(99.9%),2006/2006,2,2002,4,0,0,4,難,乱,BM,LR2
72359,FULLCOMBO,AAA,4008/4012(99.9%),2006/2006,0,2002,4,0,0,0,易,乱,KB,LR2
94774,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,BM,LR2
3906,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,BM,LR2
36488,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,KB,LR2
2725,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2002,3,0,0,1,難,乱,BM,LR2
4314,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2002,3,0,0,1,難,乱,BM,LR2
95222,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,BM,LR2
139053,FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,1,難,乱,BM,LR2
22923,FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,0,難,乱,KB,LR2
4812,★FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,13,難,乱,BM,LR2
51433,FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,8,普,乱,BM,LR2
25546,FULLCOMBO,AAA,4005/4012(99.82%),2006/2006,0,2000,5,0,0,4,普,乱,BM,LR2
...

次回はMarix Factorizationという方法を用いて、これらのデータから難度推定を行っていきます。
→すでに同等の成果が公表されていたのでこの件は保留とします。難度の他に推定したい量が思いついたら続けます。

複数のUSBファームウエアを自作コントローラーに乗せる

IIDX自作コントローラー記事のソフト編(その3)です。
以前の記事(その1, その2)で、2つの異なるUSB HIDゲームパッドの仕様を決めました。今回はこれらを1つのコントローラーに収め、切り替えながら動作させるための具体的な実装についてまとめます。当記事で一旦ソフト編は完結する予定です。

大雑把な基板の仕様

詳細は回路編に譲りますが、ファームウエア設計に関わる部分だけここで決めておきます。

  • ICとして、USBインターフェースの乗ったAVRを用いる。
  • ボタン・スクラッチ等の入力、LED等の出力も同じICで扱う。これにより、主要部をワンチップで完結させる*1
  • 同じ基板をIIDXだけでなくSDVXにも利用できる程度の汎用性を持たせる。

こんな感じ。ある程度多ピンのAVRを採用することになります。
要件を満たす物の中で入手性が良いのはATmega32U4ですかね。Arduinoとして実装済みの基板(Leonardo, Micro)が手に入る点もgoodです。しかし、値段がほぼ変わらないのに入出力ピンが多く取れて、遊ぶ余地があるAT90USB646の方が個人的には好みです*2
…こんな調子なので基板の仕様がきっちり固まりません。基板仕様のブレはファームウエア側で吸収するのが良さそうです。

ファームウエアの設計

まずは簡単な図から。
f:id:DSKK:20200603194916p:plain
ファームウエア仕様の概略。画像出典はこのブログとWikipedia
図からもわかる通り、コントローラーの構成要素は大きく分けて以下の二つです(左右に振り分けています)。後述のstate_structがこれらをつないでいます。

  • ハードウエア
    • 主に筐体+基板をハードウエアと呼んでいます。左側、IIDXの囲いの中の2つのコントローラー画像がそれぞれ異なるハードウエアに対応します。
    • 大雑把に機種ごとに括ることができます。
  • 動作モード
    • 画像の一番右の列です。PC等から見てどんな仕様のコントローラーに見えるのか、色々と異なっています。各動作モードは個別のHIDレポートディスクリプタを持つため、動作モードに応じて異なった形のHIDレポートを送出する必要があります。
    • 例えばPS3コントローラーのモードがIIDXとpop'nの家庭版に共通して用いられるように、機種をまたがって同じモードが使用される場合があります。

ハードウエア仕様は作るうちに変化する幅が広く(場合によっては採用するチップから変えてしまうこともあります)、どの程度の汎用性を持たせるべきかかなり判断しづらいです。そこで今回は、ハードウエア仕様1つにつき1つのファームウエアを作成することにします(画像にもあるように、個別のmain.cおよびmakefileを持つようにします)。コーディングのコストを最小限にするために、使うチップを変えない限りmakefileを使い回せるようにし、main.cも最低限の内容になるようにします。

EEPROMを用いた動作モード切替

明らかにこのままではハードウエアと動作モードの組み合わせが多すぎてしまい実装が大変です。まず、ハードウエア側は上述の通り、ファームウエアを個別に分けることにより個々の実装を単純にします。動作モード側は「起動後の動作モードの切り替えを考慮しなくてよい*3」ことに着目し、イニシャライズ時に一つの動作モードに確定してしまいます。すると上記の図はイニシャライズ後の時点で
f:id:DSKK:20200604102453p:plain
ここまで単純になります。また、破線を含めたループがメインループになります。

struct states_str_t states_str;
int main(void) {
    io_init(); //ピンアサインを決める。PORTx, DDRxの設定など
    usb_init(); //USB関連のイニシャライズ(後述)
    while (usb_configuration==0); // configuredになるまで待つ
    _delay_ms(1000);
    while (1) {
        io_task(); //=「キー入力を読んで更新」
        (*usb_task_ptr)(); //=「データを成形して出力」
    }
}

以上のコードに対応します。どんなハードウエアであってもmain.cの内容はこれ+io_init+io_taskになっていればOKです。io_initは入出力レジスタの設定と(必要なら)動作モード変更のためのEEPROMの書き換え、io_taskは入力ピンの読み込み・states_strの更新・出力ピンの設定を行います。詳細はgithubに今日(2020/06/04)上げたソースコードを参照してください。
次に、usb_init関数について解説します。実装は以下の通り。

void usb_init(void) {
    uint8_t eep_val = eeprom_read_byte(EEP_ADDR); // read EEPROM
    eeprom_busy_wait();
    switch(eep_val){
    case 1: //PS3
        ep_list = (const uint8_t *) ep_list_PS3;
        desc_list = (const uint8_t *) desc_list_PS3;
        ep_list_len = 2;
        desc_list_len = 7;
        is_ps3 = 1;
        usb_task_ptr=&usb_task_PS3;
        break;
    case 2: //INFINITAS
        ep_list = (const uint8_t *) ep_list_INFINITAS;
        desc_list = (const uint8_t *) desc_list_INFINITAS;
        ep_list_len = 2;
        desc_list_len = 7;
        is_ps3 = 0;
        usb_task_ptr=&usb_task_INFINITAS;
        break;
    default:
        for(;;);
    }
    //ここから下はレジスタの設定
    HW_CONFIG();  // UHWCON = 0x81 USB device mode && enable the USB pad regulator
    USB_FREEZE(); // enable the USB controller && disable the clock inputs
    PLL_CONFIG(); // (Clock division factor)=8 (External XTAL)=16MHz enable PLL
    while (!(PLLCSR & (1<<PLOCK))); // wait for PLL lock (it takes about 100 ms)
    USB_CONFIG(); // start USB clock
    UDCON = 0; // clear bit0 - DETACH (Set to physically detach
               // the device by disconnecting internal pull-up on D+ or D-).
    usb_configuration = 0; // zero when we are not configured, non-zero when enumerated
    UDIEN = (1<<EORSTE)|(1<<SOFE); //enable the EORSTI interrupt and SOFI interrput
    // EORSTI - End Of Reset Interrupt flag.
    // Set by hardware when an “End Of Reset” has been detected by the USB controller.
    // SOFI - Start Of Frame Interrupt flag.
    // Set by hardware when an USB “Start Of Frame” PID (SOF) has been detected (every 1ms).
    sei();
}

まず、eep_valにEEPROMの内容を読み込んできます。読み込むアドレス=EEP_ADDRの値はとりあえず0にしてあります。EEPROMが腐って読み書きできなくなったら別のアドレスに変えればいいと思います。
読み込んだeep_valの値に応じて各動作モードのイニシャライズをswitch文により行っています(原理上最大256種の動作モードを切り替えられます)。セットするのは以下の変数です。

const uint8_t* ep_list;
const uint8_t* desc_list;
uint8_t ep_list_len;
uint8_t desc_list_len;
uint8_t is_ps3;

特にdesc_listがわかりやすいのですが、これはUSBの各種リクエストに応じて返すべきディスクリプタ情報を構造体の配列として持ったものです。例えばPS3の場合

const struct desc_list_struct desc_list_PS3[] PROGMEM = {
    {0x0100, 0x0000, (const uint8_t*)device_desc_PS3, DEVICE_DESC_SIZE_PS3},
    {0x0200, 0x0000, (const uint8_t*)config_desc_PS3, CONFIG_DESC_SIZE_PS3},
    {0x2100, 0x0000, (const uint8_t*)config_desc_PS3+DEVICE_DESC_SIZE_PS3, 9},
    {0x2200, 0x0000, (const uint8_t*)report_desc_PS3, REPORT_DESC_SIZE_PS3},
    {0x0300, 0x0000, (const uint8_t*)&str_desc_langID, 4},
    {0x0301, 0x0409, (const uint8_t*)&str_desc_manufacturer, sizeof(STR_MANUFACTURER)},
    {0x0302, 0x0409, (const uint8_t*)&str_desc_product_PS3, sizeof(STR_PRODUCT_PS3)}
};

です。usb.cの実装を見ていただけるとわかるのですが、この中からリクエストに合うものを探して返すような動作になっています。ここで一つポイントなのが、各動作モードのディスクリプタをすべてconst PROGMEMキーワード付きでハードコードしている点と、動作モード固有のサフィックス付きで命名している点です。ディスクリプタは結構大きい構造体ですが、これによりRAMを圧迫せずに済みます。またこれらが別々の名前を持っていることから、上記desc_listなどで配列の先頭アドレスが必要になったときにも(const uint8_t*)report_desc_PS3のように問題なく記述できます。
ep_list, desc_listのような抽象化のおかげで、usb.cはハードウエア仕様によらずスタンドアロンで動作します。

各動作モード固有の実装

動作モードごとに

が必要です。これらをcommon/modes/*.cに実装します。ただしストリングディスクリプタの一部は共通なのでcommon_desc.cに実装してあります。
もし動作モードを追加したくなったら以下の手順を踏めば良いです。states_structについては説明がまだですが、後述します。また、ファイルの配置は最後の方に説明があります。

  1. 新たにcommon/modes/内にhoge.cを作る。既存のfuga.cを複製→サフィックスを置換→内容を適宜修正などでOK。
  2. common/modes/modes.hの末尾をコピペで増やしてサフィックスを置換。
  3. common/usb.cのusb_init関数内のswitch文にcaseを追加。
  4. states_struct.cにレポートを送る関数を実装、states_struct.hに関数のプロトタイプ宣言を追加。

簡単ですね。各種ディスクリプタが用意できていれば単純作業で動作モードを増やせるのが良いです。
この実装はハードウエア仕様には依存しないようになっていて、外からは「HIDレポートの送受信用関数」を呼べば良いです。この関数はいくつかのuint8_t型変数*4を受け取り、HIDレポートディスクリプタの要求に沿うように送信用レジスタにセットします。

states_struct

ハードウエア固有のパートはmain.cに押し込めておくことを先に述べました。そして、動作モード固有のパートはcommon/modes/*.cに押し込めてあります。これらを繋ぐための内容がstates_struct.cに実装されています。
states_structはマイコンレジスタよりは高級な、ボタン等の入力状態を保持する構造体です。この構造体は機種ごとに1種類定義されます。IIDXの実装例は以下の通りです。

struct states_str_t{
    uint16_t  button_state; //pressed=1 else=0
    uint8_t   button_is_locked[16];
    int32_t   button_locked_time[16];
    uint8_t   scratch_state; //neutral=0 up=1 down=2
    uint16_t  scratch_position;
    uint8_t   scratch_is_locked;
    int32_t   scratch_locked_time;
    int32_t   scratch_kept_time;
};

16bitのbutton_state(bit0がボタン0番、bit15がボタン15番に対応)、scratch_state(無操作=0, 左回り=1, 右回り=2)など。チャタリング対策のため、前の操作タイミングを控えておく変数などもあります*5
この構造体のインスタンスはexternキーワード付きで1つ作成され、main.cのio_task関数で内容が随時更新されます。そしてstates_struct.cに実装されたusb_task_PS3関数やusb_task_INFINITAS関数などによって読まれ、成形ののち送信されます(これらのusb_task_*関数は前段落で言うところの「HIDレポートの送受信用関数」を内部で呼んでいます)。
usb_task_hogeの形をした関数のうちどれを呼ぶべきかは動作モードを切り替えるたびに変わります。そのため、main.cでは関数ポインタusb_task_ptrの指す関数を呼ぶということにしてあり(上記コード参照)、その指す先はusb_init関数内のswitch文でセットしています。これもちょっとした工夫ポイントですね。

フォルダ構成

これまでの動作の依存関係を踏まえて、以下の通りファイルを配置するときれいにまとまります。

┌─common
│  │  common.h
│  │  usb.h
│  │  usb.c
│  │
│  └─modes
│          modes.h
│          common_desc.c
│          INFINITAS.c
│          PS3.c
│
└─IIDX
    │  states_struct.c
    │  states_struct.h
    │
    └─dao_red_usb_replacement
            main.c
            Makefile

[機種名]/[基板名]/main.cが多数存在していて、それらが1階層上で定義される機種固有のstates_structを更新。states_structはcommon/modes/[該当する動作モード].c内のHIDレポート送信用関数を呼ぶといった流れです。
ハードウエア仕様に依存しない部分はcommonフォルダに分離してあり、特にusb周りはusb.cとusb.hに分けてあります。usb_init関数もここ。

おわりに

以上のソースはこちら。
github.com
ブログを書きながら設計直したら最後のコミットで†Restructured everything†してしまった。gitヘタクソか?
ソフトの実装編はこの辺で完結ですかね。設計こだわったおかげでSDVX版もすぐ出来そう。
自分用の備忘録的な書き方にどうしてもなってしまったので、何か気になる点があったらコメントか
しありす (@cialis438) | Twitter
へのリプをお気軽にどうぞ。感想やコントローラー自作に関する雑談・質問なども歓迎です。
下のリンクからシェアやツイートなどしてもらえると大変執筆モチベが上がりありがたいです。どうぞよろしく。

*1:AVR以外にはトランジスタアレイ(LEDドライブ用)を用いる程度で留めることを想定。

*2:makefileを書き換えてやれば、AT90USBシリーズにもATmega32Uシリーズと同じソースコードが使えます

*3:例えば、繋げる相手がPS3からPCに変わるなら一度ケーブルを抜くことになります。したがってコントローラーの起動時、すなわちUSBケーブルを差し込むタイミングで動作モードを選択できれば十分です。

*4:もちろん、関数の実装によってはこれに限りませんが。

*5:構造体内に定義するのが適切か?と言われると結構微妙。グローバル変数でもよかったかも。

beatmania IIDX INFINITASを自作コントローラーで遊ぶ

IIDX自作コントローラー記事のソフト編(その2)です。
beatmania IIDX INFINITAS(beatmania IIDXのPC移植版。以下、略してINFと呼ぶ。)をプレーできる自作コントローラー仕様について調べてまとめました。

使えるコントローラーについて

とりあえず、INFの公式サイトはこちら
動作環境のページにもあるように、INFにおいてUSB HIDゲームパッドとしては公式コントローラーのみ動作することになっています。サードパーティーの作成したUSB HIDゲームパッド(有名どころだとDAOコンとか)の動作はサポートされておらず、JoyToKeyなどによってキーボード入力に変換して使用するのが一般的です*1。キーボードによる操作はサポートされているためです。
まあアプリをいちいち立ち上げて変換するのが面倒だったり、変換の際の遅延が心配だったり、なによりキーボードには出来ない動作が仕様上存在していて*2不便だったりといった問題があるため、今回は公式コン準拠の動作をするゲームパッドを作っていきましょう。今のところ発売されている公式コンは

の2種類。今回はフル機能を有するプレミアムモデルの再現をしていきます。
公式コンは、つないでゲームを起動するだけで普通に使えるみたいです。一方、ほかのコントローラーは一切反応しません。やはりここにも何かしらの認証プロセスが存在しているようです。

コントローラーが満たすべき仕様

いきなり結論なのですが、INFはコントローラーのベンダーID/プロダクトIDしか見ていません。これらの値が正しくてHIDレポートが同じ形式になっていれば、とりあえず動きます。細かいところは違っていて大丈夫です。
ベンダーIDは公開情報なのでweb上のデータベースを引けばよくて、例えば以下のようなサイトを使うことで0x1CCFだとわかります。
USB ID Database::Vendor ID and Product ID list - the sz development
プロダクトIDは某掲示板で0x1CCFを手掛かりにちょっと調べると分かります。あるいは、DJ DAOの配布しているファームウエアをエディタで開き、0x1CCFに隣接する2バイトを読んできても良いです*3
f:id:DSKK:20200529184459p:plain
まあ、見ての通りです。リトルエンディアン(上位バイトが後)であることに注意。CF1C→1CCFと同じように、4880を2文字+2文字に区切ってからひっくり返して読んでください。
コントローラー仕様はDJ DAOが詳細に公開しちゃってます。怒られないのかな…
youtu.be
まず、デバイス仕様は「2軸16ボタン」ですね。
キーアサインは

1鍵: 1
2鍵: 2
3鍵: 3
4鍵: 4
5鍵: 5
6鍵: 6
7鍵: 7
E1:  9
E2: 10
E3: 11
E4: 12
ターンテーブル: X軸循環

のようになっています。
各種ディスクリプターとして用いることができるものの例です。

/* Device Descriptor */
    18,                // bLength
    1,                 // bDescriptorType
    0x00, 0x02,        // bcdUSB
    0,                 // bDeviceClass
    0,                 // bDeviceSubClass
    0,                 // bDeviceProtocol
    64,                // bMaxPacketSize0
    0xcf,              // idVendor[0]
    0x1c,              // idVendor[1]
    0x48,              // idProduct[0]
    0x80,              // idProduct[1]
    0x00, 0x10,        // bcdDevice (=version number)
    1,                 // iManufacturer
    2,                 // iProduct
    0,                 // iSerialNumber
    1                  // bNumConfigurations

/* Configuration Descriptor */
    9,                              // bLength;
    2,                              // bDescriptorType;
    34,
    0,                              // wTotalLength
    1,                              // bNumInterfaces
    1,                              // bConfigurationValue
    0,                              // iConfiguration
    0x80,                           // bmAttributes
    50,                             // bMaxPower
    // interface descriptor
    9,                              // bLength
    4,                              // bDescriptorType
    0,                              // bInterfaceNumber
    0,                              // bAlternateSetting
    1,                              // bNumEndpoints (is 2 if the device has INTR_IN)
    0x03,                           // bInterfaceClass (0x03 = HID)
    0x00,                           // bInterfaceSubClass (0x00 = No Boot)
    0x00,                           // bInterfaceProtocol (0x00 = No Protocol)
    0,                              // iInterface
    // HID interface descriptor
    9,                              // bLength
    0x21,                           // bDescriptorType
    0x11, 0x01,                     // bcdHID
    0,                              // bCountryCode
    1,                              // bNumDescriptors
    0x22,                           // bDescriptorType
    50,
    0,                              // wDescriptorLength
    // endpoint descriptor
    7,                              // bLength
    5,                              // bDescriptorType
    1 | 0x80,                       // bEndpointAddress (|0x80=in)
    0x03,                           // bmAttributes (0x03=intr)
    64, 0,                          // wMaxPacketSize
    4                               // bInterval

/* Report Descriptor */
    0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
    0x09, 0x04,        // Usage (Joystick)
    0xA1, 0x01,        // Collection (Application)
    0x09, 0x01,        //   Usage (Pointer)
    0xA1, 0x00,        //   Collection (Physical)
    0x05, 0x01,        //     Usage Page (Generic Desktop Ctrls)
    0x09, 0x30,        //     Usage (X)
    0x09, 0x31,        //     Usage (Y)
    0x15, 0x81,        //     Logical Minimum (-127)
    0x25, 0x7F,        //     Logical Maximum (127)
    0x75, 0x08,        //     Report Size (8)
    0x95, 0x02,        //     Report Count (2)
    0x81, 0x02,        //     Input (Data,Var,Abs)
    0x05, 0x09,        //     Usage Page (Button)
    0x19, 0x01,        //     Usage Minimum (0x01)
    0x29, 0x10,        //     Usage Maximum (0x10)
    0x15, 0x00,        //     Logical Minimum (0)
    0x25, 0x01,        //     Logical Maximum (1)
    0x75, 0x01,        //     Report Size (1)
    0x95, 0x10,        //     Report Count (16)
    0x81, 0x02,        //     Input (Data,Var,Abs)
    0x75, 0x08,        //     Report Size (8)
    0x95, 0x01,        //     Report Count (1)
    0x81, 0x03,        //     Input (Const,Var,Abs)
    0xC0,              //   End Collection
    0xC0               // End Collection

あとは実装するだけでちゃんと公式コントローラーとして認識されます。なお、レポートディスクリプタにもある通りHIDレポートは5バイトで、

1: X軸(signed char)
2: Y軸(signed char)
3: ボタン(1~7鍵盤。順にbit0~6に対応)
4: ボタン(E1~4。順にbit0~3に対応)
5: 定数(0x00)

という構造になっています。

f:id:DSKK:20200604094055p:plain
動作OK!右下のランプが公式コントローラーとして認識していることを示しています。
次回は実装の解説をやっていきます。

まとめ

PS3への対応と比べてずいぶん簡単なコントローラー仕様解析でした。デバイスディスクリプタの時点で異なる2つのファームウエアを同じボード上に共存させる方法について、次回の記事でソースコードを具体的に示しながら解説していきます。ちょっとだけテクいのでお楽しみに(?)
下のリンクからシェアやツイートなどしてもらえると大変執筆モチベが上がりありがたいです。良かったらぜひ。何か気になる点があったらコメントか
しありす (@cialis438) | Twitter
へのリプをお気軽にどうぞ。感想やコントローラー自作に関する雑談・質問なども歓迎です。

*1:ただし、DAOコンは後述するのと同様のやり方で公式コンとして認識されるようにしています。

*2:ファンクションボタンを使った、選曲時の操作

*3:コントローラーのファームウエアはデバイスディスクリプタを丸々含んでいるはずですよね。ここで、ベンダーIDとプロダクトIDは連続するバイト列なので…という発想。

PS2タイトルを自作USBコントローラーで遊ぶ

IIDX自作コントローラー記事のソフト編(その1)です。
CS弐寺*1を初期型PS3で遊ぶために、PS2エミュレータ*2上で自作コントローラーを動作させました。

自作コントローラーをPS3に認識させるには

PS3本体のファームウエアバージョンなんかにも依存していて難しいのですが、比較的新しいファームウエアでは適当に作ったHIDゲームパッドも認識されます。4軸+13ボタンで作れば、軸が左右のスティック(X, Y方向の動き*2つで4軸分です)に、ボタンが各ボタン(方向キーを除く)に対応します。
ただPSボタンだけは特殊で、対応する13番ボタン押下の信号を送っていても無視されてしまいます。どうやらエンドポイント0(=コントロール転送)におけるHID_GET_REPORTリクエストに特定のバイト列で応答するか本体側でチェックしているようです。簡単な認証が為されているという感じですね。
この事実は結構有名で、例えば
ps3-teensy-hid - USB hid device for the PlayStation3 using the Teensy platform
におけるmagic_init_bytesのような実装例が結構見つかります。逆に、これさえ実装していればPSボタンも正しく動作するようになります。めでたしめでたし…?

PS2タイトルの謎仕様

さっそく基板を試作し、ホーム画面・PS1タイトル・PS3タイトルの操作に成功したのですが、PS2タイトルだけは全く操作できないんですよね…なんで?(全ギレ)
PS2のソフトをやる時だけ「何故かコントローラーの接続が解除されてしまい、PSボタンを押してコントローラーを再接続する必要がある」という仕様、実機を触ったことがある方ならご存じだと思います。後でわかったのですが、実はこれってPS2の時だけコントローラーの認証が非常に厳しくなっているためで、非純正コンはここですべて振り落とされるようになっています。
オフィシャルに動作が保証されているのは標準の純正コントローラー、つまりDUALSHOCK 3とSIXAXISだけ。こいつらは機能がてんこ盛りなせいで動作が複雑で、認証をすり抜けられるほど高い再現度は達成されていません*3。さすがにDUALSHOCKの解析は大変そうなので、もっと単純なコントローラーで使えるものはないか調べてみました。
www.jp.playstation.com
…ありました。HORI製のRAPV3という格ゲーコンです。
格ゲー界隈はやはりコントローラーへのこだわりが強いようで、PS2タイトルでの動作報告が活発に上がっていたので助かりました。SIEの公式サイトで「オフィシャルライセンス商品」とされているコントローラーは新しめのファームウエアでサポートしているっぽいです。

RAPV3の解析

中古でコントローラーを手に入れ、パケットキャプチャによる解析を試みました。今回はコントローラーの接続先がPCではなくPS3なので、Wiresharkなどのアプリを立ち上げておくことができません。こういう時はUSBスニファを使います。
USBスニファとは、通ったUSBのパケットをすべて記録しつつ中継する装置です。beagleboard xMというシングルボードコンピュータ(Linuxが動くボード。ラズパイみたいなやつ。)を使いました。とりあえず写真を見てもらうのが早いと思います。
f:id:DSKK:20200529111819p:plain
PS3-スニファ-コントローラーという接続になっています。LANケーブルはパケットキャプチャそのものには関係ありません。
このボードはUSBのホスト側とデバイス側両方の口を持っているので、本来USBケーブルで直結されるPS3とコントローラーの間に挿入することができます。
f:id:DSKK:20200529112038j:plainf:id:DSKK:20200529112048j:plain
USB mini-Bがデバイスとして振舞える口。標準Aコネクタにはデバイス(今回の場合コントローラー)をぶら下げられます。
パケットは速やかに(かつ、変更を受けずに)スニファをパスするので、PS3やコントローラーは直結時と変わらない通信ができます。また、beagleboard xM自体の制御は他PCからのSSH接続で行えます(LANケーブルはこのためにつないでいます)。
github.com
今回焼くファームウエアとしては、これをそのままありがたく使わせてもらいます。作者はGIMXの管理人をしているMatlo氏*4カーネルレベルでUSBパケットの中継を実装してくれているらしく、漏れや遅延が起こりにくそうです。./sniffコマンドでパケットをすべてdumpしてくれるのすごい。
さて、どんな認証が行われているのかとワクワクでdumpファイルを見たのですが…

特別なパケットは何もやり取りされていない

f:id:DSKK:20200529115408p:plain
PS2タイトル起動後にコントローラーを接続したときの挙動。デバイスディスクリプタコンフィギュレーションディスクリプタの要求のあと、セットコンフィギュレーションが行われデバイスが使用可能になっている。あとはひたすらインタラプト転送のpollとresponseが続く。
はい。いたって普通に通信しているようにしか見えませんでした。え、なにこれは…

うーん…では逆に、PS2で使えないコントローラーはどうなっているんでしょう?
今度はELECOM製のJC-U4013Sという、普通のゲームパッド(ホーム画面やPS3タイトルの操作は可能)をPS2タイトル起動後に接続したときの挙動を見てみました。

f:id:DSKK:20200529120412p:plain
セットコンフィギュレーションが来ていない!
バイスディスクリプタコンフィギュレーションディスクリプタを見た時点で通信をやめています。オフィシャルライセンス商品についてはこれらのディスクリプタホワイトリスト形式で持っていて、合致しなければガン無視を決め込む仕様っぽいです*5
これはなるほどという感じ。オリジナル要素のあるコントローラーが動かないわけだ。

自作コントローラーの仕様確定

要するに、

なら、PS2タイトルでもホーム画面でもフルで機能するってことですね。ディスクリプタ類は以下の通りです。

/* Device Descriptor */
 18,                // bLength
 1,                 // bDescriptorType
 0x00, 0x02,        // bcdUSB
 0,                 // bDeviceClass
 0,                 // bDeviceSubClass
 0,                 // bDeviceProtocol
 64,                // bMaxPacketSize0
 0x0d,              // idVendor[0]
 0x0f,              // idVendor[1]
 0x22,              // idProduct[0]
 0x00,              // idProduct[1]
 0x00, 0x10,        // bcdDevice (=version number)
 1,                 // iManufacturer
 2,                 // iProduct
 0,                 // iSerialNumber
 1                  // bNumConfigurations

/* Configuration Descriptor */
 9,                              // bLength;
 2,                              // bDescriptorType;
 0x29,
 0x00,                           // wTotalLength
 1,                              // bNumInterfaces
 1,                              // bConfigurationValue
 0,                              // iConfiguration
 0x80,                           // bmAttributes
 50,                             // bMaxPower
 // interface descriptor
 9,                              // bLength
 4,                              // bDescriptorType
 0,                              // bInterfaceNumber
 0,                              // bAlternateSetting
 2,                              // bNumEndpoints
 0x03,                           // bInterfaceClass (0x03 = HID)
 0x00,                           // bInterfaceSubClass (0x00 = No Boot)
 0x00,                           // bInterfaceProtocol (0x00 = No Protocol)
 0,                              // iInterface
 // HID interface descriptor
 9,                              // bLength
 0x21,                           // bDescriptorType
 0x11, 0x01,                     // bcdHID
 0,                              // bCountryCode
 1,                              // bNumDescriptors
 0x22,                           // bDescriptorType
 0x89,
 0x00,                           // wDescriptorLength
 // endpoint descriptor
 7,                              // bLength
 5,                              // bDescriptorType
 2,                              // bEndpointAddress (ep2,out)
 0x03,                           // bmAttributes (0x03=intr)
 64, 0,                          // wMaxPacketSize
 10,                              // bInterval
 // endpoint descriptor
 7,                              // bLength
 5,                              // bDescriptorType
 0x81,                           // bEndpointAddress (ep1,in)
 0x03,                           // bmAttributes (0x03=intr)
 64, 0,                          // wMaxPacketSize
 10                              // bInterval

/* Report Descriptor */
 0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
 0x09, 0x05,        // Usage (Game Pad)
 0xA1, 0x01,        // Collection (Application)
 0x15, 0x00,        //   Logical Minimum (0)
 0x25, 0x01,        //   Logical Maximum (1)
 0x35, 0x00,        //   Physical Minimum (0)
 0x45, 0x01,        //   Physical Maximum (1)
 0x75, 0x01,        //   Report Size (1)
 0x95, 0x0D,        //   Report Count (13)
 0x05, 0x09,        //   Usage Page (Button)
 0x19, 0x01,        //   Usage Minimum (0x01)
 0x29, 0x0D,        //   Usage Maximum (0x0D)
 0x81, 0x02,        //   Input (Data,Var,Abs)
 0x95, 0x03,        //   Report Count (3)
 0x81, 0x01,        //   Input (Const,Array,Abs)
 0x05, 0x01,        //   Usage Page (Generic Desktop Ctrls)
 0x25, 0x07,        //   Logical Maximum (7)
 0x46, 0x3B, 0x01,  //   Physical Maximum (315)
 0x75, 0x04,        //   Report Size (4)
 0x95, 0x01,        //   Report Count (1)
 0x65, 0x14,        //   Unit (System: English Rotation, Length: Centimeter)
 0x09, 0x39,        //   Usage (Hat switch)
 0x81, 0x42,        //   Input (Data,Var,Abs)
 0x65, 0x00,        //   Unit (None)
 0x95, 0x01,        //   Report Count (1)
 0x81, 0x01,        //   Input (Const,Array,Abs)
 0x26, 0xFF, 0x00,  //   Logical Maximum (255)
 0x46, 0xFF, 0x00,  //   Physical Maximum (255)
 0x09, 0x30,        //   Usage (X)
 0x09, 0x31,        //   Usage (Y)
 0x09, 0x32,        //   Usage (Z)
 0x09, 0x35,        //   Usage (Rz)
 0x75, 0x08,        //   Report Size (8)
 0x95, 0x04,        //   Report Count (4)
 0x81, 0x02,        //   Input (Data,Var,Abs)
 0x06, 0x00, 0xFF,  //   Usage Page (Vendor Defined 0xFF00)
 0x09, 0x20,        //   Usage (0x20)
 0x09, 0x21,        //   Usage (0x21)
 0x09, 0x22,        //   Usage (0x22)
 0x09, 0x23,        //   Usage (0x23)
 0x09, 0x24,        //   Usage (0x24)
 0x09, 0x25,        //   Usage (0x25)
 0x09, 0x26,        //   Usage (0x26)
 0x09, 0x27,        //   Usage (0x27)
 0x09, 0x28,        //   Usage (0x28)
 0x09, 0x29,        //   Usage (0x29)
 0x09, 0x2A,        //   Usage (0x2A)
 0x09, 0x2B,        //   Usage (0x2B)
 0x95, 0x0C,        //   Report Count (12)
 0x81, 0x02,        //   Input (Data,Var,Abs)
 0x0A, 0x21, 0x26,  //   Usage (0x2621)
 0x95, 0x08,        //   Report Count (8)
 0xB1, 0x02,        //   Feature (Data,Var,Abs)
 0x0A, 0x21, 0x26,  //   Usage (0x2621)
 0x91, 0x02,        //   Output (Data,Var,Abs)
 0x26, 0xFF, 0x03,  //   Logical Maximum (1023)
 0x46, 0xFF, 0x03,  //   Physical Maximum (1023)
 0x09, 0x2C,        //   Usage (0x2C)
 0x09, 0x2D,        //   Usage (0x2D)
 0x09, 0x2E,        //   Usage (0x2E)
 0x09, 0x2F,        //   Usage (0x2F)
 0x75, 0x10,        //   Report Size (16)
 0x95, 0x04,        //   Report Count (4)
 0x81, 0x02,        //   Input (Data,Var,Abs)
 0xC0               // End Collection

HIDレポートは27バイトありますが、初めの3バイトだけが変化します。ボタン等との対応は以下の通りです。後ろの24バイトはおそらくPS3の要求する仕様を満たすためにダミーを送っているのでしょう*6

1バイト目:
  R2, L2, R1, L1, △, 〇, ×, □

2バイト目:
   -,  -,  -, PS, R3, L3, START, SELECT

3バイト目:
  ハットスイッチ(POV)。対応は以下の通り。
  0x00 = Up
  0x01 = Up+Right
  0x02 = Right
  0x03 = Down+Right
  0x04 = Down
  0x05 = Down+Left
  0x06 = Left
  0x07 = Up+Left
  0x0f = None

1バイト目、2バイト目は左のものほど上の位です。例えば△と□が押されていると1バイト目は0x09になります。
後ろの24バイトは以下の通り固定です。
0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02

また、弐寺の鍵盤とプレステのボタンの対応は

1: □
2: L1
3: ×
4: R1
5: 〇
6: L2
7: ←

のようになっています。あとはこれを実装すればOKです。

まとめ

内容が盛りだくさんになってしまいましたが、とにかくマネしたいコントローラーを見つけてその動作を丁寧に解析・実装するということに尽きます。次回はINFINITASに対応したファームウエアの仕様について同様に解説し、次々回でそれら2つのファームウエアを共存させる実装方法について述べていくので、具体的なソースコードの解説はその時に。
下のリンクからシェアやツイートなどしてもらえると大変執筆モチベが上がりありがたいです。良かったらぜひ。何か気になる点があったらコメントか
しありす (@cialis438) | Twitter
へのリプをお気軽にどうぞ。感想やコントローラー自作に関する雑談・質問なども歓迎です。

*1:beatmania IIDXPS2移植版。いわゆる家庭用。これとか。

*2:PS3PS2後方互換機能で、最初に発売されたモデル(CECHAxxとCECHBxx。分厚くてつやつやしたやつ)にだけ搭載されている。

*3:世界最大の自作コントローラーコミュニティ、GIMXでも未解決でした。 2015年の投稿 で少しだけ話題に上がりましたが、作者が実機を持っておらず進展がないまま放置。ちなみに2020年の投稿は私によるものです。本記事のような知見をシェアして貢献できると良いなあ。

*4:コントローラー解析と実装のガチプロでGIMXと関連プロジェクトを素晴らしくメンテしているので尊敬している。いろいろなコメントに対する返答も丁寧なナイスガイ。

*5:余談ですが、HIDレポートディスクリプタはRAPV3と全く一緒でした。サードパーティーが別々にコントローラー開発したとして、1バイトも違わず偶然一致するものではないと思うのだけどいいのか…?

*6:レポートディスクリプタによれば軸が1バイト×4、vendor_definedが1バイト×12+2バイト×4。vendor_definedの前者は感圧センサ12ボタン分、後者は加速度センサ4軸分らしい。

beatmania IIDX コントローラーの製作

皆さま、自粛期間いかがお過ごしでしょうか。音ゲーマーにとってはゲーセンの閉店が一番きついかもしれませんね。
というわけで、beatmania IIDX(以下、弐寺)のコントローラー自作について書いていこうかと。初回は目次を兼ね、今後投稿する予定の記事のダイジェストみたいなものを投稿します。3~5記事くらいでしょうか。

はじめに

自作とはいいますが、正確には「ごく一部を残して大幅に改造」です。このコントローラーはもともとDAOコンでした。
現存する最も古い姿の写真です(非常に良い条件で譲って頂いて、ありがたい限りです…)
これでも機械的な部分のオーバーホール(ターンテーブル軸のベアリングのグリス交換、底面パネルのネジ山修整、断線したケーブルを新品でリプレース、ボルトとナットを全てステンレス製の新品に交換、アームレストの布張り替えなど…)が終わったところです。この辺楽しくないと思うのでブログ記事にはしません。写真を一枚だけ。
f:id:DSKK:20200424163527j:plain
全部剥きました。この後地味な作業を無限にやります。
このときついでに背面パネルも改造済みです。しかしまだまだ以下のような不満点があります。

  • ポーリングレートなど性能の決して高くない&ケーブル着脱式にするのが難しいプレステ2コントローラーとして回路が作製されている
  • 基板設計が全体的にイケていない。コンバーターを噛ませない限りプレステ2にしか対応しないし、ボタンを光らせる必要がないときでも補助電源の接続が必要
  • ターンテーブルの表面処理がアーケードとかなり違ってちょっとダサい
  • 皿の感度が悪い
  • 最新のINFINITASコントローラーはファンクションキーが4つあるが、2つしか実装されていない

などなど。

とりあえず、一番困ったのは時代遅れなプレステ2コントローラーである点ですね。コンバーターの遅延を気にしながら音ゲーをするのはキツいので基板ごと入れ替えてしまいましょう。現在、このコントローラーが対応しなければならない用途は以下の3つです。

  • CS弐寺: プレステ2タイトルですね。
  • INFINITAS: コナミ公式が配布、運営するPCゲームです。
  • BMS: やねうらお氏が規格を制定したことで有名ですね。有志による実装が何種類かそれぞれPCゲーとして配布されています。

見ての通り、一番上がプレステ2で他がPCなわけですが、少し考えたらプレステ3初期型でプレステ2タイトルがプレーできることに思い至りました。これならUSB接続のみに絞ることが出来てハッピーです。
しかしPS3とINFINITASはコントローラーがオフィシャルなものかチェックされるため普通にコントローラーとして実装しても非公式品とバレてしまい動きません。コントローラーとしての仕様もそれぞれ異なるため適当な出来合いの基板ベースで作れない、困った…

マイコンプログラミング編

仕方がないのでUSB機能の載ったマイコンの勉強から始めましょう(は?)。全部自分で書いた方が早いです。ピン数が多くてUSBトランシーバーが内蔵されている、そんなに高くないマイコンとして個人的に一番オススメなのは
AT90USB646 - 8-bit AVR Microcontrollers
これですかね。チップ自体は500円ちょっと、48ピンもGPIOが引き出せます。

実装すべきファームウエアは実機のリバースエンジニアリングで得るよりほかありません。自作コントローラーの最大のコミュニティであるGIMXのadmin、matloさんがUSBスニファの使い方などを公開してくれているので、ありがたく使わせて頂きましょう。
github.com
USBのホスト側とゲスト側、両方のコネクタを持つBBBというミニPCをUSBの信号中継器のように使って、通ったパケットを全部dumpする感じですね。USBケーブルを使って通信している以上、原理上コントローラーの全ての動作が確認できます。ビューワにはWiresharkを使えて非常に便利です。[PS3]-[BBB]-[コントローラー]という構成でPS3との通信を監視します。結果から言うと特別なパケットのやり取りなどはしておらず(PS3のコントローラーって怪しいVendor definedのUsageをいくつか持っているので疑っていましたが…)、USB HID descriptorとDevice descriptorが同一であればPS3が「既知のコントローラー」として認識してくれるという感じみたいです。オフィシャルの、動くコントローラーからデバイスの情報をベタ書き移植すれば十分動きます。この点、実はGIMXコミュニティーでも知られていなかったようなのでフォーラムに投稿しましておきました。ニッチながら世界初です(?)

お次はマイコンプログラミングですが、5年位前から自作コン自体は作りたくて、マイコンの勉強ちまちましていて本当によかったです。450ページもあるデータシートとにらめっこしつつ、実装します。普段のC++を使ったプログラミングと比べて変わったところがあるとすればレジスタの扱いを強く意識しなければならないのと、ほとんどRAMがない(64kBとか)ってところでしょうか。小さめの簡単なデバイスから順に作って経験を積むうちに大抵扱えるようになりましたが、このマイコンが問題なく扱えるようになるまで年単位で時間がかかってしまいましたね。USB2.0の規格そのものの勉強(バイブル)も結構必要でしたし。
メモリも限られているためUSB HIDとしてのディスクリプタは通常定数としてハードコードしますが、複数のファームウエアを切り替えながら動作することを想定し、全てPROGMEMへのポインタという形で持っておき実行時に参照します。これでメモリを食わずにデカい定数をハードコードすることを避けられます。あとは起動時にポインタにアドレスをセットするようにすればUSB機器としてのファームウエアを切り替えることができます。このトリックについては後日また改めて記事にします。ともあれできたコードがこれ。
github.com
ついでに他機種にも対応可能な汎用性を持たせてみました。コントローラー製作の一番の推しポイントなので詳しめの記事に出来るよう頑張ります。

回路編

あとは基板を拵えます。どうせスイッチ繋ぐだけなので、回路は至ってシンプル。ほとんどワンチップで済んでいます。メンテ性を考慮してメインボードとコネクタボードを分け、それらをケーブルでつなぐことにしました。
f:id:DSKK:20200424170914p:plain
メインボード
f:id:DSKK:20200424170933p:plain
コネクタボード
元所属ラボの基板加工機を借りて生基板から手作りします。
f:id:DSKK:20200424171227j:plain
完成した基板。それぞれ表と裏。
はんだ付け、ファームウエア書き込みなど済ませて組み込みます。
f:id:DSKK:20200424175202j:plain
コントローラー内部。コネクタ同士のクリアランスが広がりメンテ性も向上。

残りいろいろ編 記事書くの飽きた

かなり手を入れたので詳細は個別記事に譲りますが、皿など各種パーツを自作し取り付けて完成!良い自粛ライフになりますように。
f:id:DSKK:20200424175322j:plain
完成したコントローラーの天面。
f:id:DSKK:20200424175353j:plain
こだわりの背面。すっきりした構成に。
f:id:DSKK:20200428141734j:plain
最強になりました
プレー動画もそのうち。というかYoutube等でたまに配信できるといいですね~

AtCoder Beginner Contest 104

真面目にやり始めて日が浅いので、得た学びは遡り切れるだろうと考えて遡る。忙しくはないけど院試などがあり、微妙に時間がなくて悲しい。これも出られていないけどDだけ解いた。
コンテストページ: AtCoder Beginner Contest 104 - AtCoder

D: We Love ABC

  • 'A','B','C','?'(ワイルドカード)のみから成る文字列が与えられる。
  • '?'をそれぞれ好きに'A','B','C'のどれかに置き換えると、'?'の個数を Qとして 3^Q通りの文字列ができる。
  • それぞれの文字列 Tについて部分列(連続していなくても良い)*1として"ABC"が取れる箇所を数え、その個数を足しあげよ。

という問題。
入力例1の"A??C"がわかりやすいが、 Tが"ABAC","ABBC","ABCC"のそれぞれのパターンにおいて、0,1,3文字目を取ったものがカウントされている。これを重複してカウントしないものだとしばらく誤読していた。Ratedならまあまあ悲しくなっていたと思うので、入力例をよく見る癖をつけるべき。

ただ、実はこの誤読した問題、すなわち

  •  Tの部分列(連続していなくても良い)のうち、"ABC"となりうる箇所は何箇所あるか?

が解ければあとはそんなに難しくない。それぞれの「"ABC"となりうる箇所」は、 Tの残り |T|-3文字に'?'が R個含まれているとすると 3^R回重複して数えられており*2、この重みをつけて足し上げて行けばよいためである。'?'は全部で Q個なのだから、「"ABC"となりうる箇所」に含まれる'?'の数を S (0 \le S \le 3)としたとき R = Q - Sが成立し、この重み付けの係数は簡単に求まる。

「"ABC"となりうる箇所」として有り得る"???","??C","?B?","A??","?BC","A?C","AB?","ABC"の8通りについて Tから3文字選ぶような全探索を書くと O(|T|^3)で無理だが、後ろから"??","?C","B?","BC"の個数を数えていき、'A'または'?'に出会ったときにこの4通りについて処理をすれば O(|T|)。ひとまずこの方針で、以下に示すようなイケてないコードが出来上がり、通すことはできる。これではちょっとあんまりなので、よりまともな解法を検討していく。
Submission #3086889 - AtCoder Beginner Contest 104

mod = 1000000007
s = input()
n = len(s)
c = [0 for i in range(n)] #c[i]=(s[i:]に存在する'C'の個数)
d = [0 for i in range(n)] #d[i]=(s[i:]に存在する'?'の個数) 以降も、'd'は'?'に対応する
bc = [0 for i in range(n)] #bc[i]=(s[i:]に存在する"BC"の個数)
bd = [0 for i in range(n)] #上と同様、"B?"
dc = [0 for i in range(n)] #"?C"
dd = [0 for i in range(n)] #"??"


if s[-1] == "C":
    c[-1] = 1
if s[-1] == "?":
    d[-1] = 1
for i in range(-2, -n-1, -1): #後ろから2文字目~先頭を見る
    c[i] = c[i+1]
    d[i] = d[i+1]
    bc[i] = bc[i+1]
    bd[i] = bd[i+1]
    dc[i] = dc[i+1]
    dd[i] = dd[i+1]
    if s[i] == "B":
        bc[i] += c[i+1] #今見つけた'B'と、今までに見つけた'C'で"BC"をつくる。後に続くのも同様。
        bd[i] += d[i+1]
    elif s[i] == "C":
        c[i] += 1
    elif s[i] == "?":
        d[i] += 1
        dc[i] += c[i+1]
        dd[i] += d[i+1]

# d[0]は'?'の総数で、問題文中のQに対応する
if d[0] == 0: #d[0]が3に満たないときはバグるので別扱い
    mul = [0, 0, 0, 1]
elif d[0] == 1:
    mul = [0, 0, 1, 3]
elif d[0] == 2:
    mul = [0, 1, 3, 9]
else:
    mul = [1, 1, 1, 1] #以下の処理でmul=[3**(Q-3),3**(Q-2),3**(Q-1),3**Q]となる
    for i in range(d[0]-3): #3をQ-3乗
        mul[0] *= 3
        mul[0] %= mod
    for i in range(3): #あとは順に3倍
        mul[i+1] = (mul[i]*3) % mod

res = 0
for i in range(n-2): #一文字ずつ見ていって足し上げる
    if s[i] == "A":
        res += bc[i+1]*mul[3]+(dc[i+1]+bd[i+1])*mul[2]+dd[i+1]*mul[1]
    if s[i] == "?":
        res += bc[i+1]*mul[2]+(dc[i+1]+bd[i+1])*mul[1]+dd[i+1]*mul[0]
    res %= mod
print(res)

こちらhttps://www.hamayanhamayan.com/entry/2018/08/05/232127で取り上げられているように、最後の数え上げはもうちょっと楽に書ける定石があるらしい。 1\le i \le |T|として、前から i文字目までの'A'の個数 aと'?'の個数 l、後ろから i文字目までの'C'の個数 cと'?'の個数 rを初めに数えて持っておくと、改めて前から見ていって T j文字目が'B'のときに 3^Qa_{j-1}c_{j+1}+3^{Q-1}(l_{j-1}c_{j+1}+a_{j-1}r_{j+1})+3^{Q-2}l_{j-1}r_{j+1}個をカウントしていけば良いことになる( k文字目が'?'のときも同様だが、ワイルドカードが一個減るので指数を1つずつ減らす)。計算量は変わらないものの、これで素直に探索を書くことができるようになる。半分全列挙とかでもそうだが、端からではなく真ん中を決めてみると楽になることが結構ありそうなので、この感覚は身につけておきたい。

想定解はDP。全探索書かないならDPだろうなとは思うけど、解説の通りのスッキリしたDPテーブルを書ける感じがしないので、これくらいの問題で練習しておくと良い練習になりそう。勉強のために、ほぼ引き写しになるが書いておく。文字列を前後反転して処理すると簡単なので、想定解とはちょっとだけ表現やDPテーブルの対応が異なる。
 dp_{i,j} (0 \le i \le |T|, 0 \le j \le 3)は「 Tの後ろから i文字目までの中から、"ABC"の後ろ j文字を選ぶ場合の数」とする。求めるのは dp_{|T|,3} Tの前後を反転させた文字列を改めて Tとして取り直すと、 dp_{i,j}が「 Tの前から i文字目までの中から、"CBA"の前 j文字を選ぶ場合の数」となり少し書きやすい。この場合も dp_{|T|,3}を求めればよい。
推移を考えていく。特に i+1文字目が'?'のときは

  •  dp_{i+1,0}=3dp_{i,0}
  •  dp_{i+1,1}=3dp_{i,1}+dp_{i,0}
  •  dp_{i+1,2}=3dp_{i,2}+dp_{i,1}
  •  dp_{i+1,3}=3dp_{i,3}+dp_{i,2}

となる(後ろ三行の右辺にある二項のうち、後者は'?'を"ABC"の列を伸ばすために使っているので1通りしか使い方がない一方、前者は何でもよい文字として使っているので3通り使い方がある)。
その他の場合、 i+1文字目を無視すれば dp_{i+1,j}  dp_{i,j}と等しくなるのでとりあえず前の値をコピーする。コピー後に i+1文字目が何だったかに着目して増分を足してやれば良くて、

  • 'C'のとき、 dp_{i+1,1}+=dp_{i,0}
  • 'B'のとき、 dp_{i+1,2}+=dp_{i,1}
  • 'A'のとき、 dp_{i+1,3}+=dp_{i,2}

となる。
やっていることは意外に全探索と変わりがないが、見通しが良いしインデックスがテーブルの推移にそのまま使えるので以下のようにコードが見やすくなる。
Submission #3086882 - AtCoder Beginner Contest 104

mod = 1000000007
s = input()[-1::-1] #入力のタイミングで文字列を反転してしまう
n = len(s)
dp = [[0]*4 for i in range(n+1)]
dp[0][0] = 1 #0文字から0文字選ぶ方法だけ1で初期化。他は0。
for i in range(n):
    for j in range(4):
        dp[i+1][j] = dp[i][j] % mod #とりあえず一つ前の値はコピー
    if s[i] == "?":
        #dp_{i+1,0}だけは一項少ないので別扱い。
        dp[i+1][0] += (2*dp[i][0]) % mod #コピーしたものに2倍を足せば合計3倍。
        for j in range(1, 4):
            dp[i+1][j] += (2*dp[i][j]+dp[i][j-1]) % mod
    else:
        index = "CBA".find(s[i])+1 #indexを1-originにするために1を加えている
        dp[i+1][index] += dp[i][index-1] % mod
print(dp[n][3] % mod)
所感

前半のアホみたいな解法はまあまずコーナーで落ちないのでその点ではよく、こどふぉやってると上手になってくる感じがある。
とはいえあんな解法ではアレなので(語彙力)、DP上手になりたい。

*1:数列だとこれを部分列と呼んでいる例を見たことあるけど、文字列でも言うのかな・・・"部分文字列"は連続していなければならない気がなんとなくする。

*2:この"ABC"3文字分だけ確定すると、残りの文字列の作り方は何通りあるか?という考え方。作りうる全ての文字列においてこの"ABC"はABC数に寄与している。