メモの日々


2024年11月26日(火) [長年日記]

[android][net] AndroidのBLEを使った通信で512バイトを超える送信データが破棄される

AndroidのBluetooth APIを使ったBLE通信で、送信データが破棄されれ送信されないことがあったので調べた。

原因はAndroid 13と14での動作の変更だった。

Android 13での変更

ちゃんとした資料は見つけられなかったが、Android 13から512バイトを超えるデータを送受信できなくなったというissueがちらほら見つかる。これなど。

次の変更が原因のようだ。

この変更は、次に示すBluetoothの仕様でattribute valueの最大長が512バイトに制限されていることへ準じるために行われている。

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では次の変更がされたとある。

これは、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の人による説明があった。

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が混在していて分かりにくい…)。