早いもので41歳になりました。昨年の日記(2023年3月10日の日記参照)を見ると、コロナの流行を心配していました。
コロナの流行は相変わらずのように思いますが、世の人々はもう対策に疲れたのか、感染リスクを無視して暮らすスタイルが定着したようです。個人的には感染しないに越したことはないので、これからもマスクなど予防は心がけたいと思います。花粉症にもなりたくないし……。
働き方はリモート10割から、リモート:出社=7:3〜5:5くらいの割合に落ち着きつつあります。特にオフィスに用事がなければ行かないなんて、昔では考えられない通勤スタイルですね。
この働き方の利点は「遠隔地に居る人」が気にならなくなることです。オフィスに集まる場合を除けば、家も沖縄も北海道も大して変わらないのです。実際、東京から離れて住んでいる人もいるようですね。
目次: 車
またまた車のバッテリーが干上がって死にました。写真は撮っていませんが2Vくらいになっていたように思います。
しかし今日はディーラーでの半年点検の日でディーラーに行かなければならないので、ジャンプスタートしてエンジンをかけてディーラーまで行きました。去年買った(2023年4月22日の日記参照)KashimuraのジャンプスターターKD-238が大活躍です。活躍しないほうが本当は良いけど。
ディーラーに辿り着いて車を預けたら、ディーラーの駐車場から動かそうとしたらバッテリーが上がってて動かせなかった、ジャンプスタートしたと言われました。ごめんね……。
またバッテリー交換で財布が軽くなるな〜と思っていたら、整備士さんからは意外な提案が返ってきました。
とのこと。
バッテリー交換以外の新たなパターンに出会いました。交換して1年、走行距離も1000km程度、バッテリーはこんなに劣化しないはず?と判断したみたいです。たぶん安く何とかしてみたい、というご提案だと理解して素直に承諾しました。
バッテリー復活なるか?いつもと違うので楽しみですね。
目次: Arduino
M5Stamp C3をBluetooth LEデバイスにして、Linux PCもしくはRaspberry PiなどのLinux SBCとお話する取り組みの続編です。
今回はbluez-dbusを使ってBluetoothデバイスの列挙、GATTのServiceの列挙を行います。
DeviceManagerというクラスとDeviceManagerクラスのスタティックメソッドが、bluez-dbusを使うときの根っこになります。
DeviceManagerのインスタンスを作成するため最初にcreateInstance()を呼ぶ必要があります。1回で良いです。これ以降はDeviceManager.getInstance()でDeviceManagerのインスタンスが取得できます。引数はfalseならシステム全体用のバスインタフェース、trueなら現在のログインセッション用のバスインタフェースに接続します。Bluetoothの仕組みとは関係なくて、Bluezが依存しているD-Busの仕組みが透けて見えています。たぶんfalseで良いはず。
//一番最初に1回呼ぶ
DeviceManager.createInstance(false);
//以降はgetInstance()でDeviceManagerオブジェクトを取得できる
DeviceManager.getInstance();
システムに存在するBluetoothアダプターの一覧を得る方法はこんな感じです。お手軽です。
DeviceManager deviceManager = DeviceManager.getInstance();
List<BluetoothAdapter> adapters = deviceManager.getAdapters();
Bluetoothアダプターを1つ選んだら、Bluetoothデバイスを探して(Discovery)、ある程度時間を置いて見つかったデバイスの一覧を取得します。
BluetoothAdapter adapter;
adapter.startDiscovery();
try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
//ignore
}
adapter.stopDiscovery();
List<BluetoothDevice> devices = deviceManager.getDevices(true);
注意点としてはstartDiscovery()とstopDiscovery()の間はある程度の時間を開けないとデバイスが1個も見つかりません。
Bluetoothデバイス特にBLE(Bluetooth Low Energy)デバイスとはGATTプロファイルを使って通信します。BluetoothにはATTプロトコル(Attribute Protocol)という属性をやり取りするプロトコルがあります。GATTはATTプロトコルの上で汎用的(Generic)にデータを通信するための取り決め(プロファイル)になります。
GATTというかATTはクライアント・サーバー方式のプロトコルでして、BLEデバイスはサーバーになります。サーバーはService(=提供する機能)を1つもしくは複数持っています。1つのService内にはCharacteristicが並んでおり、CharacteristicにはValueとDescriptorが並んでいます。こんな感じです。
通信に使うのはCharacteristicですが、いきなりCharacteristicを列挙することはできません。上位側にあるServiceから列挙します。Serviceを探すにはconnect()してgetGattServices()を呼びます。connect()とdisconnect()は数秒レベルで時間がかかります。
今回はM5Stamp C3に対してペアリングなしで接続しています。ペアリングなし=通信路の盗聴や乗っ取りをされる可能性があります。今回は特に困らないのでこのまま話を進めますが、盗聴や乗っ取りをされると困る場合はペアリングをする必要があります。ペアリングの方法もご紹介したいですが、今はやり方がわかりません。わかったらまた別の日記にでも書きます。
BluetoothDevice device; device.connect(); device.refreshGattServices(); List<BluetoothGattService> gattServices = device.getGattServices(); device.disconnect();
GATTのService内のCharacteristicを探すにはgetGattCharacteristics()を呼びます。
BluetoothGattService srv;
srv.refreshGattCharacteristics();
List<BluetoothGattCharacteristic> gattCharacteristics = srv.getGattCharacteristics();
CharacteristicのDescriptorも取得できます(getGattDescriptors()メソッドを使います)が、今回は特に必要ありませんので列挙するためのコードは割愛します。
送受信の方法はまた今度。
目次: Arduino
最近、M5Stamp C3 + Raspberry Piを組み合わせて的あてゲームを作っています。Bluetooth通信クライアント側のLinuxマシンにRaspberry Pi 3 model Bを使っているものの、販売終了につき新品が手に入りません。今は1台あれば良いので困りませんが、後でN増しするとき困りそうです。
Linuxマシン側の要件を挙げると、
まず値段的にx86のノートPCやNUCは購入候補から外れます(中古品でも厳しい)。ARMのSBCですと、FRIENDLY ELECのNanoPi R5系(Rockcihp RK3568B, Cortex-A55 x 4)か、OKDO/Radxa ROCK 3 model C 1GB版(Rockchip RK3566, Cortex-A55 x 4)がちょうど良さそうでした。
NanoPiとROCK 3はどちらでも良かったのですが、RSコンポーネンツで容易に購入できるROCK 3 model C 1GB版に決めました。
ROCK 3 model CはRaspberry Pi 3 model Bを置き換えるにふさわしい製品だと思いますが、私の使い方だと1点だけ問題があって、Javaの画面描画が致命的に遅いです。5秒に1回くらいしか描画されません。このままでは使いたい目的に合いません。
最初にGPUが有効になっているか確認です。GPU使用率を/sys/devices/platform/fed60000.gpu/utilizationを見ると、無負荷のときは0、アプリ起動時で20くらいでGPUは動いています。問題なさそうです。むしろ暇しているというか余裕すらありそうです。Javaのライブラリ系orアプリ実装の問題でしょう。
次にJavaのライブラリ系を確認です。現在、画面描画のダブルバッファリングに使っているのはBufferStrategyで、BufferStrategyが返すVolatileImageのisAccelerated()はTrueを返します。OpenGLによるHW描画が使われているようです。これも問題なさそうです。残るはアプリ実装の問題ですね。
余談ですが下記のようにGraphics2D経由で確認すると、
((Graphics2D)strategy.getDrawGraphics()).getDeviceConfiguration().getImageCapabilities().isAccelerated()
なぜかisAccelerated()がFalseになります。良くわからん動きです。
タイトルの通りですが、アプリ実装が原因でした。コードでいえば下記の部分です。
Graphics2D g2;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
図形描画とテキスト描画のアンチエイリアスをONにして使っていましたが、このうち図形描画のアンチエイリアスをONにすると著しく描画速度が下がることがわかりました。Raspberry Pi 3 model Bは双方のアンチエイリアスをONにしても特に問題がなかったため、気づくまで結構手間取りました。
図形描画のアンチエイリアスは今のところ見た目にさほど影響がないので、OFFにしました。テキスト描画のアンチエイリアスも描画速度に多少影響ありますが、OFFにすると見た目が悪くなりすぎるので、ONのまま使います。
問題の解決はしていませんが、回避できたのは良かったです。勢いで3つも買ってしまったROCK 3 model Cの不良在庫化を避けられました……。
目次: GCC
過去の日記(2021年3月13日の日記参照)にプログラムがバグっていて未定義動作だ(つまりGCCのせいじゃない)とコメントいただいたので、C11規格を眺めていました。
C11 committee draft(N1570) 6.7.3 Type qualifiersの6に、volatileとnon-volatileを併用したときの未定義動作の記述がありました。
6.7.3 Type qualifiers ... 6 If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined. If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined. 133) 133) This applies to those objects that behave as if they were defined with qualified types, even if they are never actually defined as objects in the program (such as an object at a memory-mapped input/output address). (ざっくり訳) const-qualified型で定義したオブジェクトをnon-const-qualified型の左辺値(lvalue)を使用して変更した場合、 動作は未定義です。volatile-qualified型で定義したオブジェクトを、non-volatile-qualified型の左辺値で参照した場合、 動作は未定義です。 133) たとえプログラム内でオブジェクトとして定義されていなくても、これ(注: 上記の6のこと)は修飾された型で定義されたように 振る舞うオブジェクト(メモリマップされたI/Oアドレスにあるオブジェクトなど)に適用されます。
未定義動作ならmemmoveやmemcopyに何が起きても不思議ではないですね。C言語難しいわ……。
目次: Linux
Raspberry Pi 3 Model B (以降RasPi 3B)のHDMI出力の解像度はFull HDがデフォルト設定です。今日日のディスプレイで困ることはほぼありませんが、古いアスペクト比4:3のディスプレイなど、Full HD以外の解像度で出力してほしいときが稀にあります。
RasPi 3Bに限りませんけど、X Window Systemはxrandrコマンドで解像度を手軽に変更できます。しかし解像度の設定は保存されず、セッションを終了したり再起動すると元のFull HD出力に戻ります。まあ、これはこれで誤った設定のまま固定されないというありがたい面もあるものの、普通は再起動後も解像度を維持してほしいです。
調べるとX Window Systemの設定ファイルを変更するか、autorandrコマンドで解像度を維持できるみたいです(autorandrのソースコード)。例として1024x768 60Hzの解像度で維持するやり方を挙げます。
$ apt-get install autorandr $ xrandr -s 1024x768 --rate 60 $ autorandr --save (profile名)
設定は~/.config/autorandr/(profile名)/というディレクトリに保存されます。
起動時に解像度を変更している方法が気になったので調べると、XDG autostart(Desktop Application Autostart Specification)を使って実現していました。
Linuxのデスクトップ環境は実装ごとに自動起動の方法が違いますが、XDG autostartは多数のデスクトップ環境が対応している自動起動の仕組みのようです。私が使っている64bit版Raspberry Pi OSはデスクトップ環境としてLXDE (Lightweight X11 Desktop Environment)を採用していて、LXDEもXDG autostartに対応しています。
XDG autostartはデスクトップ表示の際に/etc/xdg/autostart/ディレクトリの下の設定を全部実行します。autorandrの設定は/etc/xdg/autostart/autorandr.desktopにありました。
[Desktop Entry] Name=Autorandr Comment=Automatically select a display configuration based on connected devices Type=Application Exec=/usr/bin/autorandr -c --default default X-GNOME-Autostart-Phase=Initialization
実行されるコマンドラインのうちオプション-cはchangeの意味で、最初に見つけたprofileの解像度に変更します。オプション--defaultはdefaultという名のprofileを使うように変更する指令らしいのですが、効き目が良くわかりません。
もし複数のprofileが存在していた場合にどれが選ばれるのか気になるので、デバッグオプションを付けて実行してみましょう。
$ autorandr -c --default default --debug fullhd (detected) (1st match) (current) xga (detected) (2nd match) | Differences between the two profiles: | [Output HDMI-1] Option --mode (= `1920x1080') is `1024x768' in the new configuration \- Config already loaded $ autorandr -l xga $ autorandr -c --default default --debug xga (detected) (1st match) (current) fullhd (detected) (2nd match) | Differences between the two profiles: | [Output HDMI-1] Option --mode (= `1024x768') is `1920x1080' in the new configuration \- Config already loaded
こんな動作をしました。最後に利用もしくは保存したprofileが使われるみたいですね。
目次: ベンチマーク
前回(2024年2月27日の日記参照)はループ、再帰なし、1000バイト以下で100万回のHello, World!を実施する問題に対し、バイナリサイズを極限まで削るためのアイデアをご紹介しました。
今回はバイナリ実装の説明を紹介したいと思います。
今回使用する100万回のHello, World!を呼ぶ命令列と処理の流れを説明します。エントリアドレスは0x983cb004なので、一番上の行から実行開始します。
983cb004: b9 40 42 0f 00 mov $0xf4240,%ecx 983cb009: 6a 01 push $0x1 983cb00b: 58 pop %rax 983cb00c: 6a 0e push $0xe 983cb00e: 5a pop %rdx 983cb00f: a9 02 00 3e 00 test $0x3e0002,%eax 983cb014: 89 c7 mov %eax,%edi 983cb016: eb 50 jmp 983cb068 <_start+0x68> 983cb028: "Hello, World!\n" 983cb060: 0f 05 syscall 983cb062: 59 pop %rcx 983cb063: e2 a4 loop 983cb009 <_start+0x9> ----- (★1)loop命令の条件に一致せず、983cb065に進んだ時に見える命令列 983cb065: 25 00 00 be 28 and $0x28be0000,%eax 983cb06a: b0 3c mov $0x3c,%al 983cb06c: 98 cwtl 983cb06d: 51 push %rcx 983cb06e: eb f0 jmp 983cb060 <_start+0x60> ----- (★2)983cb068に飛んだ時に見える命令列 983cb068: be 28 b0 3c 98 mov $0x983cb028,%esi 983cb06d: 51 push %rcx 983cb06e: eb f0 jmp 983cb060 <_start+0x60>
まず0x983cb004〜0x983cb016まで実行して、ジャンプ命令で0x983cb068に飛びます。上の図でいう(★2)の方です。
ループ100万回が終了してrcxレジスタが0になるとloop命令の次のアドレス983cb065に進みます。eaxレジスタに0x3cを入れてsyscall(exitシステムコールに相当する)するので、プログラムが終了します。
この命令列の凄いところはand命令とmov命令の重ね合わせです。アドレス0x983cb065から見たときと、アドレス0x983cb068から見たときで命令列の意味が大きく変わります。図示するとこんな感じです。
and mov cwtl push jmp | | | | | <------------> <---> <> <> <---> : アドレス983cb065から見たときの解釈 25 00 00 be 28 b0 3c 98 51 eb f0 <------------> <> <---> : アドレス983cb068から見たときの解釈 | | | mov push jmp
この工夫によってmovとjmpの合計7バイトを重ね合わせることができていて、結果112バイトにきっちり収まっています。
$ hexdump -C 112byte.out 00000000 7f 45 4c 46 b9 40 42 0f 00 6a 01 58 6a 0e 5a a9 |.ELF.@B..j.Xj.Z.| 00000010 02 00 3e 00 89 c7 eb 50 04 b0 3c 98 00 00 00 00 |..>....P..<.....| 00000020 38 00 00 00 00 00 00 00 48 65 6c 6c 6f 2c 20 57 |8.......Hello, W| 00000030 6f 72 6c 64 21 0a 38 00 01 00 00 00 05 00 00 00 |orld!.8.........| 00000040 00 00 00 00 00 00 00 00 00 b0 3c 98 00 00 00 00 |..........<.....| 00000050 00 b0 3c 98 00 00 00 00 0f 05 59 e2 a4 25 00 00 |..<.......Y..%..| 00000060 0f 05 59 e2 a4 25 00 00 be 28 b0 3c 98 51 eb f0 |..Y..%...(.<.Q..| 00000070 $ ./112byte.out | head Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! $ ./112byte.out | wc 1000000 2000000 14000000 $ ./112byte.out > /dev/null
最終的なバイナリはこんな感じです。動作もOKです。
ループ終了後exitシステムコールを呼ぶとき、アドレス983cb06aのmov命令にて0x3c(exitシステムコールを表す番号)をalレジスタに入れてsyscall命令を実行します。もしこのmov命令のみだと、writeシステムコールがエラーを返してeaxレジスタが負の値になっていると問題が起きます。alレジスタを0x3cに書き換えても上位ビットが0ではないため、eaxレジスタとして見たとき値が0x3cになりません。よってexitシステムコールが呼ばれず終了しません。
今回の112バイト版は前後のand命令とcwtl命令によってこの問題を解決しています。loop命令を通過した後の命令列を再掲します。
983cb065: 25 00 00 be 28 and $0x28be0000,%eax 983cb06a: b0 3c mov $0x3c,%al 983cb06c: 98 cwtl 983cb06d: 51 push %rcx 983cb06e: eb f0 jmp 983cb060 <_start+0x60>
ポイントは先頭の2命令です、簡単に解説します。
もしwriteシステムコールがエラーを返してeaxレジスタが負の値になっていたとしても、and命令がaxレジスタ相当の下位16ビットを0クリアし、cwtlで符号拡張するので必ずeaxレジスタは0x3cになる仕組みです。
この手のコードゴルフではエラー処理まで考えないことが多いですが、きれいに解決されています。ちなみに私はcwtl命令を初めて知りました。こんな命令あるんだ……。
< | 2024 | > | ||||
<< | < | 03 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | - | - | - | - | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | - | - | - | - | - | - |
合計:
本日: