2024年11月26日(火) [長年日記]
■ [android][net] AndroidのBLEを使った通信で512バイトを超える送信データが破棄される
AndroidのBluetooth APIを使ったBLE通信で、送信データが破棄されれ送信されないことがあったので調べた。
原因はAndroid 13と14での動作の変更だった。
Android 13での変更
ちゃんとした資料は見つけられなかったが、Android 13から512バイトを超えるデータを送受信できなくなったというissueがちらほら見つかる。これなど。
次の変更が原因のようだ。
- Set maximum attribute value to defined by spec Core 5.3 (android.googlesource.com)
この変更は、次に示すBluetoothの仕様でattribute valueの最大長が512バイトに制限されていることへ準じるために行われている。
- 3.2.9. Long attribute values (Bluetooth Core Specification 6.0)
The maximum length of an attribute value shall be 512 octets.
どうも、Bluetoothでデータ送信を行う際のデータ長の上限としてATT(Attribute Protocol)のMTUサイズから3バイト引いた値を使うコードが多いようで(手元のコードもそうなっていた)、AndroidのATT MTUサイズは最大で517バイトになり得て、そうなったときに 517 - 3 = 515 バイトのデータを送信しようとしてしまい、その結果Android 12までであれば送信できていたのにAndroid 13になったら送信できなくなったという問題があるようだ。
Android 14での変更
Android 14では次の変更がされたとある。
- MTU is set to 517 for the first GATT client requesting an MTU (Android Developers)
これは、BluetoothGatt#requestMtu()によりMTUサイズの拡張を要求したときに引数の値によらず常に517バイトが使われるということのようだ。
手元のコードでは、上述したAndroid 13での変更に対する対策として BluetoothGatt#requestMtu() で515を使うようになっていたが、この対策はAndroid 14以降では意味がなくなってしまうことになる。
変更への対応方法
送信データの長さの上限を (MTUサイズ - 3) とするだけでは問題があるということになる。
送信データの長さの上限は (min(512, MTUサイズ - 3)) とする必要がある。
AndroidのATT MTUサイズの上限が517バイトなのは何故か
検索すると、AndroidのATT MTUサイズの上限は517バイトではなく515バイトであるべきだと書いている人を見かける。これに対しては、次のissueにGoogleの人による説明があった。
- [BLE] - Incoming packets longer then 515 dropped after BluetoothGatt.requestMtu(517) used, even though remote peer supports MTU 517 (Android Public Tracker)
I would like to provide you more details about why 517 was chosen instead of 515:
The number 517 was selected because the GATT_MAX_MTU_SIZE constant was set to 517 since the initial drop of Android Bluetooths stack in 2012.
A deeper reason is that the Bluetooth Specification allows the maximum size of an ATT attribute to be 512 bytes and the largest command ATT_PREPARE_WRITE_REQ has 5 bytes of header. Hence 512 + 5 = 517.
2024年10月08日(火) [長年日記]
■ [python] SQLModelのdatetimeに対する動作
どうしてそうなるのかは理解できていないが、事実をメモ。
SQLModel0.0.22を使っている。
datetime型のフィールドを持つモデルを定義して、そのmodel_validate_json()によりJSONからのでシリアライズを行ったときに次のようになる。
- table=True無しで定義したモデルはdatetime型のフィールドにdatetime型の値が格納される
- table=True有りで定義したモデルはdatetime型のフィールドにstr型の値が格納されてしまう
検証コードと実行結果を示す。
from datetime import datetime from sqlmodel import Field, SQLModel class A(SQLModel): id: int = Field(primary_key=True) dt: datetime class B(SQLModel, table=True): id: int = Field(primary_key=True) dt: datetime json = '{ "id": 1, "dt": "2024-01-02T03:45:06+09:00" }' a = A.model_validate_json(json) b = B.model_validate_json(json) print(type(a.dt)) print(type(b.dt))
<class 'datetime.datetime'> <class 'str'>
(追記)
上記のようになるのは、データベースとして使用していたのがSQLiteで、SQLiteのdatetime型はtext型と同じだからのようだった。
2024年10月01日(火) [長年日記]
■ [windows][net][howto] WSLにポートフォワードで外部からアクセス可能にする
WSLに外部からSSHでアクセスできるようにする方法をメモ。なお、試した環境はWindows 11ではなくWindows 10。
WSL上でsshdを使えるようにする方法は割愛。WSLをホストしているWindowsからはWSLへSSH接続できるものとする。
ポートフォワーディング設定
Windowsホスト上でnetsh interface portproxyコマンドを使ってWSLへのポートフォワーディングを設定する。なお、この設定には管理者権限が必要。
> netsh interface portproxy add v4tov4 ` listenport=22222 listenaddress=0.0.0.0 ` connectport=22 connectaddress=192.0.2.100
設定の確認はshow allサブコマンドでできる。
> netsh interface portproxy show all ipv4 をリッスンする: ipv4 に接続する: Address Port Address Port --------------- ---------- --------------- ---------- 0.0.0.0 22222 192.0.2.100 22
これにより、Windowsホストのポート22222へのアクセスはWSL(のアドレスを仮に192.0.2.100とした)のポート22へ転送されるようになる。
ファイアウォール設定
上記に加え、Windowsのファイアウォール設定を変更しないとアクセスできなかった。ファイアウォールの設定はnetsh advfirewallコマンドを使って行える。こちらも要管理者権限。
> netsh advfirewall firewall add rule ` name="WSL-SSH" ` protocol=TCP ` localport=22222 ` dir=in ` action=allow
設定の確認は次の通り。
> netsh advfirewall firewall show rule name=WSL-SSH 規則名: WSL-SSH ---------------------------------------------------------------------- 有効: はい 方向: 入力 プロファイル: ドメイン,プライベート,パブリック グループ: ローカル IP: 任意 リモート IP: 任意 プロトコル: TCP ローカル ポート: 22222 リモート ポート: 任意 エッジ トラバーサル: いいえ 操作: 許可 OK
以上の設定で外部からWSLのSSHへのアクセスができるようになった。
設定の削除方法
ファイアウォール設定とポートフォワード設定を削除して元に戻すには次のようにする。
> netsh advfirewall firewall delete rule name=WSL-SSH 1 規則を削除しました。 OK
> netsh interface portproxy delete v4tov4 listenport=22222 listenaddress=0.0.0.0
NetNat系コマンドとの違い
以前にHyper-V上のVMへ外部からアクセスする方法をメモしていて、このときはnetshではなくAdd-NetNatStaticMappingコマンドなどを使っていた。
WSLについても同じようにしてできるのかもしれないが、netshを使うと仮想スイッチの設定を気にしなくていいのでこちらの方がWSL向きな気がする。
違いは理解できていない。
2024年09月27日(金) [長年日記]
■ [python] 作成するWheelファイルのファイル名
昨日の例で作られるWheelファイルのファイル名は
- oreore-0.1.0-py3-none-any.whl
になる。これの「py3-none-any」の部分はPlatform compatibility tagsと呼ばれ、
- python tag
- abi tag
- platform tag
を並べたものになっている。
Wheelが特定のPythonバージョンや特定のプラットフォームを要求する場合、これらのタグを適切に指定する必要があるが、作成するファイル名を制御するにはどうすればいいのか?
方法1: buildの--config--settingオプションを使用する
昨日の例ではWheelの作成にpipを使用したが、pipでファイル名を制御する方法はわからなかった。buildを使えばある程度制御できて、昨日の環境で次のコマンド
$ python -m build . --wheel \ --config-setting="--build-option=--python-tag hello --plat-name world"
を実行すると
- oreore-0.1.0-hello-none-world.whl
というファイル名のWheelが作られる。
abi tagを指定する方法は分からなかった。また、確認したビルドバックエンドはSetuptoolsだけで、別のバックエンドを使う場合は別の指定方法になると思われる。
方法2: wheel tagsコマンドでファイル名を変更する
wheelというパッケージがあり、これをインストールするとwheel tagsというコマンドを使えるようになって、これでWheelのファイル名の変更が簡単にできる。
$ python -m wheel tags oreore-0.1.0-py3-none-any.whl \ --python-tag=hello --platform-tag=world --abi-tag=abc
上のコマンドを実行すると次のファイル名のWheelが作られる。abi tagも指定できる。
- oreore-0.1.0-hello-abc-world.whl
適切なタグ名を取得する
各タグの名前を取得するのには、packagingというパッケージが使える。
例えば、動作しているPython環境に一番適合したタグ名は packaging.tags.sys_tags() により取得できる。
>>> import packaging.tags >>> print(next(packaging.tags.sys_tags())) cp312-cp312-manylinux_2_35_x86_64 >>>
2024年09月26日(木) [長年日記]
■ [python] Pythonにおけるパッケージング(2)
7年前にPython 2でパッケージ化する方法をメモしていた。少し前にPython 3でパッケージを作る方法を調べたので、忘れないようにメモしておく。
情報源が https://packaging.python.org/ であることは変わっていない。日本語版もある。
pyproject.tomlを用意
現代ではsetup.pyの代わりにpyproject.tomlを使う。ここに次のような内容を設定する。
[project] name = "oreore" version = "0.1.0" [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta"
[build-system] に指定可能なツールが色々あって悩みどころだが、本記事ではSetuptoolsを使う。
Pythonのコードを用意
以前と同じ構成のコードを用意する。ただし、コードは「src/パッケージ名」というディレクトリの下に置くのが標準的なようなのでそのようにする(参考:src レイアウト対フラットレイアウト)。
$ tree . --noreport . ├── pyproject.toml └── src └── oreore ├── hello │ ├── __init__.py │ └── hello.py └── world ├── __init__.py └── world.py
前回同様hello.pyを実行可能スクリプトと認識させたかったが、pyproject.tomlでそのような指定をする方法は無さそうだった。代わりに、hello.pyの関数を呼び出す実行可能スクリプトを自動生成させることはできて、そのためにはpyproject.tomlに次のような設定を追加すればよい(参考:Entry Points)。
[project.scripts] hellohello = "oreore.hello.hello:hello"
これにより、oreore/hello/hello.py の関数hello()を呼び出すスクリプト hellohello が、oreoreパッケージのインストール時に作られるようになる。
Wheelファイルを作る
以上の準備により、Wheelファイルを作成できるようになる。
Wheelファイルを作成するにはbuildを使うことが標準的なようだ。ただし、別途インストールが必要。pipにもWheelの作成機能があるので、ここではbuildではなくpipを使う。
$ python -m pip wheel --no-deps .
これにより、カレントディレクトリに oreore-0.1.0-py3-none-any.whl が作られる。
pip wheelの注意点として、buildというディレクトリが作られその中にビルド時のソースコードが残るという点がある。これが次のpip wheel実行時にも参照されるので、例えばhello.pyをhello2.pyに改名してpip wheelを実行し直すとWheelファイル内に(本来は含まれるべきでない)hello.pyが含まれてしまう(pipのバージョン24.2で確認)。
なので、pip wheelの実行前にはbuildディレクトリは削除した方がよさそう。buildにはこの問題はなさそうだったので、pip wheelではなく素直にbuildを使う方がいいのかもしれない(この段落はディレクトリ名のbuildとツール名のbuildが混在していて分かりにくい…)。