スキップしてメイン コンテンツに移動

ESP32のOTA(Over The Air)をスマホからやる:スマホ側の話(.NET MAUI)

ESP側の話の続きです

Andoroid 

まあウインドウズでも動いちゃったんですが
開発環境はVisual Studio2022
.NET MAUI
です

つーても実証実験のタメに作っただけなので、C#でTCPクライアントを作って、パケットを送りつけるというとても雑なアプリです
このページ見た人は「何だよ俺でも出来るじゃん」と思うでしょう

出来上がったイメージはこんな感じ


 

ソリューションを作る

Visual Studioで.NET MAUIのアプリを作って下さい。
今回はTcpClientMauiというnamespaceにしましたが、そこはご自由に

firmwareの用意

まあ何は無くてもファームウェアの用意です。
こいつを送り込んでやります。

つーても私の場合は他のアプリをいくつか作ってるので、その中から適当に選択したアプリのfirmware.binを使いました

ESP32のファームを作るのにVS Code上のplatform IOを使ってる人はプロジェクトフォルダの下の

.pio/buildの中を引っかき回せば見つかります。(adruinoはどこにあるんだろ・・・)

それをZIPで圧縮してVisual Studio のさっき作ってプロジェクトのResourcesの下のRAWにドラッグします

そして、大抵は勝手にプロパティのビルドアクションがMauiAssetになってるのだけどなってなかった場合はMauiAssetにしよう

MauiAsset

コーディング

後はコーディング

ZipHelperクラス(ZipHelper.cs)

namespace TcpClientMaui
{
    internal class ZipHelper
    {
        public static byte[] ExtractZipFromMemory(byte[] zipData)
        {
            using (var zipStream = new MemoryStream(zipData))
            using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Read))
            {
                // ここで必要なエントリを選択して解凍します
                // 例: 最初のエントリを取得
                var entry = archive.Entries[0];

                using (var entryStream = entry.Open())
                using (var memoryStream = new MemoryStream())
                {
                    entryStream.CopyTo(memoryStream);
                    return memoryStream.ToArray();
                }
            }
        }
    }
}

さっきリソースフォルダにZipファイルを放り込んだのですが、それをメモリに展開するためのクラスです
引数にZipのバイナリデータを与えると、戻り値に解凍したバイナリイメージが返却されます

TCPClientクラス(TcpClientClass.cs)

namespace TcpClientMaui
{
    internal class TcpClientClass
    {
        string _ipadr;
        int _port;
        TcpClient _client;
        NetworkStream _stream;
        bool _isRunning = false;

        public TcpClientClass(string ipadr, int port)
        {
            _ipadr = ipadr;
            _port = port;
        }

        public void SendData(byte[] data)
        {
            if (_stream != null)
            {
                _stream.Write(data, 0, data.Length);
            }
        }

        public void Disconnect()
        {
            if (_stream != null)
            {
                _stream.Close();
                _stream.Dispose();
            }
            if (_client != null)
            {
                _client.Close();
            }
            if (_isRunning)
            {
                _isRunning = false;
            }
        }
        /// <summary>
        /// 接続
        /// </summary>
        public void Connect()
        {
            try
            {
                // 接続
                _client = new TcpClient(_ipadr, _port);
                _stream = _client.GetStream();// ネットワークストリーム取得
                _stream.ReadTimeout = 10000;
                _stream.WriteTimeout = 10000;
                _isRunning = true;

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                if (_stream != null)
                {
                    _stream.Close();
                    _stream.Dispose();
                }
                if (_client != null)
                {
                    _client.Close();
                }
                if (_isRunning)
                {
                    _isRunning = false;
                }

            }

        }
    }
}

ESP32側で待ち受けしているTCPサーバに

  • 接続するconnect()メソッド
  • 切断するdisconnect()メソッド
  • データを送信するSendData()メソッド

を記述します(しまったいらないからって受信イベントを作るの忘れてた)

メインページ(MainPage.xaml)

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="TcpClientMaui.MainPage">

    <ScrollView>
        <VerticalStackLayout
            Padding="30,0"
            Spacing="25">
            <Image
                Source="dotnet_bot.png"
                HeightRequest="185"
                Aspect="AspectFit"
                SemanticProperties.Description="dot net bot in a race car number eight" />

            <Label
                Text="Hello, World!"
                Style="{StaticResource Headline}"
                SemanticProperties.HeadingLevel="Level1" />

            <Label
                Text="Welcome to &#10;.NET Multi-platform App UI"
                Style="{StaticResource SubHeadline}"
                SemanticProperties.HeadingLevel="Level2"
                SemanticProperties.Description="Welcome to dot net Multi platform App U I" />

            <Entry x:Name="TxtIp" Text="192.168.4.1" />
            <Entry x:Name="TxtPort" Text="80" Keyboard="Numeric"  />
           
            <Button
                x:Name="ConnectBtn"
                Text="接続"
                SemanticProperties.Hint="サーバに接続します"
                Clicked="OnConnect"
                HorizontalOptions="Fill" />

            <Button
                x:Name="DisconnectBtn"
                Text="切断"
                SemanticProperties.Hint="サーバから切断します"
                Clicked="OnDisconnect"
                HorizontalOptions="Fill" />

            <Button
                x:Name="SendBtn"
                Text="送信"
                SemanticProperties.Hint="サーバにメッセージを送信します"
                Clicked="OnSend"
                HorizontalOptions="Fill" />
           
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

何も難しいことはないかと。
.NET Mauiアプリを作ると出来上がるテンプレートに
IPアドレスとポートを記述するテキストボックス、接続、切断、送信の各ボタンを置いただけです

メインページ(MainPage.xaml.cs)

using System.IO;
using System.Net.Sockets;
using System.Text;

namespace TcpClientMaui;

public partial class MainPage : ContentPage
{
    TcpClientClass _tcpClient;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public MainPage()
    {
        InitializeComponent();
    }

    /// <summary>
    /// プラットフォームによってはリソースフォルダからリードしたstreamの長さが取得できないので、いったんアプリケーションフォルダにコピーしてからリードする
    /// </summary>
    /// <param name="filename"></param>
    /// <returns></returns>
    public async Task CopyFileToAppDataDirectory(string filename)
    {
        // バンドルされたファイルを開く
        using Stream inputStream = await FileSystem.Current.OpenAppPackageFileAsync(filename);
        // 出力ファイル名を作成
        string targetFile = Path.Combine(FileSystem.Current.AppDataDirectory, filename);
        // ファイルをAppDataDirectoryにコピー
        using FileStream outputStream = File.Create(targetFile);
        await inputStream.CopyToAsync(outputStream);
    }

    private async Task<byte[]> Fileread()
    {
        // .NEt MAUI Resourcesフォルダのバイナリデータ(xx.zip)をリードして
        // バイトデータを返す    

        await CopyFileToAppDataDirectory("firmware.zip");

        byte[] data = null;
        using(FileStream stream = File.OpenRead(Path.Combine(FileSystem.Current.AppDataDirectory, "firmware.zip")))
        {
            data = new byte[stream.Length];
            await stream.ReadAsync(data, 0, data.Length);
        }

        // Windowsの場合は以下のようにリソースフォルダからリードすればいい
        //byte[] data = null;
        ////using var stream = await FileSystem.OpenAppPackageFileAsync("firmware.zip");
        //using var stream = await FileSystem.Current.OpenAppPackageFileAsync("firmware.zip");
        //data = new byte[stream.Length];
        //await stream.ReadAsync(data, 0, data.Length);

        return data;
    }

    /// <summary>
    /// 接続
    /// </summary>
    /// <param name="sender">送信元</param>
    /// <param name="e">イベント</param>
    private void OnConnect(object sender, EventArgs e)
    {
        _tcpClient = new TcpClientClass(TxtIp.Text, int.Parse(TxtPort.Text));
        _tcpClient.Connect();
    }

    /// <summary>
    /// 切断  
    /// </summary>
    private void OnDisconnect(object sender, EventArgs e)
    {
//          _tcpClient.Disconnect();
    }

    private async void OnSend(object sender, EventArgs e)
    {

        byte[] data = await Fileread();

        byte[] zipData = ZipHelper.ExtractZipFromMemory(data);

        if (data != null)
        {
            _tcpClient.SendData(zipData);
        }
    }
}

接続と切断は接続して切断するだけ
おおう、切断がコメントになってるぜ・・・

送信について(OnSend()メソッド)

まあここが肝です(肝と言っても解凍したデータをそのまま送信関数にぶち込んでるだけなんだけど)

  1. 最初にfileread()メソッドでZIPファイルをデータとしてメモリに取り込みます
    とは言え・・実はFilereadメソッド中で変なことをしているのはAndroid等では直接streamに取り込むとサイズがわからないので、いったんリソースからファイルにコピーしてからメモリにリードしています
  2. ZipHelperクラスで解凍
  3. 送信!

まあこんな感じでしょうか
これでESP32にファームを送信出来ます

確認したのは

AndroidとWindows11です。 

使い方

 

最初にWi-FiでESP32のアクセスポイントに接続
そしたら、作ったアプリで接続→送信。↑のはWindows11でで試しているところ。
Andoridエミュレータ上からでも接続出来ました。実機の場合は実機のWi-fi接続の画面から接続して、それからアプリを動作させてください

改造

改造というか・・・ あくまで実証実験用に作ったのでとんでもなく雑に作っています。以下の点に突っ込み無用でお願いします

  1. データ送信サイズ
    受信側のESP32はこんなただのパケット送信でどうやって終了を検知するのだろう?
    そうです、このアプリはファームのサイズを固定値で決め打ちしています。
    受信側のESP32に受信サイズが定数として記述されているのはこのためです。つまり決まったサイズを受信したら、更新完了と判定しているわけです。
    当たり前ですが実アプリではこんなことしません。
  2. 一発で送信完了
    Sendでファームを1回だけ送信してるけど大丈夫なの?
    大丈夫な訳ないですね。
    通常は数キロバイト毎に分割して送信します。そしてその中で電文としてこのパケットは何番目なのかとか、最終パケットだよとか通知します。
  3. 受信がないけど?
    本来は必要です。ESP側で失敗したときは返信電文で「失敗したからロールバックしたぜ。もう送ってくんな」とか送り返します。
  4. エラートラップは?
    実証アプリつってんだろ

最後に

OTAやるぜって意気込んだ割に普通の手抜きTCPクライアントでした

おしまい


 

コメント

このブログの人気の投稿

RAM DISKを使ってみた(使ったのはImDisk)

GWだし、まあちょっとラムディスクを入れてみました うちのPCはWindows11 使ったのはImDiskというRAM Disk。 まあ、この辺のインストールとかはあちこちで解説してる人がいるので適当にぐぐってくださいな で、とりあえずベンチマーク なかなかいいスピードだ で、大抵の人はブラウザのキャッシュをRAMディスクにするといいよ・・・と言うけど そもそもメインドライブがNVMeのSSDを使っている状態で、体感速度なんか上がらない(使い終わったキャッシュを再起動したら綺麗さっぱり捨て去ってくれるという利点はある)  うちで一番効果があるのは Adobe Audition というアプリ これが結構高速化する(キャッシュをちゃんとRAMディスクにしたら・・・だけど) ハイレゾ音源だと、1時間の音源が何かする度に4GBのファイルを作られてしまう なので、RAM DISKにすると、結構編集時間を短縮できる Premiere Rushも出力先をRam Diskにしておいて終わったら、SSDにコピーすると言う事をやるとかなりスピードアップになる 実はうちのPCは普段は99%のパワーで動作していて、CPUのターボブーストが掛からないようになっている 大体3.6GHz当たりで安定してるのだけど、これを100%にするとターボブースト機能がONになって一部のコアが4.5とか4.8GHzまで上がる まあ、毎回電源オプションをいじる事になるのだけどさ・・・ そうしてベンチを取ると こんな感じ とは言え、ブーストしてるからと行ってRam Diskのスピードの差を体感する事はさすがに無理 ・・・と言うかフォトショでもRAM DISKにしてよかった・・・と言うほど変わらない SSDの性能が上がってきたしもし次にPCを買い換えたらRAM DISKよりSSDの方が速いかもね  

.NET MAUI BLE(Bluetooth Low Energy)も上手く行った

2023/10/11更新 ↓こちらの記事で更新しれました。 .NET MAUI PLUGIN BLEがWindows(10/11)に対応してた Windows対応 イヤッッホォォォオオォオウ Bluetooth LE plugin for Xamarin &amp; MAUI がWindowsに対応してた~ nugetでver3.00をみんな早速ゲットだ。   ちなみにこちらはAndroid版のサンプルアプリ   GitHub...   [ブログカード風リンクタグ作成] ------------------------------------------------------------------------------ 昔、スマホとESP32の接続確認用に作ったプログラムをXamarinからMAUIに移植したら動いちゃった   まあ、ESP32からスマホへの一方通行なアプリなんだけど 面倒くさいので github に公開した 下手くそなコード書きやがってとか思われそう 「間違ってるぞこのやろう」というのを見つけたら教えてくれると嬉しいです 要素技術の調査はこれで完了かな

.NET MAUIでスプラッシュスクリーン Android12で地獄を見る

まずは起動するところから・・・・  スプラッシュスクリーンだぬ マイクロソフトのサイト 見てると簡単そう(実際簡単で細かい事を気にしなければsvgファイルを用意して1行だけ書き換えておしまい)   なんかプロジェクトファイルに自動で記述されるらしい よし、プロジェクト作った                     つーか、もうスプラッシュがあるんだけど・・・・ そして自作のsvgファイル くうっ・・・デザインセンスない・・・・   それはともかく、このSVGファイルを Resources\Images にドラッグ&ドラッグ プロジェクトを右クリックして「プロジェクトファイルの編集」 して、編集できるようになったプロジェクトファイルを 自分のプロジェクトファイルに書き換える <MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="168,168" /> ↓ <MauiSplashScreen Include="Resources\Splash\ splashtestmaui .svg" Color="#512BD4" BaseSize="168,168" />   とりあえずAndroidで実行 お、おう・・・・ まあ最初はこんなもんよね 最初に紹介したマイクロソフトのサイトでもBaseSizeを書き換えてくださいって言ってるし 言われたとおりに <MauiSplashScreen Include="Resources\Splash\splashtestmaui.svg" Color="#512BD4" BaseSize=" 320,600 " /> 書き換えてみると いい感じじゃーん じゃあアンドロイドのバージョン毎に試してみよう   Android 7 Android 11 Android 12 Android 13