SOLID未分類 SOLID for Raspberry Pi 4 (連載16)

SOLID for Raspberry Pi 4 (連載16)

(2023/2/20)

Rust, SOLID-OS, LinuxでTCP通信

今回は、
・加速度センサ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にペーストします。

https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/rust-server-tcpecho-std/rustapp/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関数内も同じようにコピーします。

https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/rust-server-tcpecho-std/rustapp/src/lib.rs

ここまでは、サンプルコードを拝借しただけなので、ひっかかることはありませんでした。

ちなみに、このサンプルと同等機能の実装が、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個キープするためのリングバッファの実装

まだ先が長いなぁ。。。