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

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クライアントでした

おしまい


 

コメント

このブログの人気の投稿

ImDiskの後継アプリのAIM toolkitを試してみた

ImDiskの開発辞めちゃったらしい というわけで、じゃあ後継ソフトはあるのかと思ったらあった 遅いと言われているけど・・・ とインストールしてみた ImDiskのこの間の結果はこちら 再びRAM DISK(ImDisk)   blog.mazepin-led.com    そして今回のAIM Toolkit なんか、無茶苦茶遅くなってるな 下手するとSSDの方が速いじゃん CPUの使用状態はこんな感じ PIO転送なところは変わって無さそう まあ、遅いからと言ってその速度が体感できるのかというと出来ないんだけどね と思ったところで、なんか設定変えてみたらどうなるのだろう Allocate Memory Dynamicallyというのは、メモリを必要に応じてってことだからと思って以前試したことがあったけど、なんかImDiskのときは不安定だったんだよな  AdvancedのとこにあるUse AWE Physical Memoryというのが良くわからないけど、チェックしてみたら速くなった おお、大分速くなった。なったけど・・・・なんで? これだとImDiskよりちょっと遅いくらいになるのか もしやQuickFormatととかでも変わるのか?と思ったけどそこまでやる気も無かったので放置。速くなるわけないよね。 後は圧縮とか、取り外しメディアとかだし。   最近の性能の良いSSDのおかげで RAM DISKというものの存在意義が薄れちゃったね まあそれでとにかく速度を稼ぎたいぜって人は SoftPerfect RamDiskでも使ってください 昔はPrimo Ramdisk使ってました。当時使ってたけどなかなか良かった。 当時SSD無茶苦茶高かったし。 今はほぼ無用になってしまったが・・・ 未だにスタンダード版は8GBまでなんだな キャッシュ領域にしたいなら大人しく余ってるSSDを使った方がいいんじゃ無いかって気がするけど  おしまい

QCC dongle proを買った→Windows11で接続までのみちのり

何があった  買ったけど最初の接続に手間取ったので そのメモ QuestyleのQCC dongle proを買ったのだけど、使っているイヤホンと接続できない なんでー? ペアリングしてるのに 説明書 説明書には外装のQRコードを読むように書いてあるのだけど そもそもpage not found 404になる   Windowsには・・・ ちゃんと  ヘッドホンのところには QCC Dongle Pro って出てるのよね しかし音が出ない つーか、新しいペアリングはどうやるのだ  スマホが必要でした 何だそりゃっていいたくなるがどうやらまず、スマホにQCC dongle Pro接続して専用アプリで ドングルとイヤホンをペアリングする必要があった 何だそりゃー Google PlayでQuestyleで検索かけるか こちら でまずはインストール そしてスマホにドングルを刺して、このアプリ内からイヤホンをペアリング そしてそのドングルをWindows11の空いてるUSB TYPE-Cポートに突き刺す   音が出た! やったね  結局音が良くなった?  どうなんだろうね 正直言ってよくわからん ただ、マイケルジャクソンのThe Jamのオープニングのガラスの割れる音は 今までよりも細かくパリパリ聞こえるようになったから効果はあったんだと思う それと遅延が少なくなった  finalのZE3000 SVというイヤホンを使っていたのだけど今までのただのUSBドングルで YouTubeを見てると明らかに音と画面のタイミングがずれてて、 Bluetoothイヤホンってこんなもんかと表他のだけどそれが亡くなったのはとても快適 LDACは遅延が酷いとか書いてあったからビビってたんだけどね(でも有線イヤホン使うと、あ、これが合ってる状態なんだってなるので遅延はあるみたい)   あと、たまにイヤホンと繋がらなくなるねぇ 相性問題というやつなのだろうか    今までのUSBドングルが捨てられない 最期に罠が一つ そりゃ無いぜって言いたくなるのが今までのUSBドングルが捨てられないこと このQCC dongle pro はWindowsからはイヤホンとして認識されるので他のBluetoo...

結局Alternative A2DP Driverにした

なぜならば 手持ちの安物のBluetoothドングルで使えたから QCC dongle pro を買ったのってこのドングルがあれば、イヤホンに LDAC 接続してスマホとのブルートゥース接続も出来ると思ったから ところが、このドングルはイヤホンと1対1で繋がってしまって、他の接続には一切使えない 今まで使ってた安物のドングルと2つも使うことになってしまった  ソフトウェア ドライバーを入れ替えればいいのであればそっちの方がいいや そう思ってドライバーを入れました  ドングル PCに刺さっているドングルは TP-Link Bluetooth 5.4 USB Adapter というやつ 1000円ちょっとで買えます Alternative A2DP Driver インストール AlternativeA2dpのオフィシャルページ AlternativeA2dpの説明など ダウンロードは下の方にある   www.bluetoothgoodies.com ↑このページの下の方にあります   後は他のアプリと同様にインストール   パラメータ調整 この画面で適当に自分のイヤホンにあった設定をして下さい finalの ZE3000 SV はaptには一切対応していないので迷うことなくLDACを選択しました 購入  気に入らなかったら削除しておしまい 気に入ったら購入しますが、Alternative A2DP Driverの右上をクリックすると購入が出てくるので 決済ページに飛んでいくので決済 私は PayPal で払いました  PayPalだと何も入力しなくても、PayPalのアカウントにメールで認証ファイルが送られてきます  ちなみに買ったのはAACサポート有りのバージョン $8. 99でした(無しだったら$5.99) 購入したライセンスの適用 購入したライセンスの適用を選択したら、ダイアログが出てくるのでメールで到着したファイルを選択するとライセンスが適用されます 下の方に今のビットレートとか出てくるからまあそれを参考に自分の設定を詰めていってくださいな エンコード品質はSQ(中)にしました HQ(高)だとたまに無音になるのだけど、結構マシンパワーあるはずなのだけどなぁ 不思議なのは...