众所周知,手机是可以做定位的。我个人比较喜欢做一些 "自我量化" 来追溯具体的时间都去哪里了,那通勤时间和办公室停留时间这种信息自然也很想记录下来。所以在很久之前我就开始各种搜罗,想要找一个方案。但很遗憾,找了一大圈,也没找到一个让我满意的,非常失望,这里记录下之前踩的坑,然后记录下目前找到的一个还可以的方案。
shortcuts 这个东西有点像是一个手机版本的 IFTTT。它包含一些触发器,可以自动触发某些事件,然后去串联多个应用以实现一些很 fancy 的功能。常见的触发器包括:
等等等,具体就不细说了。
不过很显然,我可以使用那个地理位置的触发器去记录我什么时候离开了哪里、什么时候到达了哪里。比如对于我通勤的记录,那我就设置两个关键地点:家和公司,然后记录离开到达时间就行了。具体就是这么个样子:
我是每天创建一个笔记本,然后把信息都追加到笔记本里了。
然后似乎大功告成了,可是很遗憾,实际上完全不生效,网上一搜很多相关问题,但就是没解决方案,我尝试了很多的方法都不成功。只有偶尔会成功那么一下下...我甚至咨询了在 Apple 工作的同学,最终也没有得到解决...
iOS 自带方案不行,就只好自己写 app 了,不过很遗憾我并没有移动端开发的经验,所以这个事情就搁置了...直到我一天发现了 owntracks 这个东西。
简单来说,owntracks 就是个不收集你个人信息,只负责将你的信息上报到你指定的服务器的一个工具。所以其实 owntracks 分为两个部分:
我最开始的需求很简单,就是周期性记录我的位置,然后周期性上报就好了,后续的数据处理交给服务端做好了。不过 OwnTracks 除了这个朴实的功能外,也有那种区域触发的功能。这样我就可以和 iOS 快捷指令一样关注几个地点就好了。
不过相比于 iOS 这边直接把数据记录到笔记本,我需要一个 track server 接受上报的信息。owntracks 支持两种协议:MQTT 和 HTTP,鉴于我只会 HTTP 就自己写了一个简单的 HTTP server 去接收这些信息了。然后在 app 这边配置下服务端信息就可以接收数据了。
注意这里要配置到具体的某一个路径的,比如 http://112.118.221.2/pub
而不是只给个域名或者 ip 地址就完事了。
为啥不用官方的?
OwnTracks 有官方的 track server ,但有这么几个问题:
- 代码是 c 的,自己部署有点晦涩难懂,只能用 docker 部署吧,但自己修改的可能性就很小了
- 同时支持 MQTT 和 HTTP 而实际上我觉得对个人来说 HTTP 会更简单一些吧,那 MQTT 部分的代码就很没必要了
- 有一些和 Google 地图集成的功能,这些东西在国内统统没得用
OwnTracks 会以 json 的格式把所有的信息都上报到服务器,提交的具体结构可以直接 print 出来看看。这里给出我的 server 的一个片段:
import json
from flask import Flask, request
FILENAME = "data.jsonl"
app = Flask(__name__)
@app.route("/")
def hello():
return "hello"
@app.route("/pub", methods=["POST"])
def pub():
# print raw request data
print(request.json)
with open(FILENAME, "ab") as f:
f.write(request.data)
f.write(b"\n")
return "done"
if __name__ == "__main__":
print("Starting server...")
app.run(host="0.0.0.0")
可以看到我就是把所有的数据到塞到了一个 data.jsonl
的文件里了,反正就是我自己的数据,没几条,这样就够了。
有了这些数据,我就可以把每天的通勤给按照时间顺序展示出来了,代码如下:
@app.route("/report")
def report():
data = []
with open(FILENAME, "r", encoding="utf-8") as f:
data = f.readlines()
data = [json.loads(item) for item in data]
data = [item for item in data if item.get("event") in ["leave", "enter"]]
chinaTz = pytz.timezone("Asia/Shanghai")
# datetime from unix timestamp
data = [
{
"desc": item["desc"],
"event": item["event"],
"date": datetime.fromtimestamp(item["tst"], chinaTz).strftime(
"%m/%d"
),
"time": datetime.fromtimestamp(item["tst"], chinaTz).strftime(
"%H:%M"
),
"datetime": datetime.fromtimestamp(item["tst"], chinaTz).strftime(
"%m/%d %H:%M"
),
"tst": item["tst"]
}
for item in data
]
# remove item with same time
masks = [False] * len(data)
for i in range(1, len(data)):
if abs(data[i]["tst"] - data[i - 1]["tst"]) < 120 and \
data[i]["desc"] == data[i - 1]["desc"]:
masks[i - 1] = True
masks[i] = True if data[i]["event"] != data[i - 1]["event"] else False
data = [item for i, item in enumerate(data) if not masks[i]]
# group by time
grouped = OrderedDict()
for item in data:
if item["date"] not in grouped:
grouped[item["date"]] = []
grouped[item["date"]].append(item)
result = [
[
date,
"\n".join(
[
f" {item['time']}\t{item['event']}\t{item['desc']}"
for item in items
]
),
]
for date, items in reversed(grouped.items())
]
return (
"\n".join([y for x in result for y in x]),
200,
{"Content-Type": "text/plain; charset=utf-8"},
)
最终展示效果大概就是这样子:
07/18
09:11 leave home
12:22 enter gym
13:41 leave gym
15:00 enter office
07/17
09:11 leave home
12:22 enter gym
13:41 leave gym
15:00 enter office
17:00 leave office
这个 server 的代码也放到了 owntracks-server。
很久之前就处理过这种坐标数据,还是有不少坑的。趁这次再次接触到也做一个记录。
经纬度坐标貌似并不是非常稳定,会有误差,你在同一个位置可能会有不同的坐标,这就会导致你可能动都没动但是突然你就离开了一个地方然后又回到了这个地方。
对于这种情况就只能做个简单的过滤,把那些时间戳非常接近的事件直接忽略掉。当然,这种过滤也可以顺便把你路过的那种地点一并清理掉。
出于一些特殊的考虑,不同的地图给的具体经纬度都是有区别的,就是说你所在的同一个位置对于不同的地图给你标记的经纬度是不同的。目前有三个主流的坐标系:
在我 iphone 上我的实际位置和地图标记的位置是有偏差的,我想这应该是苹果没有考虑到中国和国外使用的坐标系是不同的,也就是说 iOS 给我获取的经纬度应该是 WGS84 的,但实际展示却用了 GCJ02 的高德地图,所以展示的位置就不对了。我猜测这也很有可能是快捷指令无法触发的原因所在。
在目前的情况下,这个问题只是对 app 本身的展示有影响,对事件触发并没有影响。可以先不管。
data.jsonl
时间久了可能还是不太够,还是需要起码按照月份做个数据拆分最近在实验一个用 GraphQL 支持 REST 接口的方案,需要在 Java 里写大量的 graphql 的内容,如下所示。
@RestController
@AllArgsConstructor
class GraphQLTest {
private DgsQueryExecutor dgsQueryExecutor;
@GetMapping("/g")
public ResponseEntity<?> test() {
String query =
"query {\n" +
" plans(category: [PERMANENT_COMPUTATION]) {\n" +
" id\n" +
" ... on TimeBoxedComputationPlan {\n" +
" resource {\n" +
" name\n" +
" gpu {\n" +
" ... on PhysicalGPU {\n" +
" type\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
ExecutionResult result = dgsQueryExecutor.execute(query);
return ResponseEntity.ok(result.getData());
}
}
可以看到这种多行拼接的 string 非常丑陋,并且没办法使用 intellij 的 inject language
这样的功能。
不过在 java 15 就已经引入了 text blocks 的语法了,并且在目前的情况下,引入这个语法确实会为后续开发提供不少便利。于是决定就直接从目前的 java 11 升级到最近的 LTS 版本 java 17 了。这里就记录下更新版本需要处理的问题。
去 azul 官网安装对应的操作系统的版本。
安装后在命令行输入命令确认安装成功了:
$ java --version
openjdk 17.0.3 2022-04-19 LTS
OpenJDK Runtime Environment Zulu17.34+19-CA (build 17.0.3+7-LTS)
OpenJDK 64-Bit Server VM Zulu17.34+19-CA (build 17.0.3+7-LTS, mixed mode, sharing)
打开 idea intellij 对配置做如下修改。
java 17 相对比较新,有些 gradle 的插件没有做很好的适配,需要做一些修改。我这里遇到了两个插件的问题,这里也记录下。
spotless 是做代码格式化的,里面用了 googleJavaFormat 似乎有一些兼容性问题。这里没有花太多时间做调研,只是跟着 issue 做了调整。
根据这个 PR 将 gradle.properties
修改如下:
org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
build.gradle
里更新 google java format 到最新版本spotless {
java {
target project.fileTree(project.rootDir) {
include '**/*.java'
exclude 'build/generated/**/*.*', 'build/generated-examples/**/*.*'
}
toggleOffOn('@formatter:off', '@formatter:on')
+ googleJavaFormat('1.15.0')
}
}
按照 https://github.com/n0mer/gradle-git-properties/issues/171#issuecomment-817569604 对 gradle.properties
做修改:
org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
+ --add-opens java.base/java.io=ALL-UNNAMED
做了如上更新后,本地跑 java 的项目就不报错了,不过还是有一些 warning 后续慢慢处理。
text blocks 就能用了,效果如下:
又发现了集群里出现了挂掉的 nvidia gpu 这里记录下如何屏蔽掉它以保证其他 GPU 可以继续被使用。
首先是监控告警,告知 nvidia-smi
命令出错了,去机器上看一下有这么个错误:
$ nvidia-smi
Unable to determine the device handle for GPU 0000:89:00.0: Unknown Error
感觉是这块卡 0000:89:00.0
出问题了。然后去执行下 dmesg
看看情况:
$ dmesg -T
[Mon May 9 20:37:33 2022] xhci_hcd 0000:89:00.2: PCI post-resume error -19!
[Mon May 9 20:37:33 2022] xhci_hcd 0000:89:00.2: HC died; cleaning up
[Mon May 9 20:37:34 2022] nvidia-gpu 0000:89:00.3: i2c timeout error ffffffff
[Mon May 9 20:37:34 2022] ucsi_ccg 6-0008: i2c_transfer failed -110
看不懂,搜了搜也有点懵逼。这台机器已经运行了挺久了,驱动也在短期内没有出过问题,那么就感觉是硬件出问题了。重启机器后,nvidia-smi
恢复了。不过如果有其他任务使用了 GPU 就又会出现这个问题了。所以考虑使用 nvidia-smi
的命令先屏蔽掉这块报错的 GPU 。
$ nvidia-smi drain -p 0000:89:00.0 -m 1
Successfully set GPU 00000000:89:00.0 drain state to: draining.
然后再执行命令 nvidia-smi
就看不到这块卡了。