dnackのブログ

コード書く仕事の間にコード書いてる知命目前のおっさんです。

Prism/XamarinでAndroidアプリの作成(9.5):バリデーションの別解

前回調べたバリデーションの方法は以下の通り。

dnack.hatenablog.com

これが今作っているコードで使えなかったので別解を調べた。

定義

        [Required(ErrorMessage = "ユーザ名を入力してください")]
        [RegularExpression(@"[a-zA-Z0-9]+", ErrorMessage = "半角英数字のみ入力できます")]
        public ReactiveProperty<string> Username { get; }

初期化

            Username = new ReactiveProperty<string>()
                .SetValidateAttribute(() => Username)
                .AddTo(Disposable);

エラーの取り出し

            UsernameErrorMessage = Username.ObserveErrorChanged
                .Select(x => x?.Cast<string>().FirstOrDefault())
                .ToReadOnlyReactiveProperty()
                .AddTo(Disposable);

これを、VaritdationAttributeを使わないで、以下のように書ける。

定義

        public ReactiveProperty<string> Username { get; }

初期化

            Username = new ReactiveProperty<string>()
                .SetValidateNotifyError(s => string.IsNullOrEmpty(s) ?
                   "ユーザ名を入力してください" :
                   Regex.IsMatch(s, @"[a-zA-Z0-9]+") ?
                   null : "半角英字のみ入力できます")
                .AddTo(Disposable);

エラーメッセージの入り方は同じなので、エラーメッセージの取り出しやボタンのディセーブルは 前回のサンプルコードのままでOK。

Prism(Xamarin.Form)の共通部分を.NetStandardライブラリで実装すると 前回調べたVaridationAttributeを使って書けるが、 共通部分をshared projectにしたりしていると使えない(?)ようなので、 その場合はこちらを使うといい。 (ドキュメント読むと対応してなくはないようなのだけど、 何かおかしいことしてるのかなぁ…)

Prism/XamarinでAndroidアプリの作成(9):入力テキストのバリデーション

1週間も放置してましたね。まあ最初がおかしかっただけで更新頻度はこんなもんかと。

バリデーション

入力値のバリデーションがしたい気分のときがある。 数字だけしか受け付けたくないとか、ユーザ名とかパスワードのように半角英数字しか受け付けないようにしたいだとか。

パスワード

パスワードの話が出たがパスワードは実に簡単で、EntryにIsPasswordというプロパティがいるのでこいつをTrueにセットしてやればいい。

            <Entry 
                  IsPassword="True"
                  Placeholder="パスワード"
                  Text="{Binding Password.Value}"
                  />

これで、入力用のソフトキーボードは半角しか入力できない状態で表示され、しかもエントリに入力済みの文字はちゃんと*で伏せられる。

なんかもう楽すぎてユーザ名もこいつでいいかって思いたくなっちゃうんだけど、やっぱり入力した文字が見えないのはまずいよなぁ。 でもこれがこんなに簡単にできるんだから簡単にできるよねと思ったんだけど、残念ながらそんなに簡単でもなかった。

まずは動かしてみる

ValidationAttribute(検証属性)というのが使えるらしい。

docs.microsoft.com

一番簡単な例で、nullだったら怒られるバリデーション属性のRequairedAttributeというのを使ってみよう。 これは、C#のほうでバインドするプロパティを宣言するときにこう書ける。

        [Required]
        public ReactiveProperty<string> Username { get; }

これがちゃんと動いてるのを簡単な例でみてみる。

問題は、どうやってみえるようにするのが一番簡単かが問題なのだけれど…。

前回のエントリで調べた方法を使ってIObservable<bool>trueの時だけ押せるボタンを作ってみるか。 ってことでこんな感じ。

MainPage.cs

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="EntryTest.Views.MainPage"
             Title="{Binding Title}">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Entry Text="{Binding Username.Value}"
               Placeholder="ユーザー名"
               />
        <Button Text="Login" Command="{Binding ButtonLoginCommand}" />
    </StackLayout>
</ContentPage>

MainPageViewModel.cs

using Prism.Navigation;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

namespace EntryTest.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        [Required]
        public ReactiveProperty<string> Username { get; }
        public AsyncReactiveCommand ButtonLoginCommand { get; }

        private CompositeDisposable Disposable = new CompositeDisposable();

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            Title = "Main Page";
            Username = new ReactiveProperty<string>()
                .SetValidateAttribute(() => Username)
                .AddTo(Disposable);

            ButtonLoginCommand = Username.ObserveHasErrors
                .Inverse()
                .ToAsyncReactiveCommand()
                .WithSubscribe(AttemptLogin)
                .AddTo(Disposable);
        }

        public async Task AttemptLogin()
        {
            await Task.Delay(3000);
        }

        public override void Destroy()
        {
            base.Destroy();
            Disposable.Dispose();
        }

    }
}

基本的には前回のエントリでReactviPropertySlim<bool>型のプロパティからコマンドを作ったときと同じようにすればいいけれど、エラーの時にTrueになるプロパティをもとにしているので.Inverse()しなければいけないことには注意。

前回: Prism/XamarinでAndroidアプリの作成(8):ReactivePropertyを使って排他処理 - dnackのブログ

これで、確かに起動時に押せなかったボタンが、 エントリに何か入力すると押せるようになっているので、 入力値のバリデーションをする仕組みが働いていることがわかる。 f:id:dnack:20211003082549p:plain:h320 f:id:dnack:20211003082602p:plain:h320

正規表現は正義

このように便利な検証属性であるが、上のマイクロソフトのドキュメントのページにValidationAttributeの派生クラスとして記載されている検証属性の一覧は以下の11個のみ

CompareAttribute
CustomValidationAttribute
DataTypeAttribute
MaxLengthAttribute
MinLengthAttribute
RangeAttribute
RegularExpressionAttribute
RequiredAttribute
StringLengthAttribute
MembershipPasswordAttribute

個々の属性の細かい説明は本筋ではないので割愛するけれど、 とにかく半角英数字だけOK みたいな都合のいいのはない。 というか文字種で縛りをかけるものがそもそもない。 RegularExpressionAttributeがあるんだからいらねーだろって話なんだろうね。 ごもっとも。 ということで、ViewModelに[RegularExpression(@"[a-zA-Z0-9]+")]の一行を追加する。

なお、正規表現でもうまくいかんわという場合には、CustomValidationAttributeを使って自前でValidationのメソッドを書くことになる。

        [Required]
        [RegularExpression(@"[a-zA-Z0-9]+")]
        public ReactiveProperty<string> Username { get; }

これで全角だとボタンがEnableされないようになった。

f:id:dnack:20211003083012p:plain:h320

(わかりづらいけどエントリに入力しているのは全角”h”)

エラーメッセージを出してみる

人間というのは察しがよくないので、ボタンが押せないときに、なんで押せないかわからなくて途方にくれたりキレたりする。人間は愚か。

そして残念ながらUIというのは人間様を相手に想定しているわけなので、なんでボタンが押せないのか教えてやる必要があるだろう。

まず、Viewで、エントリとボタンの間にエラーメッセージ表示用のラベルを追加。

        <Label Text="{Binding UsernameErrorMessage.Value}"
               TextColor="Red"
               />

ViewModelではまずバインドするプロパティを宣言して(表示専用だからReadOnlyをつかうのがいい)

        public ReadOnlyReactiveProperty<string> UsernameErrorMessage { get; }

コンストラクタの中での初期化はこんな感じ。

            UsernameErrorMessage = Username.ObserveErrorChanged
                .Select(x => x?.Cast<string>().FirstOrDefault())
                .ToReadOnlyReactiveProperty()
                .AddTo(Disposable);

UserNameのObeserveErrrorChangedにバリデーションエラーがIEnumerableで入るので、 その中の先頭をとってきて、stringに解釈している。 FirstOrDefault()でとってきているので、IEnumerableがひとつも入っていない場合は stringのデフォルト値であるnullが入る。

さて、こうするとちゃんとエラーメッセージが出ている。

f:id:dnack:20211003083140p:plain:h320 f:id:dnack:20211003083150p:plain:h320

もしエラーメッセージが気に入らないときはこんな感じで好きなメッセージを設定できる。

        [Required(ErrorMessage = "ユーザ名を入力してください")]
        [RegularExpression(@"[a-zA-Z0-9]+", ErrorMessage = "半角英数字のみ入力できます")]

2つのエントリの両方ともエラーがない時だけ押せるボタン

今まではバリデーションする入力エントリが1つだったが、 ユーザ名・パスワードを入力してログインする場合だと、 両方とも有効でないとログインボタンを押せるようにする意味がない。 そこで、こんな感じにする。

            ButtonLoginCommand = Username.ObserveHasErrors
                .CombineLatest(Password.ObserveHasErrors, (x, y) => !x && !y)
                .ToAsyncReactiveCommand()
                .WithSubscribe(AttemptLogin)
                .AddTo(Disposable);

見ているのが一つの時は、Username.ObserveHasErrorsを反転するだけでよかったが、 Username.ObserveHasErrorsPassword.ObserveHasErrorsの両方を見ないといけないので、 それをCombineLatestで実現している。Rxマジで便利だな。なんもわからんけど。。 で、両方がfalseのときtrueを返すIObservable<bool>型を作れば、あとはいつも通り。

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="EntryTest.Views.MainPage"
             Title="{Binding Title}">
    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Grid ColumnDefinitions="*,*"
              RowDefinitions="*,*" >
            <Entry Text="{Binding Username.Value}"
                   Placeholder="ユーザー名"
                   Grid.Row="0"
                   Grid.Column="0"
                   />
            <Label Text="{Binding UsernameErrorMessage.Value}"
                   TextColor="Red"
                   Grid.Row="0"
                   Grid.Column="1"
                   />
            <Entry Text="{Binding Password.Value}"
                   Placeholder="パスワード"
                   IsPassword="True"
                   Grid.Row="1"
                   Grid.Column="0"                 
                   />
            <Label Text="{Binding PasswordErrorMessage.Value}"
                   TextColor="Red"
                   Grid.Row="1"
                   Grid.Column="1"
                   />
        </Grid>
        <Button Text="Login" Command="{Binding ButtonLoginCommand}" />
    </StackLayout>
</ContentPage>

MainPageViewModel.cs

using Prism.Navigation;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Linq;
using System.Reactive.Disposables;
using System.ComponentModel.DataAnnotations;
using System.Reactive.Linq;
using System.Threading.Tasks;

namespace EntryTest.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        [Required(ErrorMessage = "ユーザ名を入力してください")]
        [RegularExpression(@"[a-zA-Z0-9]+", ErrorMessage = "半角英数字のみ入力できます")]
        public ReactiveProperty<string> Username { get; }

        [Required(ErrorMessage = "パスワードを入力してください")]
        [RegularExpression(@"[a-zA-Z0-9]+", ErrorMessage = "半角英数字のみ入力できます")]
        public ReactiveProperty<string> Password { get; }

        public ReadOnlyReactiveProperty<string> UsernameErrorMessage { get; }
        public ReadOnlyReactiveProperty<string> PasswordErrorMessage { get; }

        public AsyncReactiveCommand ButtonLoginCommand { get; }

        private CompositeDisposable Disposable = new CompositeDisposable();

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            Title = "Main Page";

            Username = new ReactiveProperty<string>()
                .SetValidateAttribute(() => Username)
                .AddTo(Disposable);
            Password = new ReactiveProperty<string>()
                .SetValidateAttribute(() => Password)
                .AddTo(Disposable);

            UsernameErrorMessage = Username.ObserveErrorChanged
                .Select(x => x?.Cast<string>().FirstOrDefault())
                .ToReadOnlyReactiveProperty()
                .AddTo(Disposable);

            PasswordErrorMessage = Password.ObserveErrorChanged
                .Select(x => x?.Cast<string>().FirstOrDefault())
                .ToReadOnlyReactiveProperty()
                .AddTo(Disposable);

            ButtonLoginCommand = Username.ObserveHasErrors
                .CombineLatest(Password.ObserveHasErrors, (x, y) => !x && !y)
                .ToAsyncReactiveCommand()
                .WithSubscribe(AttemptLogin)
                .AddTo(Disposable);
        }

        public async Task AttemptLogin()
        {
            await Task.Delay(3000);
        }

         public override void Destroy()
        {
            base.Destroy();
            Disposable.Dispose();
        }
    }
}

これで、両方のエントリがValidの時のみログインボタンが押せるコードが書けた。めでたしめでたし。

f:id:dnack:20211003083223p:plain:h320 f:id:dnack:20211003083231p:plain:h320 f:id:dnack:20211003083237p:plain:h320

宿題

このコードでは、ページができた状態ではエントリの中身がNullなので、当然エラーになっていて、 ページができた時からエラーメッセージが表示されている。 つまり何もしていないのにエラーが出ることになるので、これは少し気持ち悪いかもしれない。

これを抑制する仕組みがReactivePropertyにはあって、コンストラクタの引数に mode: ReactivePropertyMode.IgnoreInitialValidationErrorを渡せば、初期化時にはバリデーションを行わないようにできる。

しかし、起動時にバリデーションが行われないということはすなわち起動時にバリデーションエラーが出ないということので、本来なら「両方ともNullなので押せない」はずのログインボタンが押せるという判定になってしまっている。 したがってこの対応はちょっと微妙である。というかこの場合は使わないほうがよさそう。

となると、初期化時のReactiveProperty側のValidationの抑制を行うのではなく、エラーメッセージのほうだけを抑制する仕組みを何か考える必要がある。

できないことはないような気はするけれど、まあ、エラーメッセージくらい出ててもいいんじゃねーの?(ぶん投げた)

参考にしたURL

qiita.com

qiita.com

Prism/XamarinでAndroidアプリの作成(8):ReactivePropertyを使って排他処理

まあ、全然コード書く暇ないと言ってたのは嘘じゃなくて今も泣きながら仕事でコード書いているんだけど(?) 仕事のコード書いてる間にReactivePropertyの新しい使い方を覚えたので書いておく。 ReactivePropertyマジ便利。

今回のお題は3つ

1. ReactiveCommandを、ある条件で実行可能にする(i.e.ある条件で実行不可にする)

2. 1.の仕組みを使って、複数のコマンドを排他する。

3. あるコマンドが2の仕組みによってコマンド実行できなかった場合、できるようになったら実行するようにする。(これがやりたいこと)

をReactiveProperyとReactiveCommandの組わせてでむちゃ読みやすくかけるみたい。

以下は御託なので上のリンクからコードに飛んじゃってくれてOKです。

今回のこれの動機ですが、

あるメソッドのコールバックで画面遷移をしてしまう(ユーザ操作がタイミングに直接影響しない)画面で、

・ユーザがボタンをタップしたときに表示されるあるダイアログが表示表示されている間は画面遷移をしないで、

・そのダイアログが表示されている間に画面遷移の条件が満たされた場合はダイアログが破棄されたときに画面遷移する。

という処理をなんかわかりやすく書けないかというところ。(抽象化すると上の3つ目)

まあ、素直にセマフォ使えやって話なんだろうけど、なんか重たそうだし面倒くさそう。

コマンド実行時にCanExecuteが立たなかったらCanExecuteChangedを自分で購読するみたいなことができたらいいのかなあと思ったんだが、やり方がわからないしなんかそれはそれで危うそう。

(Prism/Xamarin なのか Android なのか、とにかくこの環境やりだしてから 「こうすればいいんだろうけど書き方がわからん(しそもそもこの環境でそれができるのかわからんしやっていいのかもわからん)」が多くていやになるな。勉強しろ。はい。

というわけでいきなり参考URLから

本家のドキュメントむっちゃ充実してきましたね。(まえからでした?)

okazuki.jp

英語はちょっと、、という私には同じくokazukiさんのこれなんかもすごい。

qiita.com

本当にこんなに有用なライブラリを書いた挙句、 私くらいの適当な理解でもとりあえず使えるくらいに 丁寧なドキュメントまで用意してくれるとは。 神か。 というわけでこれを読めばええように使えます。解散。

1. 実行条件付きのRactiveCommand

とあるBool値がtrueの時だけ押せるボタンを作りましょうみたいなのは基本中の基本。 ReactiveCommandはICommandを継承しているので、そのCanExecuteに条件を書けば使えるので、それ自体はなんてことない。 これを、とあるBool値をReactiveProperty(というかIObservable)にすると、こういう風に書けてしまうらしい。 神か。

        public ReactiveCommand ButtonACommand { get; }
        public ReactivePropertySlim<bool> Enable { get; }

        public MainPageViewModel()
        {
            Enable = new ReactivePropertySlim<bool>(true);

            ButtonACommand = Enable
                .ToReactiveCommand()
                .WithSubscribe(CommandAExecute);
        }

ReactivePropertyからToReactiveCommandすると、なんとそのboolがtrueの時だけ実行できるコマンドができる。 お手軽か。

なおコードではReactivePropertyではなくてReactivePropertySlimを使っているが、なるべくこっちを使おうね。むっちゃ軽いので。(といいつつ考えるの面倒だから全部ReactivePropertyにしてしまいがちなんだよなぁ…)

この辺は参考コードは本家とか見られるので、動く形のコードは省略(一応動かしたコードからの抜粋ではある)。 やっぱり神なんだよなぁこれ。。

2. 1.の仕組みを使って、複数のコマンドを排他する。

に行きたいんだけどその前に

ボタンの二度押し抑制

これも本家にコードがあるので動く例はそっちに譲るけど、 AsyncReactiveCommandというのがあって、上のReactiveCommandによる起動制御と合わせて次のように書くと、 コマンド発行した瞬間から処理が終わるまでEnableがfalseになって、2度押しの抑制ができる。

        public AsyncReactiveCommand ButtonACommand { get; }
        public ReactivePropertySlim<bool> Enable { get; }

        public MainPageViewModel()
        {
            Enable = new ReactivePropertySlim<bool>(true);

            ButtonACommand = Enable
                .ToAsyncReactiveCommand ()
                .WithSubscribe(CommandAExecuteAsync);
        }
        
        private async Task CommandAExecuteAsync()
        {
            await Task.Delay(3000);       //なんか重い処理
        }

でもって排他処理

Enableを共有したら本当に複数コマンドの排他処理できんじゃねーのって思うよね。 痒い所に手が届くReactivePropertyにそれができないわけがない。(マジで神)

このコードはPrismTemplateでつくったBlanAppで Viewに適当にボタンとテキスト張れば動くよ。(usingは付けてね)

namespace ReactiveCommandStudy.ViewModels
{

    public class MainPageViewModel : ViewModelBase
    {
        public AsyncReactiveCommand ButtonACommand { get; }
        public AsyncReactiveCommand ButtonBCommand { get; }
        public ReactivePropertySlim<bool> SharedEnable { get; }
        public ReactivePropertySlim<string> Message { get; }

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
             SharedEnable = new ReactivePropertySlim<bool>(true);
            Message = new ReactivePropertySlim<string>();

            ButtonACommand = SharedEnable
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyCommandAAsync);

            ButtonBCommand = SharedEnable
               .ToAsyncReactiveCommand()
               .WithSubscribe(HeavyCommandBAsync);
        }

        private async Task HeavyCommandAAsync()
        {
            Message.Value = "A Button";
            await Task.Delay(3000);
            Message.Value = "A Button Released";
        }
        private async Task HeavyCommandBAsync()
        {
            Message.Value = "B Button";
            await Task.Delay(3000);
            Message.Value = "B Button Released";
        }

    }
}

同じReactivePropertySlim<bool>からコマンドを作ると、うまいこと排他制御してくれるってわけ。 待ち行列のないセマフォみたい。なんだこれこんなに簡単にできるのか。なぜオレはあんなムダな時間を…(三井顔で)

3. 実行できなかったコマンドを実行可能になったら実行する。

待ち行列のない”セマフォといったが、やっぱり待ち行列が欲しいわけ。 待ちたい処理が1つだけなら行列作らなくてもこれでいける。 (2つ以上でも順番にこだわらないのならこれで行けるのかな?)

まずはコードから

View

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ReactiveCommandStudy.Views.MainPage"
             Title="{Binding Title}">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="{Binding Message.Value}" />

        <Label Text="A:重い処理" />
        <Button Text="Button_A" Command="{Binding ButtonACommand}" />

        <Label Text="D:待ってから処理" />
        <Button Text="Button_D" Command="{Binding ButtonDCommand}" />

    </StackLayout>

</ContentPage>

ViewModel

using Prism.Navigation;
using Reactive.Bindings;
using System.Threading.Tasks;

namespace ReactiveCommandStudy.ViewModels
{

    public class MainPageViewModel : ViewModelBase
    {
        public AsyncReactiveCommand ButtonACommand { get; }
        public AsyncReactiveCommand ButtonDCommand { get; }
        public ReactivePropertySlim<bool> SharedStatus { get; }
        public ReactivePropertySlim<string> Message { get; }

        public ReactiveCommand OnTimerEvent { get; }


        public MainPageViewModel(INavigationService navigationService )
            : base(navigationService)
        {
            Title = "Main Page";
            SharedStatus = new ReactivePropertySlim<bool>(true);
            Message = new ReactivePropertySlim<string>();

            ButtonDCommand = new AsyncReactiveCommand()
                .WithSubscribe(CommandD);

            ButtonACommand = SharedStatus
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyCommandAAsync);
        
        }

        private async Task HeavyCommandAAsync()
        {
            Message.Value = "A Button";
            await Task.Delay(3000);
            Message.Value = "A Button Released";
        }

        private async Task CommandD()
        {
            if (!SharedStatus.Value)
                await SharedStatus.WaitUntilValueChangedAsync<bool>();
            Message.Value = "D Button";
        }

    }
}

ボタンAを押している間にボタンDを押せるけど、実行はボタンAが終わってからになる。 (Aを押して、AボタンがInactiveな表示になっている間にDを押すと、 画面表示が"A Button Released"ではなくて"D Button"になっているので、 ちゃんとAがおわってからDが実行されたのがわかると思う。)

本家にはちゃんとCancellationToken使いなさいって書いてるんだけど、わかんないのでこれで! (おいおいお勉強しやす。すいません) まあ、というわけでとりあえず今日のところはこんな感じで。

Raspberry Pi:SSDブートしたい。

※結局なんだかんだごちゃごちゃしてしまったので、手順をまとめました。 手順のまとめ

μSD構成のコンパクトさは魅力ではあるけれど、このままではストレージの遅さがシステムのパフォーマンスの足を引っ張ってもったいないことこの上ないので、ストレージをμSDからSSDに変更する。

当然先人がたくさんいたので、まずは手順をなぞってしまおう。お勉強はそのあと。写経は大事。

というわけで、今回参考にした先人様はこちら。 raspida.com

このサイトでは64bit版のOSでやっていたけれど、 64bit版はまだ安定版がリリースされていないようなので、OSはこのままでいきましょうかね。 (OS入れ替えるにしても、SSDにしてからのほうが速いんじゃね?とも思うし)

ところで本筋全然関係ないけどローダーというとこの本はいい本なんだけど、もう20年前なんだよな。 いい感じのもうちょい新しい感じの本とかないのかな。まあこの辺は特に新しくなるものでもないから 今でもこの知識でほぼ通用するんだろうけど。

books.rakuten.co.jp

まあ寄り道はさておき始めましょうかね。

準備

SSDと、SATA-USB変換ケーブルを用意。

私のところでは以下の環境で確認しています。

Raspberry piRaspberry Pi 4 8GB (OS等は、2022/9/20時点でapt でとれる最新版に更新済)

SSDTRANSCEND トランセンド TS240GSSD220S

SATA-USB変換ケーブル:NewLife NewDesing社製、SB3 to SATA アダプター (ケーブルは、チップの相性があるらしく、外れ引くと大変っぽいので、上のリンクにあるラズパイダさんところでおすすめされていたものを買った。)

https://www.yodobashi.com/product/100000001003104707www.yodobashi.com

www.amazon.co.jp

家から一歩も出ずにこれがそろうのだから便利な時代だなぁ。。

ブートローダーの更新

raspberry pi 4のブートローダーは古いものはSSDブートに対応しないようなので、まずは確認。

dnack@raspberrypi:~ $ vcgencmd bootloader_version
Sep  3 2020 13:11:43
version c305221a6d7e532693cc7ff57fddfc8649def167 (release)
timestamp 1599135103
update-time 0
capabilities 0x00000000

ブートローダを確認してみたが2020年9月のバージョン。

いろいろなサイトを見ると多分このバージョンでもUSBブートは対応していそうなのだけど (この辺ってリリースノート見てもわかるように書かれていないみたいなんだよな…)、 1年前のままってのもなんだし、更新しておく。

dnack@raspberrypi:~ $ ls /lib/firmware/raspberrypi/bootloader/stable/pie* -la
-rw-r--r-- 1 root root 524288  424  2020 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2020-04-16.bin
-rw-r--r-- 1 root root 524288  617  2020 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2020-06-15.bin
-rw-r--r-- 1 root root 524288  720  2020 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2020-07-16.bin
-rw-r--r-- 1 root root 524288  810  2020 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2020-07-31.bin
-rw-r--r-- 1 root root 524288  97  2020 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2020-09-03.bin
-rw-r--r-- 1 root root 524288 1215  2020 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2020-12-11.bin
-rw-r--r-- 1 root root 524288  114  2021 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2021-01-11.bin
-rw-r--r-- 1 root root 524288  117  2021 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2021-01-16.bin
-rw-r--r-- 1 root root 524288  223  2021 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2021-02-16.bin
-rw-r--r-- 1 root root 524288  319  2021 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2021-03-18.bin
-rw-r--r-- 1 root root 524288  430 20:57 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2021-04-29.bin
-rw-r--r-- 1 root root 524288  77 22:25 /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2021-07-06.bin

なんかいっぱいいるけど、最新版焼けばいいよね。

dnack@raspberrypi:~ $ sudo rpi-eeprom-update -d -f /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2021-07-06.bin
*** INSTALLING /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2021-07-06.bin  ***

   CURRENT: 202093日 木曜日 12:11:43 UTC (1599135103)
    UPDATE: 202176日 火曜日 10:44:53 UTC (1625568293)
    BOOTFS: /tmp/tmp.00VqIMxn84

EEPROM updates pending. Please reboot to apply the update.
To cancel a pending update run "sudo rpi-eeprom-update -r".

再起動しろとのことなのでリブートして待つことしばし。

dnack@raspberrypi:~ $ vcgencmd bootloader_version
Jul  6 2021 11:44:53
version c258ef8fe1d2334a750078b17dab5e2c1a1787fc (release)
timestamp 1625568293
update-time 1632126508
capabilities 0x0000007f

確かに更新されている。

SSDに環境をコピー

μSDからSSDに環境をコピーするのはGUIにあるツールを使うらしい。VNCいれといてよかった。 VNC接続して、GUIのメニューから”SD Card Copier”を選択。

f:id:dnack:20210920180504p:plain

New Partition UUIDsにチェックを入れてStartボタンを押下。 しばしそのまま放置すると、終了ダイアログが出るので、それまで待つ。

ブート順の設定

ブート順は、万一トラブったらμSDからブートするだろうから SD→USBの順でいいだろう。

dnack@raspberrypi:~ $ sudo raspi-config

から

6.Advanst Options → A6 Boot Order → B1 SD Card Boot Boot from SD Card if available, otherwise boot from USB

を選択。

私のところでは最初からそうなってたので何もせずに抜けた。

これで、SDカードがあればSDカードから、なければUSBからブートしてくれる設定になっているはず。

設定を変えた場合は、Finishでraspi-configから抜けると自動でリブートする(はず)。

設定ファイルの変更

Shutdownして電源落として、uSDカードを抜いてから起動してみると、、、

起動しねえ。。。

このままでは「起動しない」以上の情報量がないので 以前しまい込んだモバイルディスプレイを引っ張ってきて繋いでみると、 SCSIがどうたらという行でつんのめってるっぽい。

以前にいろいろいじってるうちに画面解像度をモバイルディスプレイに表示するにはあまりに細かくしてしまったので読めない。。

SDカードからブートして、画面解像度を字が読めるところ(800x600くらい?)に設定して、 そのSDからSSDへ環境のコピーからやり直し。これで起動時のメッセージ読めるはず。

そうすると、ディスクをマウントしようとしてるところでつんのめってるっぽいことがわかる。 ということで、もう一度SSD挿して、fstabを見てみると、

dnack@raspberrypi:~ $ cat /etc/fstab
proc                      /proc           proc    defaults               0       0
/dev/mmcblk0p6  /boot           vfat    defaults                0       2
/dev/mmcblk0p7  /                  ext4    defaults,noatime  0       1

ちょまてよ(キムタク顔)

これをそのままコピーしたってことは、 SSD起動でもmmclkほげほげを探しに行って (SD引っこ抜いてるから当然存在しないので) つんのめっているのではないかしら。

ということで、ビルド周りでストレージのデバイス名を変更。

SSDをマウント

dnack@raspberrypi:~ $  sudo mkdir /mnt/ex_ssd6
dnack@raspberrypi:~ $  sudo mkdir /mnt/ex_ssd7
dnack@raspberrypi:~ $  sudo mount -t vfat /dev/sda6 /mnt/ex_ssd6
dnack@raspberrypi:~ $  sudo mount -t ext4 /dev/sda7 /mnt/ex_ssd7

/boot/cmdline.txtの修正

dnack@raspberrypi:~ $  sudo nano /mnt/ex_ssd6/cmdline.txt 

console=serial0,115200 console=tty1 root=/dev/mmcblk0p7  rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

root=/dev/mmcblk0p7root=/dev/sda7 に変更。

fstabの修正

dnack@raspberrypi:~ $  sudo nano /mnt/ex_ssd7/etc/fstab で

/dev/mmcblk0p6  /boot           vfat    defaults                0       2
/dev/mmcblk0p7  /                  ext4    defaults,noatime  0       1

mmcblk0p6sda6mmcblk0p7sda7にそれぞれ変更。

これでSDカード引っこ抜いて起動して、無事起動を確認。

f:id:dnack:20210920231327j:plain

先人のブログを見てもこんなところで苦労していた形跡はないので、 この辺はSD Card Copierくんがよろしくやってくれたりするのかなとか思ってたんだけど、そんなことはなかったらしい。 いやそれにしたって、前にやってうまくいっていた人たちと何が違うんだろうなぁ。

densi.biz

この苦境を救ってくれたのはこちらの先人様でした。 この人はSD Card Copierつかわずにやってたけど、 こうやって見るとつかわないほうが簡単だったのかも。

まあなにはともあれ、なんとかうまくいったわけだけど SDをコピーしただけなので、もとのSDのサイズ以外使われてないわけですよね。

f:id:dnack:20210920212733p:plain

なにこれすげえもったいない。 ちょっと後でええようにしないと。

とりあえずsda8,9をつぶしてsda7ひろげて、 前のほうは適当にswapだけつくって残りはどこかにマウントしてしまえばいいのかな。 bootとルートだけつぶさなきゃいいんだよな?

まあ今度にします。細かい字を読んだの目が疲れた。(持病の老眼が…)

※追記 ひょっとしてNew Partition UUIDsにチェックを入れたのがわるかったのかな。 ここにチェックを入れなかったらデバイス名にsdaじゃなくて、 SDカードと同じmmcblkほげほげがはいって、 なんもしなくてもうまくいってくれたのかもしれんな。

手順のまとめ

1. ブートローダーの更新

(最近買ったものだと、出荷時からSSDブートに対応しているブートローダーが入っている可能性が高いが、 確実を期して最新の安定版にしておくといい)

dnack@raspberrypi:~ $ ls /lib/firmware/raspberrypi/bootloader/stable/pie* -la
dnack@raspberrypi:~ $ sudo rpi-eeprom-update -d -f /lib/firmware/raspberrypi/bootloader/stable/pieeprom-2021-07-06.bin
dnack@raspberrypi:~ $ sudo reboot

(pieeprom-2021-07-06.bin のところは、上の行のlsコマンドで表示されたもののうち最新のファイル名を入れる)

2. ブート順の設定

dnack@raspberrypi:~ $ sudo raspi-config

から 6.Advanst Options → A6 Boot Order → B1 SD Card Boot Boot from SD Card if available, otherwise boot from USB を選択。 ※ メニューツリーはバージョンによって異なる可能性があります。

3. ブート環境のコピー

リモートからVNCで接続して、メニューから”SD Card Copier”でSDからSSDへコピー

4. cmdline.txt,fstabの修正

SSDをマウント
dnack@raspberrypi:~ $  sudo mkdir /mnt/ex_ssd6
dnack@raspberrypi:~ $  sudo mkdir /mnt/ex_ssd7
dnack@raspberrypi:~ $  sudo mount -t vfat /dev/sda6 /mnt/ex_ssd6
dnack@raspberrypi:~ $  sudo mount -t ext4 /dev/sda7 /mnt/ex_ssd7
/boot/cmdline.txtの修正

/mnt/ex_ssd6/cmdline.txt

console=serial0,115200 console=tty1 root=/dev/mmcblk0p7  rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

root=/dev/mmcblk0p7root=/dev/sda7 に変更。

fstabの修正

/mnt/ex_ssd7/etc/fstab

/dev/mmcblk0p6  /boot           vfat    defaults                0       2
/dev/mmcblk0p7  /                  ext4    defaults,noatime  0       1

mmcblk0p6sda6mmcblk0p7sda7にそれぞれ変更。

5. SDを引っこ抜いてリブート

これでSSDから起動できるはず。

Prism/XamarinでAndroidアプリの作成(7):MVVMとReactivePropertyを使って書き直してみる

ちょっと前に書いたBLEでデバイススキャンするコードをMVVM化。

まだまだいろいろ課題が残っていたり今回の変更で新たに課題が出たりと、キリがいいとは言いづらいが、 諸般の事情でまとまった時間をとりづらくなってきたので、 備忘の意味もこめていったんここまでの作業をまとめた。

今回のまとめ

  • ごちゃごちゃした処理はModelにおしつけて、ViewModelは軽くしよう。 (理想はViewとModelをつなぐだけ)
  • ModelはINotifyChangedを継承して(BindableBaseを継承すればそれが継承している)、ViewModelにプロパティの変更が通知できるようにする。
  • プロパティのバインディングはReactivePropertyを使う。(便利)
  • コマンドのバインディングはReactiveCommandを使う。(超便利)
  • ReactivePropertyの変更は購読できる。(超々便利)
  • リアクティブプロパティは明示的にDisposeする必要がある。Disposeするためには、リアクティブプロパティを使うクラスにIDisposableを継承したうえで、おまじないを書け。(面倒だけどおまじないだから仕方ない)。

MVVM化

前回のBLEスキャンのプログラムは、Modelのクラスを作るのが面倒なので、ごちゃごちゃした処理を全部ViewModelに書いていた。

しかし、本来はViewModelはModelからViewに表示するものだけを引っ張ってきてViewに表示できるように加工するためだけにいるのが美しく、BLEを何するみたいなUIに関係ない処理はModelにもっていくほうがいいだろう。

ちょっと古い記事だけど、この辺りに書かれている考え方。 ugaya40.hateblo.jp

まあそういうことでちょっときれいにしてみた。

Model

まずはModel。

BleControllerModel

using Plugin.BLE;
using Plugin.BLE.Abstractions.Contracts;
using Prism.Mvvm;
using Prism.Services;
using Reactive.Bindings;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Essentials;

namespace SamplePrismApp.Models
{
    public class BleControllerModel : BindableBase
    {

        private List<IDevice> _foundDevices = new List<IDevice>();
        public List<IDevice> FoundDevice
        {
            get { return _foundDevices; }
            protected set { SetProperty(ref _foundDevices, value); }
        }

        private int _foundDeviceCount = 0;
        public int FoundDeviceCount
        {
            get { return _foundDeviceCount; }
            protected set { SetProperty(ref _foundDeviceCount, value); }
        }

        private IPageDialogService _dialogService;
        public BleControllerModel(IPageDialogService dialogService )
        {
            _dialogService = dialogService;
        }

    public async void ScanBleDevices()
        {
            var ble = CrossBluetoothLE.Current;
            var adapter = CrossBluetoothLE.Current.Adapter;

            FoundDeviceCount = 0;
            FoundDevice.Clear();

            //State
            Debug.WriteLine("★★★★BLE Stete:" + ble.State);

            //Permission
            PermissionStatus permission = await CheckAndRequestLocationPermission();
            if (permission != PermissionStatus.Granted)
            {
                return;
            }

            //scan device 
            adapter.DeviceDiscovered += (s, a) =>
            {
                FoundDevice.Add(a.Device);
                Debug.WriteLine("★★★★Found:" + a.Device.Id.ToString());
                FoundDeviceCount++;
            };

            adapter.ScanTimeout = 10000;

            await adapter.StartScanningForDevicesAsync();

            foreach (var device in FoundDevice)
            {
                Debug.WriteLine("★★★★:ID" + device.Id.ToString() + ":Name:" + device.Name + ":RSSI:" + device.Rssi.ToString() + ":State:" + device.State.ToString());
            }
        }

        private async Task<PermissionStatus> CheckAndRequestLocationPermission()
        {
            var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();

            if (status == PermissionStatus.Granted)
                return status;

            if (status == PermissionStatus.Denied && DeviceInfo.Platform == DevicePlatform.iOS)
            {
                // Prompt the user to turn on in settings
                // On iOS once a permission has been denied it may not be requested again from the application
                return status;
            }

            if (Permissions.ShouldShowRationale<Permissions.LocationWhenInUse>())
            {
                // Prompt the user with additional information as to why the permission is needed
                await _dialogService.DisplayAlertAsync("Alert",
                    "For BLE communication, permission to location is required.", "OK");
            }

            status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();

            return status;
        }

    }
}

処理の中身は前回のViewModelとほぼ同じなので詳細の説明は省く。

バイスのリストをpublicにした。が、今のところViewModelでこれを使うコードは書けていない。 というわけで代わりにというわけではないがデバイスの数を数えるFoundDeviceCountを追加した。

実際のBLEのユースケースを考えると、そうするとデバイススキャンの終了をViewModelに知らせる仕組みが必要になる気もしなくもないのだが、まあとりあえずそういうのもいったん置く。 Modelからダイアログ出すのはどうなのよという気もしなくもないのだが、これも一身上の都合で気にしない。

大したことはやっていないのだが、一点少し気にしておいたほうがいいかなと思うの、FoundDeviceCount++;の一行。 これは直接プライベートフィールドを触る_foundDeviceCount++と置き換えても、カウンタの値はインクリメントされて正しく動くのだが、 こっちをインクリメントしてしまうとSetPropertyメソッドを通らないので、ViewModelに値の変更が伝わらず、画面に値の変更が反映されない。

逆に言うと通知を飛ばしたくない場合はprivate変数のほうを直接書き換えればいいということになるのかしら。(バグの温床になりそうでぞっとしないな…)

次に説明の都合上ViewModelはかっ飛ばして先にView。

View

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SamplePrismApp.Views.MainPage"
             Title="{Binding Title}">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="Welcome to Xamarin Forms and Prism!" />
        <Label Text="{Binding FoundDeviceCount.Value} "/>
        <Button Text="Device Scan" Command="{Binding DeviceScanCommand}"/>
    </StackLayout>

</ContentPage>

例によってコードビハインド(MainPage.xaml.cs)は空っぽ。 あー、最初から居座ってるWelcomeほげほげっていうメッセージまだ消してないな。。

Modelに新たに実装した、見つけたデバイスの数を表示するためのラベルと、スキャンコマンド実行用のボタンだけ配置。ラベルのバインド先はReactivePropertyをつかうから”.Value"を付けるのを忘れてはいけない。 ここは特にめあたらしいものはないのでさっさと次に行く。

ViewModel

最後にこいつらをつなぐViewModelはこんな感じ。 MainPageViewModel.cs

using Prism.Navigation;
using Reactive.Bindings;
using SamplePrismApp.Models;
using System;
using System.Linq;
using Reactive.Bindings.Extensions;
using System.Reactive.Linq;
using System.Reactive.Disposables;

namespace SamplePrismApp.ViewModels
{
    public class MainPageViewModel : ViewModelBase, IDisposable
    {

        public ReactiveProperty<string> FoundDeviceCount { get; set; }
        public ReactiveCommand DeviceScanCommand { get; private set; } = new ReactiveCommand();

        private INavigationService _navigateionService;
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();

        public MainPageViewModel(INavigationService navigationService, BleControllerModel bleController )
            : base(navigationService)
        {
            Title = "Main Page";
            _navigateionService = navigationService;

            DeviceScanCommand.Subscribe( bleController.ScanBleDevices );

            FoundDeviceCount = bleController.ObserveProperty(x => x.FoundDeviceCount)
                .Select(x => x.ToString())
                .ToReactiveProperty()
                .AddTo(Disposable);
        }

        public void Dispose()
        {
            Disposable.Dispose();
        }
    }
}

面倒なことはModelに押し付けてだいぶすっきりした。

まずIDisposableを継承して、DisposableDispose()を定義している点。 ちゃんとしないとメモリリークの可能性があるとか。 私はちゃんと理解していないけれど、おまじないと思ってやっている。(だめじゃん) blog.okazuki.jp

Reactive Propertyを使うための税金と思って払っておく。払った以上の利益が出るから問題ない。 ※追記 (どこからかはわからないが)最新のPrismだと ViewModelBaseにIDestructiveが継承されているので、IDisposableを継承せずに、public void Dispose()の代わりにpublic override void Destroy()として同じことができる。

コンストラクタの引数に BleControllerModel bleController を持っているが、これはこの前ちょっと書いたと思うDependency Injectionの仕組みでオブジェクトを渡している。(あとのエントリーポイントのところでちょっとだけ説明する。)

ViewにあったUI要素のFoundDeviceCountDeviceScanCommandはそれぞれReactiveProperty<string>ReactiveCommandで実装。 (Model.FoundDeviceCountはintだけど、Viewではラベルのテキストとバインドしているので、stringにする必要がある。)

コマンドのほうはSubscribeメソッドでモデルのScanBleDevicesを登録。こっちは簡単。

ラベルのほうはちょっといかついけれど、順を追っていくと簡単で(簡単でないところはとりあえずそういうものと思えばいい。Don't think.Feeeeeeel!!)

bleController.ObserveProperty(x => x.FoundDeviceCount) で表現される、bleControllerのFoundDeviceCount変更を監視するIObeserveProperty<int>型(FoundDeviceCountがIntのため型がintになる)を

.Select(x => x.ToString())IObeserveProperty<string>型に変換して、

.ToReactiveProperty()ReactiveProperty<string>型に変換、これでめでたくラベルのテキストにバインドできる形になる。

最後に .AddTo(Disposable)でお祈り。(お祈りいうな)

FoundDeviceCountが変更になったときに実行したい処理(仮にDoFunc)があれば、さらに

FoundDeviceCount.Subscribe( _ => DoFunc(FoundDeviceCount.Value)).AddTo(Disposable)

みたいな感じでお手軽に購読もできる。何これむちゃ便利じゃん。 (なお、この場合も.AddTo(Disposable)を付ける必要があるみたい)

Modelの変数とViewの変数の型があってれば、.Selectの行はいらなくて、この行のない形を、 ModelのプロパティをViewにつなぐときの公式みたいに考えればいいのかもしれない。 まあ私も実は中身は全然わかってないので、使う分には慣れでどうにかなるっちゃなる。 (いや、勉強しようという気はあるんですよ、気だけは… Rxマジでなんもわからん。)

エントリーポイント(コンテナの登録)

最後に、App.xaml.cx

using Prism;
using Prism.Ioc;
using SamplePrismApp.Models;
using SamplePrismApp.ViewModels;
using SamplePrismApp.Views;
using Xamarin.Essentials.Implementation;
using Xamarin.Essentials.Interfaces;
using Xamarin.Forms;

namespace SamplePrismApp
{
    public partial class App
    {
        public App(IPlatformInitializer initializer)
            : base(initializer)
        {
        }

        protected override async void OnInitialized()
        {
            InitializeComponent();

            await NavigationService.NavigateAsync("NavigationPage/MainPage");
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterSingleton<IAppInfo, AppInfoImplementation>();

            containerRegistry.RegisterForNavigation<NavigationPage>();
            containerRegistry.RegisterForNavigation<MainPage, MainPageViewModel>();

            containerRegistry.RegisterSingleton<BleControllerModel>();
        }
    }
}

containerRegistry.RegisterSingleton<BleControllerModel>(); がウィザードが勝手に描いたコードから手動で変更した唯一の変更にして最大のポイント。 ここで、BleControllerModelをDIコンテナに登録している。 BleControllerは2つ動かせるようになっていないので、Singletonにしておけばいい。 ここに登録しておくと、あとはDIコンテナのアーキテクチャがうまいことしてくれて、 クラスの引数としてBleContollerModelを渡したら、存在しているBleControllerModelに解釈してくれる。(存在していなければ内部で自動的にnewするのだと思う。)

というわけで今回は以上。

こんな風に、処理は全部Modelがやって、見せ方はViewが勝手に考えて、その間をViewModelが取り持つというのが私の持っているイメージ。

f:id:dnack:20210919210340p:plain

まあ、なかなかうまいことこんなにきれいにならないんだけどねぇ。。

2021/9/25 追記:最新のPrismだと ViewModelBaseにIDestructiveが継承されているので、それを継承しているなら、わざわざ追加でIDisposableを継承しなくてよい。

Raspberry Pi:GPIOをたたこう(準備編)

Raspberry Pi買ったからにはGPIO叩きたいよね。 PythonだとRPi.GPIOがいい感じなんだけど、C言語だとWiringPiが開発中止になってC言語で使えるいい感じのがみつからない。 WiringPiの開発者がフリーライドとサポートの教養で嫌気がさしたんだとか。 まあ、あれだ。オープンソースの貢献者への感謝と敬意は忘れちゃいけない。 そういう人々の無償の貢献あってこそのソフトウェア技術なわけで。

まあそれはさておき、なくなったものは仕方ないわけで、 まあ、どっかから探してくることもできるだろうし、代替のものもいくつかあるんだけど、 そもそもGPIO制御レジスタの仕様はSoCのデータシート見れば書いてあるんだからレジスタ叩けばいいよね。 でもユーザ空間からレジスタ叩くのもなんかだってだってなんだもんって感じなので勉強がてらドライバ書きますかね。 となってこのエントリができる運びとなる。

もっと手っ取り早く、RPi.GPIOのソースとってきて"c_gpio.h"をincludeしちゃえば、C版のRPi.GPIOみたいに使えそうな気はして、それはそれで試したい気もするのだけど。それはまた今度気が向いたら。

kernel-header のインストール

$sudo apt-get install raspberrypi-kernel-headers

の後再起動。

再起動後にカーネルバージョンもヘッダのバージョンに変わる。

再起動しないで作業すると、カーネルのバージョンとヘッダのバージョンが合わなくて、 作ったモジュールがロードできなくて困る。

ビルド

ホームの下にsourceとでもディレクトリを掘って、その下で次の2つのファイルを作る。

test.c

#include <linux/module.h>

static int load_module(void)
{
        printk("load test module.\n");
        return 0;
}

static void unload_module(void)
{
        printk("unload test module.\n");
}

module_init(load_module);
module_exit(unload_module);
MODULE_LICENSE("Dual MIT/GPL");

なんもしてないので、説明は省きますねー。

Makefile

obj-m += test.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean

これを書いたら、makeしちゃおう。

dnack@raspberrypi:~/source $ make
make -C /lib/modules/5.10.60-v7l+/build M=/home/dnack/source modules
make[1]: ディレクトリ '/usr/src/linux-headers-5.10.60-v7l+' に入ります
  CC [M]  /home/dnack/source/test.o
  MODPOST /home/dnack/source/Module.symvers
  CC [M]  /home/dnack/source/test.mod.o
  LD [M]  /home/dnack/source/test.ko
make[1]: ディレクトリ '/usr/src/linux-headers-5.10.60-v7l+' から出ます

ロード

無事makeできてtest.koもできたようなので、ロードしてみる。

dnack@raspberrypi:~/source $ sudo insmod test.ko
dnack@raspberrypi:~/source $ sudo rmmod test.ko

何もしていないドライバなので何も起きないが、 dmesgでちゃんとprintkで書いた内容が書かれているのでちゃんと動いていそうなことがわかる。

dnack@raspberrypi:~/source $ dmesg

[ 4006.636317] load test module.
[ 4009.320525] unload test module.

めでたしめでたし。 というわけでドライバ書く準備できたので、次から本気出す。 次はいつになるんだろうなぁ。。

Raspberry Pi:雑多な作業メモ

朝活で多少いじったが、まとめる時間がないので取りあえずメモ。

OS/アプリの更新

$ sudo apt update $ sudo apt full-upgrade -y

ここでコマンド実行中にapt-listchangesのところでつんの目ってるようなので、いったん中断(^C)して、 $ sudo dpkg -r apt-listchanges してから

$ sudo apt full-upgrade -y $ sudo apt autoremove $ sudo apt clean

groups.google.com

qiita.com

Samba

$ sudo apt-get install samba

(DHCP から WINS 設定を使うよう smb.conf を変更しますか?に対しては いいえ)

smb.conf を適宜修正して、

$ testparm でconfファイルチェック

$ sudo smbpasswd -a ユーザ名 でユーザにsmbのパスワード追加

$ sudo systemctl restart smbd でサンバサーバの起動。

smb.confの設定は色々見なおす必要ありだが とりあえずWindowsから入れたのでOK。

homedify.com

愚痴

JCOMから借りてるルータにラズパイの優先側もDHCPの固定IP割り当てしようと思ってマックアドレス入れ間違えたら、 それが何をしても消せなかった。 試しに前に入れた無線のマックアドレスのIP固定設定を消すと消せたのだけど、新しく入れたほうは消せない。 バグっぽいのであきらめて、無線ルータの設定を出荷時初期化して入れなおして事なきを得たがいらぬ時間をとってしまった。

それよりJCOM回線何とかしたいけど賃貸だしなぁ。