ブレッドボード上に、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軸センサー情報を送り続けます。
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のコードを下記に示します。
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; } } } }
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()
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()