トップ

LSM9DS1(9軸センサ)を付けたRaspberry Pi Pico W より受け取ったTCP受信データで、UnityのGameObjectを動かす

実験のイメージ

このページは、「AE-LSM9DS1-I2Cで実験した内容 その1」と「その2」の続きです。
使用しているSTマイクロ社の複合センサーLSM9DS1をI2Cで容易に制御可能したモジュールです。
この複合センサーLSM9DS1は、3軸の加速度センサーと3軸のジャイロセンサおよび3軸の磁力センサーの9軸を持っています。
このセンサー情報を、Unityの作品へTCPで送信し、Unity側ではこの受信したセンサーデータでGameObjectを動かす実行の紹介です。
(左がUnity、右がセンサー付きPicoWで、Unityで受信したセンサー情報で直方体のGameObjectを動かしている)

Raspberry Pi Pico and Pico W の情報源

ブレッドボード上に、Raspberry Pi Pico WとLSM9DS1(9軸センサ)モジュールを取りつ付けています。
これに小型のモバイルバッテリーに固定し、手に持って空中で操作するすることで、Unityアプリを操作しようとする試みです。
下記で示すUnityアプリは、TCPサーバーを起動して、Pico WからTCP接続して、次のようなデータを送信する仕組みにしています。

Accelerometerの'A'、またはgyroscopeの'G'、またはmagneticの'M'の1byte Xの2byte情報 Yの2byte情報 Zの2byte情報

この各センサーの受信データは、Unityのトグルボタンで、使うか使わないか?の切り替えて実験できるようにしています。
なおUnit側では起動時にTCPサーバーとして起動し、接続がなければ その"TCPサーバーとしてのIPv4ドレス:ポート番号"を、 マルチキャストで送信します。
Raspberry Pi Pico W側ではWifiに接続後、マルチキャストで受信した"サーバーのIPv4ドレス:ポート番号"で、 そのサーバーに接続して、そこにLSM9DS1の9軸センサー情報を送り続けます。

センサー情報を受信して、直方体を動かすUnity側の例

Unit側では起動時(Startup)にTCPサーバーを非同期で起動しています。
そしてFixedUpdateで接続要求を監視します。
FixedUpdateで接続要求があればそのクライアントの接続を許可して、その通信用ストリームをnetworkStreamに記憶し、
受信処理のループ(receiveLoop)メソッド駆動します。
このスレッドが起動していない状態(networkStream==null)では、 サーバーのIPアドレスとポート番号を知らせるため、2秒間隔でマルチキャスト送信を行います。
マルチキャストで送信する文字列は、"TCPサーバとしてのIP V4ドレス文字列:ポート番号"の文字列です。
(送信例、"192.168.1.110:59002" のような文字列を、utf-8でエンコードしたバイナリで送信)
なお、下記ではマルチキャストのアドレスが"239.192.0.0"、ポート番号が59001のコードになっています。

receiveLoopメソッドで7byteごとのデータを受信します。
その先頭1byteの'M','G','A'のいすれかで、 それに続く6byteは、磁気センサーまたはジャイロセンサーまたは加速度センサーの情報となり、 対応するreceiveFlag〜のフラグ変数をtrueにします。

FixedUpdateでは、これら受信があるかのフラグ変数で、対応するセンサー情報を使い、 下記ソースをアタッチした直方体のGameObjectを動かしています。

なおOnGUIでは、各センサー情報それぞれのセンサー受信情報を表示しています。
同時に各センサー受信情報を使って、操作対象の直方体を動かすか指定するチェックボタンを設けています。
つまりチェックがついているセンサー情報だけを使って、直方体を動かしています。
また、操作対象の直方体GameObjectを初期位置に戻すなどを行う「リセット」ボタンを配置しています。
なお、磁気センサーで傾き、ジャイロセンサーで回転動作、加速度センサーで移動を行わせています。
しかし、希望の挙動は得られていない状態で、下記コードはまだ改良が必要です。
以下のコード(SensorServer.cs)を操作対象の直方体GameObjectにアタッチしています。
using UnityEngine;
using System.Net.Sockets;
using System.Net;
using System;
using System.Threading.Tasks;
using System.Threading;

public class SensorServer : MonoBehaviour
{
    IPAddress multicastIP = IPAddress.Parse("239.192.0.0");
    const int multicastPortNo = 59001;
    Task ipSendTask = null;//マルチキャストでip送信するタスク

    const int tcpServerPortNo = 59002;// TCPサーバで使うポート番号
    public IPAddress ipAddress;// TCPサーバで使うデバイスのIPアドレス
    public TcpListener tcpListener = null; //サーバー用オブジェクト(接続待機用)
    TcpClient tcpClient = null;// 
    NetworkStream networkStream = null;// クライアントと通信するストリーム

    bool toggleM= true, toggleG = true,toggleA = true;// OnGUIトグルボタン状態

    bool receiveFlagM = false, receiveFlagG = false, receiveFlagA = false;// 受信制御
    long receiveCount = 0;//受信ブロックデータ数

    Vector3 v3M = Vector3.zero;// Magnetometerセンサーの受信情報
    float degreeZ;  // 上記から計算したZ軸の角度
    Vector3 v3G = Vector3.zero;// Gyroscopeセンサーの受信情報
    Vector3 v3A = Vector3.zero;// Accelerometerセンサーの受信情報
    Vector3 v3M_rest = Vector3.zero;
    Vector3 v3A_rest = Vector3.zero;

    void Start()
    {
        this.ipAddress = MyServer.GetMyIPv4();// このマシンのIPv4アドレスを取得
        this.tcpListener = new TcpListener(this.ipAddress, SensorServer.tcpServerPortNo); //サーバー用オブジェクト生成
        this.tcpListener.Start();
        Debug.Log($"Use ip of  address with {this.ipAddress.ToString()}-----Sever Strat-----" );
        Task task = MyServer.udpReceiveAsyncLoop(multicastIP.ToString(), 59000);
    }

    void FixedUpdate()
    {
        if (tcpListener.Pending())// 保留中の接続要求があるか(接続依頼が来た?)
        {
            this.tcpClient = tcpListener.AcceptTcpClient();// 接続を許可 
            Debug.Log("-----Accept-----");
            this.networkStream = this.tcpClient.GetStream();// クライアントと通信するストリーム取得
            Thread receiveThread = new System.Threading.Thread(new System.Threading.ThreadStart(receiveLoop));
            receiveThread.Start();
        }
        if (this.networkStream == null)
        {
            if (this.ipSendTask == null)
            {   // 自身のサーバとなるIPアドレスの文字列を、マルチキャストで送信
                string sendData = this.ipAddress.ToString() + "," + tcpServerPortNo;
                byte[] buf = System.Text.Encoding.UTF8.GetBytes(sendData);
                this.ipSendTask = MyServer.SendMulticast(multicastIP, multicastPortNo, buf);
            }
            else if (this.ipSendTask.IsCompleted) this.ipSendTask = null;
        }
        if (this.networkStream == null) return;// 接続状態でない

        // 以下は、このアプリのTCPに接続してきたクライアントと接続状態で実行--------
        while (this.receiveFlagM || this.receiveFlagG || this.receiveFlagA) {
            if (!this.toggleM) this.receiveFlagM = false;
            if (!this.toggleG) this.receiveFlagG = false;
            if (!this.toggleA) this.receiveFlagA = false;

            if (this.receiveFlagM)
            {// 磁気センサーの情報で、このGameObjectを姿勢(rotation)を設定
             // Quaternion参考:https://xr-hub.com/archives/11515
                Quaternion rotation = Quaternion.LookRotation(v3M);//方向を、回転情報に変換 // y と xを、逆に指定
                if(this.v3M_rest != Vector3.zero)
                {
                    Quaternion rotation2 = Quaternion.LookRotation(this.v3M_rest);//方向を、回転情報に変換
                    rotation = rotation * Quaternion.Inverse(rotation2);//Quaternionの引き算に相当する計算
                }
                transform.rotation = rotation;
                this.receiveFlagM = false;
            }

            if (this.receiveFlagA)
            {// 加速度センサーの情報で、このGameObjectを移動
                Vector3 acc = this.v3A - this.v3A_rest;
                //Vector3 acc = this.v3A ;
                if (this.receiveCount >= 10) this.gameObject.transform.Translate(acc * 0.00001f);
                this.receiveFlagA = false;
            }
            if (this.receiveFlagG)
            {// ジャイロセンサーの情報で、このGameObjectを制御。
                Quaternion rotation = Quaternion.LookRotation(this.v3G);
                Vector3 g = rotation.eulerAngles;
                this.transform.Rotate(this.v3G * 0.01f);
                this.receiveFlagG = false;
            }
        }
    }

    private void receiveLoop()// 別スレッドで動作させるTCP受信ループ
    {
        byte[] buf = new byte[7]; // 受信用バッファ 
        int idx = 0;
        int c;
        byte[] buffer2 = new byte[sizeof(UInt16)];// バイナリから整数に変換用バッファ
        while (this.networkStream != null)
        {
            try
            {
                c = this.networkStream.ReadByte();
                if (c == -1) break;
            }
            catch (Exception e)
            {
                Debug.Log(e);
                break;
            }
            buf[idx++] = (byte)c;
            if (idx < buf.Length) continue;
            idx = 0;

            this.receiveCount++; // センサー受信回数(M,A,Gの合計)
            if (this.receiveFlagM == true && this.receiveCount > 100) continue;
            Buffer.BlockCopy(buf, 1, buffer2, 0, buffer2.Length);
            short x = System.Buffers.Binary.BinaryPrimitives.ReadInt16BigEndian(buffer2);
            Buffer.BlockCopy(buf, 1 + 2, buffer2, 0, buffer2.Length);
            short y = System.Buffers.Binary.BinaryPrimitives.ReadInt16BigEndian(buffer2);
            Buffer.BlockCopy(buf, 1 + 4, buffer2, 0, buffer2.Length);
            short z = System.Buffers.Binary.BinaryPrimitives.ReadInt16BigEndian(buffer2);

            if(buf[0] == 'M' && this.receiveFlagM == false)
            {
                v3M = new Vector3(y, -x, z);
                degreeZ = Mathf.Atan(v3M.y / v3M.x) * 360 / Mathf.PI / 2; // 角度算出
                this.receiveFlagM = true;
            }
            else if(buf[0] == 'G' && this.receiveFlagG == false)
            {
                v3G.x = -y; v3G.y = -x; v3G.z = z;
                this.receiveFlagG = true;
            }
            else if(buf[0] == 'A' && this.receiveFlagA == false)
            {
                v3A.x = y; v3A.y = x; v3A.z = -z;// y と xを逆、zは逆に指定
                this.receiveFlagA = true;
            }
        }
        this.networkStream = null;
        Debug.Log("break receiveLoop");
    }

    private void OnGUI() // 参照:https://docs.unity3d.com/ja/2022.3/Manual/gui-Controls.html
    {
        GUIStyle lableStyle = GUI.skin.label;// ---Labelスタイル設定
        lableStyle.fontSize = 26;
        GUI.color = new Color(0.9f, 0.0f, 0.9f); //文字色設定
        float whidth = 720;
        float height = 100; 
        float xPos = 120;
        float yPos = 50;
        toggleM = GUI.Toggle(new Rect(0, yPos, 120, 50), toggleM, " Magnetometer");
        string msg = $"x={v3M.x,6:F}, y={v3M.y,6:F}, z={v3M.z,6:F} degZ={degreeZ,6:F2}";//受信データの確認
        GUI.Label(new Rect(xPos, yPos, whidth, height), msg);

        yPos += 50;
        toggleG = GUI.Toggle(new Rect(0, yPos, 120, 50), toggleG, " Gyroscope");
        msg = $"x={v3G.x,6:F}, y={v3G.y,6:F}, z={v3G.z,6:F} ";//受信データの確認
        GUI.Label(new Rect(xPos, yPos, whidth, height), msg);

        yPos += 50;
        toggleA = GUI.Toggle(new Rect(0, yPos, 120, 50), toggleA, " Accelerometer");
        msg = $"x={v3A.x,6:F}, y={v3A.y,6:F}, z={v3A.z,6:F} ";//受信データの確認
        GUI.Label(new Rect(xPos, yPos, whidth, height), msg);

        yPos += 50;
        Vector3 acc = this.v3A - this.v3A_rest;
        msg = $"x={acc.x,6:F2}, y={acc.y,6:F2}, z={acc.z,6:F2} ";//実験データの確認
        //GUI.Label(new Rect(xPos, yPos, whidth, height), msg);

        yPos += 50;
        GUI.color = new Color(1f, 1f, 0.0f); //文字色設定
        if (GUI.Button(new Rect(xPos + 100, yPos, 100, 60), "リセット"))
        {
            this.transform.position = Vector3.zero;
            this.transform.rotation = Quaternion.identity;
            this.v3M_rest = v3M;
            this.v3A_rest = v3A;
            Debug.Log($"---------------- REST -----{v3M_rest}----------------");
        }
    }
}
上記で使っているMyServer.csのコードを下記に示します。
ここでは、実行マシンのIPv4のアドレス群の検索で、最初に見つかったアドレスを返すメソッドや、 マルチキャスト関連のstaticメソッドを定義しています。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System;

public class MyServer 
{
    // Unity C# 実行マシンのIPv4のアドレス群の検索で、最初に見つかったアドレスを返すメソッド(見つからない場合はnullを返す)
    public static System.Net.IPAddress GetMyIPv4()
    {
        int idxIp = -1;
        string hostName = System.Net.Dns.GetHostName();
        Debug.Log($"HostName==============================={hostName}");
        IPAddress[] ipAddressArray = Dns.GetHostAddresses(hostName);
        for (int i = 0; i < ipAddressArray.Length; i++)
        {
            Debug.Log($"{ipAddressArray[i]}======IPv4 :{ipAddressArray[i].AddressFamily == AddressFamily.InterNetwork}");
            if (ipAddressArray[i].AddressFamily == AddressFamily.InterNetwork) // 最初に見つかったIPv4
            {
                idxIp = i;
                break;
            }
        }
        if (idxIp == -1) return null;
        // 以上で、使うIPアドレスが、ipAddress[idxIp]に決定
        return ipAddressArray[idxIp];
    }

    // ip:portNo のマルチキャストアドレス用Socketを生成して、送信準備をして、そのソケットを返す
    public static Socket getMulticastSocket(IPAddress ip, int portNo)
    {
        Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(ip));
        socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 2);
        IPEndPoint ipep = new IPEndPoint(ip, portNo);
        socket.Connect(ipep);
        return socket;
    }

    // 2秒後に、ip:portNo のマルチキャストでsendDataを送信する
    public static async System.Threading.Tasks.Task SendMulticast(IPAddress ip, int portNo, byte []sendData)
    {
        System.Net.Sockets.Socket udpSocket = getMulticastSocket(ip, portNo);
        await System.Threading.Tasks.Task.Delay(2000); // 2秒の遅延を非同期に待機
        udpSocket.Send(sendData);
        udpSocket.Close();
    }

    //  マルチキャスト受信と、Debug.Log表示
    public static async System.Threading.Tasks.Task udpReceiveAsyncLoop(string multicast_IP, int portNo)
    {
        UdpClient client = new UdpClient();
        client.ExclusiveAddressUse = false;
        client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        IPEndPoint localEp = new IPEndPoint(IPAddress.Any, portNo);
        client.Client.Bind(localEp);
        IPAddress multicastIP = IPAddress.Parse(multicast_IP);
        client.JoinMulticastGroup(multicastIP, IPAddress.Any);
        //client.JoinMulticastGroup(IPAddress.Parse("239.255.0.1"), IPAddress.Any);//複数登録可能
        while (true)
        {
            try
            {
                UdpReceiveResult udpReceiveResult = await client.ReceiveAsync();
                string text = System.Text.Encoding.ASCII.GetString(udpReceiveResult.Buffer);
                Debug.Log($"udpReceiveAsyncLoop:{multicastIP}:{text}");
            }
            catch (System.Exception e)
            {
                Debug.Log($"{e.ToString()}");
                break;
            }
        }
    }
}

LSM9DS1(9軸センサ)を付けたRaspberry Pi Pico W 側の例

Raspberry Pi Pico W起動時に自動実行させるため、下記コードのファイル名は「main.py」の名前で作成します。
(Thonnyの環境でPico内に保存します。これで次回起動時(電源オン時)から自動実行されます。)
これで、モバイルバッテリーに接続して操作できます。
このようにPCに繋いでいなくてもどこまで動作しているかが分かるように、LEDを点灯の制御をしています。
電源投入で、次のように実行します。(途中でLEDの点滅が無くなったら,何かしらの不具合がると分かります)
  1. GP14接続のLEDを点灯
  2. Wifiのアクセスポイント(picowtcpモジュールにSSIDとパスワードが記述)に接続するまで繰り返す。
  3. GP14接続のLEDを消灯
  4. "239.192.0.0:59001"のマルチキャストの受信で、サーバーIPアドレスとポート番号を得る。
  5. GP14接続のLEDを点灯
  6. LSM9DS1の初期化、レジスタなどの設定
  7. サーバーに接続していなければ、接続してセンサー情報を送信し続ける。
    1. GP14接続のLEDを点灯
    2. UnityのサーバーにTCP接続する
    3. GP14接続のLEDを消灯
    4. センサー情報が得られたらTCP送信し、WifiのオンボードのLED点灯状態を逆転させる。
from machine import Pin, I2C
import picowtcp
import utime
import _thread

target_pin = Pin(14,Pin.OUT) # 使用LED利用の準備
onboard_led = Pin( "LED", Pin.OUT )

target_pin.high()# 外付けLED点灯(GP14接続のLEDを点灯)
print( picowtcp.wifi__connect() ) # Wifi接続
target_pin.low()# 外付けLED消灯(GP14接続のLEDを消灯)

ip_port_bin = picowtcp.receiveIpMulticast("239.192.0.0",  59001) # 接続サーバIPアドレス受信
ip_port_a = ip_port_bin.decode('utf-8').split(',')
picowtcp.ip=ip_port_a[0]
picowtcp.portnumber=int(ip_port_a[1])
print(f"Multicast Recive IP:PORT:{picowtcp.ip}:{picowtcp.portnumber}")

target_pin.high()# 外付けLED点灯(GP14接続のLEDを点灯)

client=None# TCPサーバに接続で得られる自身のクライアント用socket記憶用

def receive(sock): #TCP受信のスレッド用関数
    print( "start receiveData" )
    while True:
        bin = sock.recv(1)#1byte受信
        print(f"----{bin}")
    #
    print( "receiveData ended." )

from machine import Pin, I2C
import utime

I2C_SDA=16# GP16のジェネラル端子をSDA「Serial Data」に指定用変数
I2C_SCL=17# GP17のジェネラル端子をSCL「Serial Clock」に指定用変数
I2C_CH=0# I2Cのチャンネル指定用変数(Picoでは0と1のチャンネルがある)
I2C_M_ADDR=0x1C#磁力センサのアドレス
I2C_AG_ADDR=0x6a#加速センサとジャイロセンサのアドレス

# SCL はスタンダードモードの400KHzの通信モードで、I2Cを初期化
i2c=I2C( I2C_CH, scl=Pin(I2C_SCL), sda=I2C_SDA, freq=400000)#★
for addr in i2c.scan(): print( f"enabled address:{addr:02x}" )

# 磁気センサー関連のレジスタ番号と設定値(lsm9ds1.pdf参照)
CTRL_REG1_M_N=0x20# レジスタ番号
CTRL_REG1_M=0b10010000# 温度補正 Low-power mode ODR:10Hz デフォルト:00010000
CTRL_REG2_M_N=0x21
CTRL_REG2_M=0b00000000# Full scale:±4gauss デフォルト:00000000
CTRL_REG3_M_N=0x22
CTRL_REG3_M=0b00000000# I2Cのenable Continuous conversion mode デフォルト:00000011
CTRL_REG4_M_N=0x23
CTRL_REG4_M=0b00000000# Z-axis:low power, data LSb at lower address, デフォルト: 00000000
CTRL_REG5_M_N=0x24
CTRL_REG5_M=0b00000000# continuous update デフォルト: 00000000
STATUS_REG_M=0x27# 状態
OUT_X_L_M=0x28 # このアドレスより、OUT_X_H_M、OUT_Y_L_M、OUT_Y_H_M、OUT_Z_L_M、OUT_Z_H_Mレジスタが並ぶ
OFFSET_X_REG_L_M=0x05

# ジャイロのセンサー関連のレジスタ番号と設定値
CTRL_REG1_G_N=0x10# レジスタ番号
CTRL_REG1_G=0b11010000# 角速度センサ制御レジスタ デフォルト:00000000
#CTRL_REG1_G[7〜5bit]のODR_G:110 測定周期952Hz
#CTRL_REG1_G[4〜3bit]のFS_G:01 Gyroscope full-scaleを 58dps
#CTRL_REG1_G[1〜0bit]のBW_G:00 帯域幅
CTRL_REG2_G_N=0x11
CTRL_REG2_G=0b00000000# デフォルト:00000000
CTRL_REG3_G_N=0x12
CTRL_REG3_G=0b00000000# デフォルト:00000000
#CTRL_REG3_G でLow-power disabled、High-pass filter desable,Gyroscope high-pass filter cutoff frequency set
STATUS_REG=0x17# 状態
OUT_X_G=0x18 # このアドレスより、OOUT_Y_G (1Ah - 1Bh)、OUT_Z_G (1Ch - 1Dh)のレジスタが並ぶ

# 加速度のセンサー関連のレジスタ番号と設定値
CTRL_REG4_N=0x1E
CTRL_REG4=0b00111000#0,0,Zen_G,Yen_G,Xen_G,0,LIR_XL1,4D_XL1 #デフォルト値:00111000
CTRL_REG5_N=0x1F
CTRL_REG5=0b00111000#0,0,Zen_G,Yen_G,Xen_G,0,LIR_XL1,4D_XL1 #デフォルト値:00111000
CTRL_REG6_XL_N=0x20
CTRL_REG6_XL=0b00000000#デフォルト値:00000000
#CTRL_REG6_XL[7〜5bit]はジャイロスコープ利用時無効?で、ジャイロスコープと同じ測定周期
#CTRL_REG6_XL[4〜3bit]はAccelerometer full-scale:±2g
#CTRL_REG6_XL[4〜3bit]
OUT_X_L_XL=0x28#OUT_X_H_XL,OUT_Y_L_XL,OUT_Y_H_XL,OUT_Z_L_XL,OUT_Z_H_XL

#磁力センサのアドレスを指定して、各制御レジスタを設定
i2c.writeto_mem( I2C_M_ADDR, CTRL_REG1_M_N, bytes([CTRL_REG1_M]))
i2c.writeto_mem( I2C_M_ADDR, CTRL_REG2_M_N, bytes([CTRL_REG2_M]))
i2c.writeto_mem( I2C_M_ADDR, CTRL_REG3_M_N, bytes([CTRL_REG3_M]))
i2c.writeto_mem( I2C_M_ADDR, CTRL_REG4_M_N, bytes([CTRL_REG4_M]))
i2c.writeto_mem( I2C_M_ADDR, CTRL_REG5_M_N, bytes([CTRL_REG5_M]))

#各ジャイロセンサレジスタをを上記の通り設定
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG1_G_N, bytes([CTRL_REG1_G]))
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG2_G_N, bytes([CTRL_REG2_G]))
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG3_G_N, bytes([CTRL_REG3_G]))

#各加速度のセンサーレジスタを上記の通り設定
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG4_N, bytes([CTRL_REG4]))
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG5_N, bytes([CTRL_REG5]))
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG6_XL_N, bytes([CTRL_REG6_XL]))

target_pin.low()# 外付けLED消灯(GP14接続のLEDを消灯)
while True:
    try:
        while client == None: # サーバが受け入れるまで、TCP接続要求を繰り返す
            target_pin.high()# 外付けLED点灯(GP14接続のLEDを点灯)
            client=picowtcp.tcp_connect()
            if client != None: t_id = _thread.start_new_thread(receive, (client,) )
        #
        statu=i2c.readfrom_mem( I2C_M_ADDR, STATUS_REG_M,1)
        #print(f"statu:{statu[0]:08b}", end=" ")
        if statu[0] & 0x80 :
            bin=i2c.readfrom_mem( I2C_M_ADDR, OUT_X_L_M, 6) # X,Y,Z軸の磁気センサー情報取得
            #print( [v for v in bin] )
            magbin=[bin[n] + (bin[n+1]<<8) for n in range(0,len(bin),2)] # 2byteからuint16へ変換
            magbin=[v if v <= 32767 else v - 65536 for v in magbin] # uint16からint16へ変換
            #deg = math.atan(magbin[1]/magbin[0]) * 360 / math.pi / 2 # 角度算出
            #print( f"M:{magbin[0]:6}, Y:{magbin[1]:6}, Z:{magbin[2]:6} , deg={deg:5.2f}" )
            picowtcp.send_data(client, 'M', magbin)
        if statu[0] & 0x02 : # ジャイロセンサー情報あり?
            bin=i2c.readfrom_mem( I2C_AG_ADDR, OUT_X_G, 6) # X,Y,Z軸のジャイロセンサー情報取得
            #print( [v for v in bin] )
            gyro=[bin[n] + (bin[n+1]<<8) for n in range(0,len(bin),2)]
            gyro=[v if v <= 32767 else v - 65536 for v in gyro]
            picowtcp.send_data(client, 'G', gyro)
        if statu[0] & 0x01: # 加速度情報情報あり?
            bin=i2c.readfrom_mem( I2C_AG_ADDR, OUT_X_L_XL, 6) # X,Y,Z軸の加速度情報取得
            #print( [v for v in bin] )
            accel=[bin[n] + (bin[n+1]<<8) for n in range(0,len(bin),2)]
            accel=[v if v <= 32767 else v - 65536 for v in accel]
            #print( f"Accelerometer:{accel}" )
            picowtcp.send_data(client, 'A', accel)
        if (statu[0] & 0x80) or (statu[0] & 0x02) or (statu[0] & 0x01):
            if onboard_led.value(): onboard_led.low()
            else: onboard_led.high()
            #utime.sleep(0.5)
    except Exception as e:
        print( e )
        client = None
        utime.sleep(0.5)

client.close()

上記で利用しているpicowtcp.pyモジュールの内容を以下に示します。
Wifiに接続するメソッドを定義しています。  の箇所は、利用環境に合わせて変更して使います。
また、マルチキャスト受信メソッド、TCP接続メソッド、TCP送信メソッドを定義しています。
import socket
import network
import utime
import struct

SSID='〇〇'# Wifi用
PW = '〇〇〇'# 上記に接続するためのパスフレーズ

ip, portnumber="192.168.0.110",59001#接続先サーバのアドレス

def wifi__connect():# Wifiに接続するまで繰り返す
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(SSID, PW)
    while wlan.isconnected() == False:
        print('Connecting to Wi-Fi AP')
        utime.sleep(1)
    #
    wlan_status = wlan.ifconfig()
    return f'''Connected!
    Pico IP Address:{wlan_status[0]} Netmask:{wlan_status[1]}
    Default Gateway:{wlan_status[2]} Name Server: {wlan_status[3]}'''

# https://micropython-docs-ja.readthedocs.io/ja/latest/library/socket.html

def ipv4_inet_pton(ip: str)->bytes:# iPv4のアドレス文字列に対応する4byteバイナリを返す
    a=ip.split('.')
    return bytes(map(int,a))

def receiveIpMulticast(multicast_group, portNo): #マルチキャスト
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # ソケットの作成
    server_socket.bind(('', portNo)) # ソケットにアドレスをバインド
    # マルチキャストグループに参加
    group = ipv4_inet_pton(multicast_group) # 引数のIPアドレス文字列に対応する4byteバイナリ取得
    mreq = struct.pack('4sL', group, 0)
    server_socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
    # 受信
    data, addr = server_socket.recvfrom(1024)  # データとアドレスを受信
    return data
    
def tcp_connect():
   client=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   print(f'Connecting to {ip}:{portnumber}')
   client.connect((ip, portnumber))
   client.setblocking(True)# client.settimeout(None)  # タイムアウトを秒数で指定
   #client.setblocking(False)
   return client

def send_data(sock:socket, prefix:str, d16s:list):
    bin=prefix.encode('utf-8') + int(d16s[0]).to_bytes(2,'big')
    bin+=int(d16s[1]).to_bytes(2,'big')+int(d16s[2]).to_bytes(2,'big')
    n=0
    while n < len(bin):#上記の(1+2*3)=7byteを送る。(非ブロッキングにも対応)
        n+=sock.write(bin[n:])
    #sock.write(bin)、 sock.send(bin)、sock.sendall(bin)# などで全てOSError: [Errno 103] ECONNABORTED
    return bin
    
if __name__ == '__main__':
    print( wifi__connect() )
    client = tcp_connect()
    bin=send_data(client, 'M', [50, 127, -1]) # 7byteのバイト例で送信
    print( f"送信データ:{bin}")
    while True:
        client.write(bin)
        #utime.sleep(0.001)
    client.close()