众所周知,小巧的手机上有很多传感器。

某一天(昨天)我突发奇想,如果把手机传感器作为电脑的输入设备呢?

比如说,拿方向传感器控制鼠标,控制游戏角色移动什么的。

闲的也是闲的,不如写个程序试试吧。

策划

写程序嘛,要先策划一下,既然要在手机上取值,Android开发是少不了的,然后取到值发给电脑,电脑需要一个程序来接收,考虑到从手机上得到的值可能不只是用在一个地方,避免写多个接收程序,干脆写一个专门接收值的程序。

所以,现在有两个任务,

  • 写一个发送程序
  • 写一个接收、分发程序

继续策划,

接收程序可能会接收不止一个发送程序,而且数据的传送是要尽可能低延迟、数据包丢了也没什么关系,所以,用UDP协议。

发送程序要去接收程序那里注册一下,这个注册的过程需要稳定的通信,用TCP协议。

综上,UDP和TCP我都要,接收程序毫无疑问用Go写了,为了减少代码量,TCP换成HTTP。

发送程序

这是运行安卓上的,我虽然学过点安卓开发,可真的就一点。

这个程序是这样的,1连接服务器注册 -> 2获得传感器值 -> 3UDP发送 -> 4回到第2步

多么简单的程序啊,现学现用吧。

我学过一点点kotlin,这不用不知道,这不全是用的Java的东西嘛。

Http请求,不管在哪里写,都很简单,经过查来查去,把发送Http请求的代码写好了,先测试一下Http请求的代码,目前接收程序没写,先随便找个网站连接。

不试不要紧,一试就崩了。

经过我ADB附加调试+查百度,原因是:没有设置连接网络权限。

接下来又出了大问题:输入IP地址能够连接,但是输入域名就异常。

还是附加调试,看错误对象的内容,错误内容是和线程有关系的。

我到现在为止还没碰过线程,你给我报个线程错误?肯定是程序的错!

接下来,百度,我发现,Android不允许在主线程内做长时间操作,你只要敢写,他就敢给你抛异常。

线程这东西,我在Java里面不会用,在Kotlin里面不会用,在Android + Kotlin + Java更不会啦。

线程什么的,干脆百度复制粘贴。

Go里面一句话完成的事情,在这里要搞这么久!?

接下来是Json解析,Json解析没用操什么心,这样,注册部分完成。

注册会告诉注册的ID和UDP的端口号

接下来是传感器和UDP。

传感器还是很省事的,类实现传感器接收的接口,然后接收方法就会不断被主线程调用。

主线程的坑又来了,不管你是什么地址,总之UDP包就是不能在主线程里发。

好,我创建线程发包还不行嘛,于是把前面线程的代码复制了一遍。

线程启动的方法是start,可以通过线程对象监视线程的状态,我的想法是,每一次传感器方法被调用就看看线程的状态,如果没有正在运行,就把数据放进临时变量里然后启动线程。

没问题吧,也保证线程安全了。

但是,Java的线程一旦结束就不允许重启。Java,我***

让线程一直运行吧,我又不会线程交互数据,干脆每次重造线程对象。

能让程序辛苦一点的何必让人辛苦。

完成!

接收程序

这个我熟,Go嘛。

Http没什么好说的,UDP这儿我需要一个超时时间,问题来了。

Go里面的时间,所有整数时间都是用的纳秒,防小学没毕业,还准备了转换的进率,我设置超时时间时用的却不是这个纳秒,是Time这个结构。

这个结构的第一个属性看注释,似乎是秒,我直接3传进去构造了Time。

然后他就莫名其妙收不到UDP数据包。

最终经排查,接收包的函数不断报Timeout错误(为了清静,我从来不让程序打印错误信息)

所以,这个超时时间怎么设置?

Time表示的是绝对时间,所以,这里用time.Now().Add(···),在当前时间上加一些时间作为超时时间,这样在每一次接收数据包之前都需要设置一次超时时间。

数据编码

我想用Json在两个程序之间做数据传输,但是对于这种一直发来发去的场合,编解码Json比较浪费时间,数据包也更大,浮点还会丢失精度。

直接把四字节float的内存发过去,不同平台在float的规范上应该是相同的吧。

在Android上,

// floats -> data
val bos = ByteArrayOutputStream()
val dos = DataOutputStream(bos)
dos.write(regid)
for (f in floats!!){
    dos.writeFloat(f)
}

var data : ByteArray = bos.toByteArray()

在Go上,

// data -> floats
buff := bytes.NewBuffer(data)

for i := 0; i < len(floats); i++ {
	binary.Read(buff, binary.BigEndian, &floats[i])
}

成功了,有一种拿魔法打败魔法的感觉。

实际应用

这样之后的所有数据交互就都在本机进行了,本地数据交换就用Http吧,主要是懒

角度可视化

通过Unity,获取三个旋转参数,赋值给一个物体。

这是一个简单的问题。

但是,

  • C#做Json解析似乎要像Go那样麻烦
  • 代码提示不能用(Unity安装在另一个操作系统,那里只有VSCode,环境没装对)

这两个问题加一起,那就是寸步难行啊,比写Android还难。

不用Unity了,用Godot。

直接三个数值赋值给欧拉角变换,完成!

是有那么一些地方不是那么如意。

控制鼠标指针

linux我不清楚,但是windows上面提供了控制鼠标的dll接口,这个dll叫user32.dll, 这里面都是C风格的导出函数,Go可以直接调用。

user32.dll中有个叫SetCursorPos的函数,传入两个整数,用于移动鼠标。

程序启动时会取得当前角度作为基准角度,根据与基准角度的偏差移动鼠标指针。

这其中最复杂的就是处理临界问题吧。

角度传感器的三个值的范围:
[0,180]
[-180,180]
[-90,90]

移动鼠标指针,我只用到前两个,第一个值需要处理一下临界,Go计算与基准差的代码:

func getDelta(x float32, y float32) (float32, float32) {
	absmin := func(a float32, b float32) float32 {
		abs := func(a float32) float32 {
			if a > 0.0 {
				return a
			}
			return -a
		}
		if abs(a) > abs(b) {
			return b
		}
		return a
	}
	return absmin(x-base_x-360.0, absmin(x-base_x+360.0, x-base_x)), y - base_y
}

之前用作3D,又加了平滑,看不出来抖动,但是在鼠标上这一点特别明显,用平均值滤波效果也不好。

在Go调用Dll函数时,要求传入uintptr的数据类型,Dll真正需要什么需要查文档,调用时需要把正确的类型转换成uintptr

func_SetCursorPos.Call(uintptr(x), uintptr(y))// 真就硬转

所以,做了这个有什么用呢?没用

关于安卓,写安卓程序真的一言难尽,要想一个办法避开直接写安卓程序呢,我觉得React native之后可以了解一下。

我能想到的,最大的成功就是无愧于自己的心。