Rust, SOLID-OS, LinuxでTCP通信 (連載16)
今回は、
・加速度センサADXL345への制御を完全に隠蔽する
・Linuxのネットワーク機能をSOLID-OSから使用することを試す
を行いたいと思います。
それが終わったら、Linuxのネットワーク機能をSOLID-OSから使用することを試してみます。
1.加速度センサADXL345への制御を完全に隠蔽
前回はspi_adxl345クレートのインタフェースを、
・init()関数
・regwrite()関数
・regread()関数
としました。
今回は加速度センサADXL345への制御を完全に隠蔽してしまうべく、
・init()関数:ADXL345への初期化を完全に終わらせる
・current_acceleration()関数:X, Y, Z軸の加速度値を取得する(Main関数で計算不要にする)
とします。
1.1 spi_adxl345クレートのinit()関数
これはまったく複雑ではないので、さっと飛ばします。
・pub公開していたinit()関数を、privateとしspi_init()と名称変更する。
・新しくpub公開するinit()関数を作成し、ADXL345への初期化処理を書く。
新しくpub公開するinit()関数は、以下のようになりました。
pub fn init() {
const BW_RATE: u8 = 0x2c;
const BW_RATE_VAL: u8 = 0x0b;
const DATA_FORMAT: u8 = 0x31;
const DATA_FORMAT_VAL: u8 = 0x0B; //(4 mg/LSB +-16g)
const POWER_CTL: u8 = 0x2D;
const POWER_CTL_VAL: u8 = 0x08;
spi_init();
//ADXL345_init
regwrite(BW_RATE, BW_RATE_VAL);
regwrite(DATA_FORMAT, DATA_FORMAT_VAL);
regwrite(POWER_CTL, POWER_CTL_VAL);
}
1.2 spi_adxl345クレートにcurrent_acceleration()関数作成
一度呼べば、X, Y, Z軸の加速度値が一気に返ってくる、という関数を作成します。
すなわち、戻り値が複数になる、という事です。
ところで複数の戻り値って、どう書けばいいのでしょう。
配列でポインタ返し?いやポインタはなかったんだっけ。
それともpythonPythonみたいなタプル型ってできたりするのかな?
Cの配列ポインタ返し:
void func1 (int *val)
{
*val++ = 0x02;
*val++ = 0x01;
*val = 0x03;
}
int main() {
int value[3];
func1(value);
return 0;
}
Pythonのタプル型:
def func1():
return 123, 2, 'string'
Rustではこれら、両方できるようです。
試してみました。
配列返し:
pub fn current_acceleration_array() -> [f32; 3] {
const DATAX0: u8 = 0x32;
const DATAY0: u8 = 0x34;
const DATAZ0: u8 = 0x36;
let mut axis_array: [f32; 3] = [0.0, 0.0, 0.0];
//連続複数リードを行う。-> 0xc0とORをとる。
//0.0392266=(4/1000*9.80665)
axis_array[0] = regread(0xc0 | DATAX0) as f32 * 0.0392266;
axis_array[1] = regread(0xc0 | DATAY0) as f32 * 0.0392266;
axis_array[2] = regread(0xc0 | DATAZ0) as f32 * 0.0392266;
return axis_array;
}
使う人:
let axis_array: [f32; 3] = spi_adxl345::current_acceleration_array();
タプル型返し:
pub fn current_acceleration_tuple() -> (f32, f32, f32) {
const DATAX0: u8 = 0x32;
const DATAY0: u8 = 0x34;
const DATAZ0: u8 = 0x36;
//連続複数リードを行う。-> 0xc0とORをとる。
//0.0392266=(4/1000*9.80665)
let x_axis = regread(0xc0 | DATAX0) as f32 * 0.0392266;
let y_axis = regread(0xc0 | DATAY0) as f32 * 0.0392266;
let z_axis = regread(0xc0 | DATAZ0) as f32 * 0.0392266;
return (x_axis, y_axis, z_axis);
}
使う人:
let (x_axis, y_axis, z_axis) = spi_adxl345::current_acceleration_tuple();
ここで一点、気になることがあります。
配列返しの場合の、Rustコードについて、です。
筆者は常日頃から、サブモジュール内でローカル以外の変数を定義することは、なんとなく避けています。
なので、current_acceleration_array()関数内で戻り値となり得るメモリの新設、ここで言うと
let mut axis_array: [f32; 3] = [0.0, 0.0, 0.0];
を行うのに強烈に違和感があるのです。。。
C言語だと、これって
float *axis_array=(float *)malloc(sizeof(float)*3);
をサブルーチンコールの時に毎回行っていて、それを上位で使っています。
こうすると、メモリの解放を忘れがちです。
なんだか危険な匂いがしてきます。
毎度毎度あまり深くは考えてはいませんが、もう体に染みついていて、こういうコードを見ると頭の中で拒否反応が起こってしまいます。
どうしても仕方のない時は、危険信号を察知して十分注意した上でコーディングします。
でも、これはRust。
そうか、これが所有権か!
所有権が上位関数に移って、上位関数でaxis_arrayを使い終わると、そこでライフタイムが終わる、というアレか!
「危険信号を察知して十分注意した上でコーディング」をしないといけないケースがある、という反省を活かして、Rustの言語仕様は作られているのが良くわかります。
ですが、これに慣れてしまうと、C言語に戻れないのかもしれない、という不安も若干。
こういう、体に染みついている危険信号が一旦リセットされてしまうと、C言語に戻ったときにおかしなコードを書きそうな気がします。
そのあたり、C言語とRust両方のパワーユーザの方に、頭の切り替え方を伝授していただきたいものです。
おっと本筋から離れてしまいました。
これで配列返しもタプル型返しも使えることがわかりました。
配列返しの方が後々使いやすそうなので、そちらを選択することにします。
2.Linuxのネットワーク機能をSOLID-OSから使用する
さて、ここからはSPIのことは一旦忘れます。
SOLID-OSでは、Linux側のネットワーク機能を使用することができます。
SOLID-OSと Linuxの二つの OSがラズパイ4上で同時に動作しているのですが(CPU0,1が SOLID-0S、CPU2,3が Linux)、Linuxに搭載されている TCP/IPを、SOLID-OSからも OS間通信機能を使って、使えるようになっています。
SOLID-OSからは普通に socketインタフェースが使えるようになっていて、
Linuxとの SOLID-OSとの OS間通信を意識せず、プログラムができます。
この機能を使った Rustでサンプルがありますので、コードをまるっと頂いてしまいましょう。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/tree/main/rust-server-tcpecho-std
このサンプルコードでは、TCP/IPサーバを立ち上げています。
ポート 7777 でリッスンし、受信データをそのままループバックする、という仕様です。
2.1 Cargo.toml編集
サンプルの依存関係はそのまま持ってきます。
すなわち、サンプルのCargo.tomlに書かれている、[dependencies]部分をコピーし、自分のCargo.tomlにペーストします。
[dependencies]
env_logger = { version = "0.9", default-features = false }
log = "0.4"
ログをUARTターミナル に出力するためのクレートのようです。
2.2 Unstable featureの指定
次に、サンプルで使用している”unstable feature”を、使えるようにします。
自分のlib.rs先頭に以下を記述します。
#![feature(let_else)] // let pattern = ... else { ... };
#![feature(solid_ext)] // std::os::solid::prelude::AsRawFd
2.3 使用するSOLID-OSインタフェースの定義
次に、使用するSOLID-OSインタフェースを定義します。
use std::{
io::{self, prelude::*},
net::{Shutdown, TcpListener, TcpStream},
os::solid::prelude::AsRawFd,
sync::mpsc,
time::Duration,
};
ネットワーク操作系のインタフェースも、ここで定義されて、ソースコード内で使用できるようになっています。
net:: {Shutdown, TcpListener, TcpStream}
2.3 サブモジュール、main関数内の追加
後は、サンプルのlib.rsから、サブモジュールをまるっとコピーし、main関数内も同じようにコピーします。
ここまでは、サンプルコードを拝借しただけなので、ひっかかることはありませんでした。
ちなみに、このサンプルと同等機能の実装が、C++でも記述されています。
ご興味ある方、比較してみてください。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/cpp-server-tcpecho/cpp-server-tcpecho/main.cpp
3.加速度値をネットワーク経由で返すようにする
さて、ここでSPIのことを思い出します。
現時点では、TCP/IPサーバとして、来たデータをそのまま返すという動きをしています。
これを変更して、
「何かデータが来たら、加速度値を返す」
ようにしてみます。
fn serve_client()関数の中で、返すデータを作成しています。
match (&*client_fd).write_all(&buffer[..num_read_bytes]) {
Err(e) if e.kind() == io::ErrorKind::WriteZero => {
break;
}
result => result?,
};
ここを、加速度値をセンサから取得しその値を返す、というコードに変更してみます。
加速度値をセンサから取得:
let axis_array: [f32; 3] = spi_adxl345::current_acceleration_array();
これでしたね。
axis_arrayはf32の型を持つ配列でした。
一方、ネットワーク送信用バッファの型を見てみます。
送信関数 write_allを軽くマウスでポイントしてみると、情報が表示されます。
u8型の配列ですね。
という事は、また型変換ですね(汗)
今度は32ビットのfloatからu8への型変換。
もう慣れました。何かあるんでしょう?便利なのが。
https://doc.rust-lang.org/std/primitive.f32.html#method.to_le_bytes
ほら、あった。
//let bytes = 0x12345678u32.to_le_bytes();
//assert_eq!(bytes, [0x78, 0x56, 0x34, 0x12]);
(ちなみに前回はこの逆、「from_le_bytes」を使いました。)
という事で、このように書きました。
let axis_array: [f32; 3] = spi_adxl345::current_acceleration_array();
println!("x_axis:{} y_axis:{} z_axis:{}", axis_array[0], axis_array[1], axis_array[2]);
let retbytes_x: [u8; 4] = axis_array[0].to_le_bytes();
let retbytes_y: [u8; 4] = axis_array[1].to_le_bytes();
let retbytes_z: [u8; 4] = axis_array[2].to_le_bytes();
let mut retbytes: [u8; 12] = [0x00; 12];
for n in 0..4 {
retbytes[n] = retbytes_x[n];
retbytes[n+4] = retbytes_y[n];
retbytes[n+8] = retbytes_z[n];
}
match (&*client_fd).write_all(&retbytes) {
Err(e) if e.kind() == io::ErrorKind::WriteZero => {
break;
}
result => result?,
};
4.PC側のTCP/IPクライアントアプリ作成
PC側で、加速度値を取得&表示するアプリをC#で作ってみました。
仕様:
get dataボタン押下:
ホスト名「raspberrypi」にポート「7777」で接続し、何か適当にデータを送信。
受信した12バイトのデータを4バイトごとに区切り、それぞれf32変換することでX,Y,Z軸の加速度値を得る。
さらにそれらを文字列に変換し、表示する。
4.1 C#の型変換
こちらのC#アプリでも、型変換を二回行っています。
「受信した12バイトのデータを4バイトごとに区切り、それぞれf32変換」
「さらにそれらを文字列に変換」
ご参考に、C#の場合はこう書けます、というのをご紹介。
・受信した12バイトのデータを4バイトごとに区切り、f32変換
指定オフセット値から4バイトを抽出するので、以下のように書きました。
float x_axis = BitConverter.ToSingle(data, 0);
float y_axis = BitConverter.ToSingle(data, 4);
float z_axis = BitConverter.ToSingle(data, 8);
・文字列に変換
showstrings という文字列で、ダイアログボックスに表示する文字列を作成しています。このように書きました。
showstrings = "x:" + x_axis.ToString() + " y:" + y_axis.ToString() + " z:" + z_axis.ToString();
5.実行!
実行してみます。
SOLID側のUART出力は以下のようになりました。
PCアプリは以下のようになりました。
今回はここまで。
次回は、
・常に最新の加速度値を100個キープしておく
・TCP/IPサーバ経由で、その瞬間の100個の加速度値を送信する
という変更を行う予定です。
これには、(今わかるだけでも)3つ、筆者にとっての試練があります。
その1:加速度値取得をスレッド化
その2:スレッド間のデータ受け渡し
その3:100個キープするためのリングバッファの実装
まだ先が長いなぁ。。。