查看原文
其他

一文教你如何用 Python 将 iPhone “玩弄于股掌之中”!

John Coates CSDN 2018-07-22

点击上方“CSDN”,选择“置顶公众号”

关键时刻,第一时间送达!

关于 iOS 的技术解读有很多,但是却鲜有设备可视化同步的介绍文章。本文一起了解下这个酷炫的 iOS 黑科技。


以下为译文:

一直以来,我可能都定义错了“量子纠缠(Quantum Entanglement)”这个古老的力学技术。通过实验我发现,利用它甚至可以应用到技术领域,实现酷炫的设备同步!下面我们来一步步验证看看吧。

我们的任务很简单——如上图所示,实时获取设备的当前方向。

UIDevice.current.orientation

首先,需要调用

beginGeneratingDeviceOrientationNotifications()

但仅仅这样还不行。因为如果设备上的旋转被锁定了,那么就不会产生以上通知。我的相机应用程序从头到尾都需要知道方向——所以我意识到我需要直接根据设备的加速度计算方向。

......情况不妙,这比我一开始想的复杂多了。

不过,运行 Core Motion 也并不是难事。一个简单的 import CoreMotion,之后可以使用 CMMotionManager() 来创建一个移动管理器。然后通过向该移动管理器的 startDeviceMotionUpdates 传递闭包和队列来接收值。

import CoreMotion
let motionManager = CMMotionManager()
let queue = OperationQueue()
motionManager.startDeviceMotionUpdates(to: queue) { (data: CMDeviceMotion?, error: Error?) in
   guard let data = data else {
       print("Error: \(error!)")
       return
   }
   let attitude: CMAttitude = data.attitude
   print("pitch: \(attitude.pitch)")
   print("yaw: \(attitude.yaw)")
   print("roll: \(attitude.roll)")                                                  
}

好了,现在有了这些值,我们该做些什么呢?这是一个较难的部分。如果将所有内容都输出到控制台,那么我们很快就会被大量数据淹没。我认为还是在屏幕上显示这些值比较好。

但是,等等,如果将数值显示在图表上,会怎么样?别想图表了,我们可以来用开源的 Blender 试试,它可以实现这些值的可视化,并且很容易扩展。

这不是在为 Blender 打广告,只纯粹探讨利用其技术实现的可视化功能。

经过深思熟虑后,我打开了 Blender.org,并下载了最新版本。下载好后,搜索“iPhone 3D 模型”并点击第一个结果,它还自带 .blend 文件,非常棒。

打开该文件后,屏幕中间会显示一个漂亮的 iPhone 6 的模型。漂亮!所以我们只需要将这个模型与真正的 iPhone 通过量子纠缠在一起。

事实上,我也不知道这个屏幕上绝大部分控件是干什么用的

我之前从未使用过 Blender,所以我在 Google 上搜索了一大堆东西,并试图找出正确的搜索条件,以获得足够的知识来完成我想做的事情。

最终我发现下一步是使用 Text Editor 面板。因此,我通过点击 Blender 窗口左下角的小时钟图标,将 Timeline 面板改成了 Text Editor 面板。

点击底部中心的小按钮+,就可以创建新的文本文件。如果你希望按照本文描述按部就班,那么请将其命名为 Motion Server。

Blender 插件是用 Python 编写的。在新的编程环境中要做的第一件事,就是在控制台输出一些东西。因此,输入 print("hi") 并单击 Run Script 就可以确认控制台输出。

你应该看不到有文字输出。我甚至检查了 macOS 控制台应用程序,空空如也。好吧,让我们将右下方的面板变更为 Python 控制台。

现在我们只需输入 print("hi"),但是我们想要使用之前写的代码,免得之前敲键盘的努力白费了。我们可以复制并粘贴以下代码到控制台:

exec(compile(bpy.data.texts[‘Motion Server’].as_string(), Motion Server’, ‘exec’))

按 Enter 键运行该代码段,然后在控制台中显示 “hi”。 搞定!

刷新监控台

然而 Blender 并不是很好的代码编辑器,所以我们还是使用钟爱的外部编辑器吧。为了调用外部文件,我们可以将 print("hi") 替换成以下代码:

import bpy
import os
filename = os.path.join(os.path.dirname(bpy.data.filepath), "server.py")
exec(compile(open(filename).read(), filename, 'exec'))

下一步,我们需要在与 .blend 文件相同的文件夹中创建新的 server.py 文件,我们真正的代码就要保存在这里。现在我们可以用任何编辑器打开它,你可以选择 Atom、Sublime,甚至 Word 2007 都行。

我不是 Python 专家,所以我不得不在 Google 上搜索如何使用数组,以及如何解析 JSON。

最终,在一连串的复制和粘贴之后,我的服务器开始运行了。

我们使用的是 Python 的 C 语言绑定,所以实际上它是非常底层的代码。我们绑定一个 TCP 套接字来监听所有的接口,然后循环接受新的连接,并调用 select 来检查任何可读的连接。所有这些工作都需要在一个新的线程中完成,以免阻塞 Blender 的主线程。因为这种行为可能会让 Blender 锁死,这不是闹着玩的事儿。

有趣的是,重新运行上面的脚本会覆盖我们的全局函数,所以可以使用它在不断开客户端的情况下更新功能,这样修改代码就更容易了。

一旦连接可读,就读取数据,解析 JSON,并将生成的对象发送给 receivedMotionData。该函数引用当前屏幕上的 iPhone 对象。我们可以使用右上角的面板重命名 iPhone,以便我们的代码正常工作。

Cube 并不是立方体,来改个名

找到该 Cube 对象,点击右键并选择重命名,重命名为 iPhone。现在让我们再来看一看 server.py。

import socket
import select
import json
import threading
import traceback
class ServerThread(threading.Thread):
   def __init__(self):
       threading.Thread.__init__(self)
       self.running = True
   def stopServer(self):
       self.running = False
       self.server.running = False
   def run(self):
       try:
           self.server = Server()
           while self.running:
               self.server.receive()
       except:
           pass
class Server:
   def __init__(self):
       self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
       self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
       self.socket.setblocking(False)
       self.socket.bind((str(socket.INADDR_ANY), 9845))
       self.socket.listen(2)
       self.running = True
   def __exit__(self, exc_type, exc_value, traceback):
       self.socket.close()
   def receive(self):
       pairs = []
       timeout = 1
       while self.running:
           sockets = list(map(lambda x: x[0], pairs))
           if len(pairs) > 0:
               read_sockets, write_sockets, error_sockets = select.select(sockets, [], [], timeout)
               for sock in read_sockets:
                 data = sock.recv(4096)
                 if not data :
                   print('Client disconnected')
                   pairs = []
                 else :
                    self.connectionReceivedData(connection, data.decode())
           try:
               try:
                   connection,address = self.socket.accept()
                   print("new connection: ", connection)
                   pairs.append((connection, address))
               except:
                   pass
           except:
               pass
       for pair in pairs:
           (connection, address) = pair
           connection.close()
   def connectionReceivedData(self, connection, data):
       try:
           motionData = json.loads(data)
       except json.decoder.JSONDecodeError:
           print("Invalid JSON: ", data)
           return None
       receivedMotionData(motionData)
# This is a global so when we run the script again, we can keep the server alive
# but change how it works
import bpy
def receivedMotionData(motionData):
   phone = bpy.context.scene.objects["iPhone"]
   phone.rotation_quaternion.x = float(motionData['x'])
   phone.rotation_quaternion.y = 0 - float(motionData['z'])
   phone.rotation_quaternion.z = float(motionData['y'])
   phone.rotation_quaternion.w = float(motionData['w'])
   pass
try:
   if serverThread.running == False:
       serverThread = ServerThread()
       serverThread.start()
       print("Starting server")
   else:
       print("Server already running, using new motion handler.")
except:
   serverThread = ServerThread()
   serverThread.start()
   print("Starting server")

乍一看上去很多代码的样子。

将上面的代码放到 server.py 中,然后在 Python 控制台中按方向键上,调用最后一个执行语句。服务器运行后,控制台现在应该显示说“服务器已启动”。现在我们快速地测试一下。创建一个名为 client.py 的新文件,并保存以下代码:

import socket
send = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
send.connect(("localhost", 9845))
send.send("{ \"x\": 1.2, \"y\": 0, \"z\": 1.2, \"w\": 0}")
send.close()

如果你使用的是 Sublime,可以点击 Tools - > Build 菜单项运行它。否则,可以在终端上,使用 python /path/to/client.py 运行。

检查 Blender,你应该看到 iPhone 根本没有改变。这是因为上面的脚本使用四元组设置了 iPhone 的旋转角度,并且它使用了欧拉角进行旋转。需要做一些修改。将 Python 控制台面切换到 “Properties”,然后单击该面板顶部的橙色立方体图标。中部 Transform 的下面,点击 XYZ Euler 并选择 Quaternion。现在尝试再次运行 client.py。

你应该看到 iPhone 立即翻转过来了。不要惊慌,这就是我们想要的。现在,我们需要让这个模型跟着实际的 iPhone 旋转。

上述步骤成功后,你应该看到这样的效果

我们需要将运动数据从 iPhone 发送到运行 Blender 的计算机。感谢上苍我们不需要深入到 Swift 中的原始 C 套接字级别,因为 Foundation 具有抽象。

我们可以将以下代码放入新的 iOS 项目中,以替换默认的 ViewController。请确保使用计算机的本地 IP 地址替换 host 变量。

import UIKit
import CoreMotion
class CoreMotionViewController: UIViewController, StreamDelegate {
   let motionManager = CMMotionManager()
   let queue = OperationQueue()
   let host = "192.168.1.2"
   override func viewDidLoad() {
       super.viewDidLoad()
       setUpStreams(host: host)
       motionManager.startDeviceMotionUpdates(to: queue) { (data: CMDeviceMotion?, error: Error?) in
           guard let data = data else {
               print("Error: \(error!)")
               return
           }
           let attitude: CMAttitude = data.attitude
           let quaternion = attitude.quaternion
           var motionData = MotionData()
           motionData.x = quaternion.x
           motionData.y = quaternion.y
           motionData.z = quaternion.z
           motionData.w = quaternion.w
           let encoder = JSONEncoder()
           do {
               let json = try encoder.encode(motionData)
               self.send(data: json)
           } catch let error {
               print("Couldn't send data, error: \(error)")
           }
       }
   }
   // MARK: - Streams
   var inputStream: InputStream?
   var outputStream: OutputStream?
   func setUpStreams(host: String) {
       var readStream: Unmanaged<CFReadStream>?
       var writeStream: Unmanaged<CFWriteStream>?
       CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault,
                                          host as CFString, 9845,
                                          &readStream,
                                          &writeStream)
       inputStream = readStream!.takeRetainedValue()
       outputStream = writeStream!.takeRetainedValue()
       guard let inputStream = inputStream, let outputStream = outputStream else {
           print("Failed to create streams")
           return
       }
       inputStream.delegate = self
       outputStream.delegate = self
       inputStream.schedule(in: .current, forMode: .commonModes)
       outputStream.schedule(in: .current, forMode: .commonModes)
       inputStream.open()
       outputStream.open()
   }
   func send(data: Data) {
       guard let outputStream = outputStream else {
           return
       }
       _ = data.withUnsafeBytes {
           outputStream.write($0, maxLength: data.count)
       }
   }
   func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
       if eventCode == .errorOccurred {
           inputStream = nil
           outputStream = nil
           print("Error: Stream error")
       } else if eventCode
== .endEncountered {
           inputStream = nil
           outputStream = nil
           print("Error: Encountered end of stream")
       }
       let maxReadLength
= 4096
       if eventCode == .hasBytesAvailable {
           guard let inputStream = inputStream else {
               return
           }
           while inputStream.hasBytesAvailable {
               let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxReadLength)
               inputStream.read(buffer, maxLength: maxReadLength)
               buffer.deallocate()
           }
       }
   }
}
// MARK: - Data Model
private struct MotionData: Codable {
   var x: Double = 0
   var y: Double = 0
   var z: Double = 0
   var w: Double = 0
}

代码非常简单。它为 host 打开一个套接字,然后为每个动作更新创建一个 MotionData 值,在其上设置属性,将其编码为 JSON,并发送到在 Blender 中运行的脚本。同时读取 host 发送的任何数据并将其丢弃。

现在你应该可以看到完整的可视化效果了,恭喜。

成功实现了!

那么最终我是如何从移动管理器获取方向信息的?这个是留给读者的一个练习。哈哈,开个玩笑,点击这里获取代码:

https://github.com/JohnCoates/Slate/commit/34e89b2eb7a0d80f144abaa10d98d2a7b52d7fe6#diff-7115c8127d96763420a91c4c72a58788R91

原文:https://medium.com/@JohnCoatesDev/visualizing-an-ios-device-in-blender-through-quantum-entanglement-ba8b6f0b47a5

作者:John Coates

译者:弯月,责编:郭芮

声明:本文已获作者翻译授权。

  征稿啦!

CSDN 公众号秉持着「与千万技术人共成长」理念,不仅以「极客头条」、「畅言」栏目在第一时间以技术人的独特视角描述技术人关心的行业焦点事件,更有「技术头条」专栏,深度解读行业内的热门技术与场景应用,让所有的开发者紧跟技术潮流,保持警醒的技术嗅觉,对行业趋势、技术有更为全面的认知。
如果你有优质的文章,或是行业热点事件、技术趋势的真知灼见,或是深度的应用实践、场景方案等的新见解,欢迎联系 CSDN 投稿,联系方式:微信(guorui_1118,请备注投稿+姓名+公司职位),邮箱(guorui@csdn.net)。

————— 推荐阅读 —————

点击图片即可阅读

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存