0%

背景与动机

为什么要做个人数字资产管理系统?

使用比特存储的这些信息一定程度上组成了每个现代人的一部分,信息的无序,遗失,损坏无疑都会给我们的生活带来一定的不便,甚至间接造成时间,经济和精神上的损失。
说这话是因为,前阵子坏了两块硬盘,一块用了7年的WD MyCloud,另外两块是新买的两块 Seagate 酷狼。好在WD上的数据有备份,希捷附送的数据恢复服务则免费找回了数据,还附赠了两块移动硬盘。

个人数字资产包括哪些部分

一切使用比特存储的数据均可能是数字资产的一部分,种类繁多,几乎无法特别完备的归类,对大部分人来说,数字资产可以包括:

  • 包含个人回忆的多媒体资料,主要是照片和音视频
  • 工作或生活项目中的文档,包括代码,素材,工程文件等。
  • 个人的知识库和材料库,包括笔记,收藏,论文,音视频素材等等
  • AppData,和某些数字应用强关联的数据,如微信朋友圈,聊天记录,大众点评收藏等
  • 数字凭证,包括网络账号的账户密码,一些物理证件的电子版(如身份证照片)等

一个好的个人数字资产管理系统如何评价

我目前想可以大致分为四个方面去评价:

  • 便利性:数字资料应该容易被索引和找到,有一个比较清晰的组织逻辑,不应该散落四处,获取数据的速度不应该太慢(索引速度和数据传输速度之和)。
  • 安全性:应该对因为意外(设备损坏丢失,天灾人祸等)导致数据丢失的情况进行预防和备案。
  • 隐私性:和个人隐私相关的信息应当尽量避免被第三方获取利用
  • 成本:系统的搭建和维护成本不应该太高

系统设计

我个人的笔记使用Obsidian加 iCloud云同步管理,重要密码凭证交给 1Password,这里的系统主要指大部分非AppData的文件。

基本原则

设计1.0系统时,基本原则如下:

  1. 避免数据单点丢失:对于不进行云备份,以及云服务提供商不具有很好资质的数据,保证至少在两点以上存储备份。对于iCloud,阿里云,Git这一类的云服务,选择信任服务商。
  2. 个人照视频以及多媒体资料不上云。个人照视频有较多隐私问题,多媒体资料可能具有一定版权风险。
  3. 数据应当进行定时的归档和冷热分区,保证低频率访问的数据使用更低的存储成本以及更小的访问带宽

系统设计


系统简述

  1. 系统中大部分文档使用PARA原则管理,大部分PARA文档可以在隐私和保密等级上应该比较低,允许使用云存储。
  2. 隐私和保密性要求较高的使用PRIVATE存储,不使用PARA单独管理,避免文件散落,而是在主PARA区域中建立数据索引访问。PRIVATE中文件夹数量不宜太多
  3. 对于热PARA数据,数据在云端和NAS上进行双向同步储存。
  4. 对于热Private数据,数据在外接大硬盘HDD和NAS之间同步。
  5. NAS中设立通用备份区域,用于进行设备的自动定时备份,其中数据只在设备故障时进行恢复使用,并不断覆盖更新。设备中零散的文件应该定期整理备份到PARA或PRIVATE目录下
  6. 对于比较旧的极少访问的数据,挪动到 NAS 的 Archive Warm Zone,这个区域的盘会和连接在 NAS 上的一个USB盘做双盘同步备份,释放HotZone 空间以及云端存储
  7. 当 Warm Zone 充满时,将 Warm Zone 的存储硬盘和外接的USB硬盘进行归档存储。HDD挪至一个仅进行日常硬盘健康检查的 NAS上,外接USB硬盘则冷存储在避光干燥的容器中。

实践操作

目前自动同步和备份功能均基于群晖NAS提供的功能完成:

  • USB大容量磁盘到NAS的自动同步使用 群晖的 Synology Drive 软件完成
  • 云端同步使用群晖套件中的CloudSync,在Aliyun存储自己申请了一个oss bucket,并且购买了个 9 元 40GB 一年的存储包。
  • WarmArchive 到 Cold Zone Copy 使用群晖的USB Copy 套件做计划任务,每天半夜自动拷贝,并且使用增量同步

其他问题

  • 由于自己非常爱拍视频,又不爱整理,长期带来了很大存储和整理负担,因此有必要整理一套影音管理流程,比如优化拷贝素材和进行数据压缩的流程
  • 鉴于Aliyun Archive 类型的存储成本其实很低,部分Non-Sensitive archive 可以考虑迁移到阿里云上
  • 当Archive内容多了,硬盘数量扩增以后,如何建立一个中心化的索引机制,管理物理硬盘和存储内容

PS. 在整理旧的数据上几乎耗费了一整个周末,虽然很多可能没什么用的数据还是不舍得删,还是学不会断舍离啊。不过换个角度,也许这些看似没有的数据能让我未来数字生命的生成要更丰满些呢嘿嘿。

Shell脚本一直被我视为上古的黑科技,从来都是是敬而远之。然后最近工作中遇到一个进程无法按照预期退出的问题,着实让我产生了困扰,不得不静下心来再补习一些基础知识。
问题是这样的:我的一个Python程序,在运行过程中,使用 Subprocess 启动了一个 Shell 脚本进程,并且获取到了进程的 pid。并且在适当的时候,我需要给进程发送 SIGINT 信号,触发进程正常退出。将这个Shell脚本简化,并且以 ping 程序代替实际调用的可执行程序之后,大概如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash

function onSigint() {
echo "sigint captured"
}

function go() {
trap "onSigint" SIGINT

ping 127.0.0.1 &
pid=$!

echo "! Waiting for ping process to exit"
wait $pid
echo "! Ping process exited or shell interrupted"

if ps -p $pid >/dev/null; then
echo "! process ${pid} still alive, try kill it"
kill -2 $pid
fi
wait $pid
echo "! Ping process exited or shell interrupted"
}

go | tee -i /tmp/test_log.txt

这个脚本我们暂时取名为test.sh,我遇到的问题就是,这个程序在Terminal运行的过程中,Ctrl + C 可以触发进程正常的退出,但是我的Python程序中,给我启动的子进程发 SIGINT却没有任何反应。为了解释其中的原理,需要了解一些容易被忽略的基础原理。

1. 当你按 Ctrl + C 时,究竟发生了什么

在一般的认知中,Ctrl + C 等同于给终端的前台进程发送 SIGINT 信号,但是精确来说,其实是给前台进程组(Foreground Process Group) 中的所有进程发送 SIGINT 信号 。而前台进程组可以理解为在当前Termnial可以接受你键盘交互的进程,以及由它启动的所有子进程。

下面这张图很好的解释了前台进程组以及一些额外的概念(Session,Session Leader),图片出自Killing a process and all of its descendants

通过ps j -A可以很清楚的看到每个进程的PID,PPID(父进程ID),PGID(进程组ID),SID(Session ID),在这里,我们在一个终端运行 bash ./test.sh 之后,看一下进程信息:

1
2
3
4
5
6
$ ps -j A
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2596 2668 2668 2596 pts/1 2668 S+ 0 0:00 bash ./test.sh
2668 2669 2668 2596 pts/1 2668 S+ 0 0:00 bash ./test.sh
2668 2670 2668 2596 pts/1 2668 S+ 0 0:00 tee -i /tmp/test_log.txt
2669 2671 2668 2596 pts/1 2668 S+ 0 0:00 ping 127.0.0.1

可以看出这一个脚本启动了四个进程,至于为什么是四个进程,而且其中包含两个同名进程之后再说。这里面 STAT 一列中的 “+” 号表示该进程属于前台进程组,TPGID则是前台进程组的 ID。可以看出,虽然我们在脚本中把 ping 程序 通过 “&” 符号放到了后台,但它依然属于前台进程组。在键盘按下 Ctrl + C 之后,上面所有的进程均会收到 SIGINT 信号。

2. Bash进程接收到 SIGINT 信号时,会怎样?

这篇文章Why Bash is like that: Signal propagation,给出了解释

When the shell is interrupted, it will wait for the running command to exit. If this child’s status indicates it exited abnormally due to that signal, the shell cleans up, removes its signal handler, and kills itself again to trigger the OS default action (abnormal exit). Alternatively, it runs the script’s signal handler as set with trap, and continues.

If the shell is interrupted and the child’s status says it exited normally, then Bash assumes the child handled the signal and did something useful, so it continues executing. Ping and top both trap SIGINT and exit normally, which is why Ctrl-C doesn’t kill the loop when calling them.

总结下来,Bash进程对于SIGINT信号的处理有几点特性:

  1. Bash进程一定会等待当前程序运行结束后,再处理 SIGINT 信号。如果当前程序返回值为1,则直接退出脚本。如果返回值为 0,则继续执行脚本。
  2. 如果Bash脚本中使用了 trap 命令,捕捉了信号,那么Bash会在当前程序运行结束之后,先运行trap指令中指定的函数指令,然后再继续运行接下来的指令,无论当前程序返回值是什么。
  3. 如果当前Bash脚本执行的是 wait 命令,该命令会被直接打断,此时如果没有设置信号捕捉的话,脚本会直接退出。(wait可以理解为Bash程序的内部指令,可以被打断)

由于Bash的这个特性,会产生一些看起来很奇怪的现象。比如说在一个循环中调用 sleep 命令。通过Ctrl + C 可以直接退出脚本,而如果循环中调用的是 ping 命令,则 Ctrl + C 会让脚本进入下一个循环。这是因为按下 Ctrl + C 以后,ping 和 sleep 都会接收到 SIGINT 信号,并立即退出,但 sleep 的返回值不为0,而ping的返回值为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ sleep 10
^C
$ echo $?
130
$ ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.018 ms
^C
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.018/0.018/0.018/0.000 ms
$ echo $?
0

回到我们上面的 test.sh 脚本,其中对信号进行了捕捉,并且使用了两次wait,在两次wait中间还再次给目标程序发送信号。那么如果在脚本运行过程中按下 Ctrl + C,则会发生:

  • ping程序退出
  • wait被打断,触发onSigint函数,然后继续执行接下来的逻辑
  • 由于ping程序已经退出,找不到对应pid,不会再次执行 kill 命令
  • 由于ping程序已经退出,第二个wait也直接完成,脚本结束

如果不是Ctrl + C,而是只给执行shell脚本的bash进程发送 SIGINT,则会发生:

  • wait 被打断,触发 onSigint函数,然后继续执行接下来的逻辑
  • 由于 ping 程序依然活着,if判断下面的 kill 命令会被执行,给 ping 进程发送SIGINT 信号
  • 第二个 wait 在 ping 程序退出之后结束,脚本结束

所以按照test.sh的写法,似乎只给 Bash进程发 SIGINT 信号,也是可以实现正常退出的,那么为什么在我最初提到的问题中,会失败呢。这与脚本最后使用的管道命令有关

3. 在Bash脚本使用函数加管道会发生什么

上面第一点提到了存在两个同名进程的问题,我们使用ptree -p 指令看一下进程之间的层级关系:

1
2
3
4
5

$ ptree -p

systemd(1)─sshd(668)─┬─sshd(2576)───bash(2596)───bash(2668)─┬─bash(2669)───ping(2671)
└─tee(2670)

可以看到由于go | tee -i /tmp/test_log.txt 的存在,bash 额外启动了一个子线程(可能是直接fork的方式,进程名完全相同)进行函数内 Shell脚本的执行,并将管道接入 tee 的子线程中。此时执行脚本的顶层 Bash 进程实际上没有 Trap 任何 信号,只会按照之前提到的默认规则进行响应,即等待当前命令执行完毕。

而我通过Python Subprocess 调用 test.sh 拿到的,正是顶层 Bash 进程的 PID,对它发送SIGINT信号,自然也得不到任何响应。

解决问题

大部分网上的问题解答会提供这样的方法,即通过 在 pid前面加 ‘-‘ 减号,来达到给整个进程组发送信号的目的。(其中的 “–” 两个短横线是告诉shell不要把后面的 “-“ 误认为参数标志)

1
kill -s SIGINT -- -$gpid

但在我的问题中,这样做有点棘手,因为 shell 脚本是在 python 程序的子线程里执行的,事实上,bash进程和python进程属于同一个进程组。我当然不想让 python 进程也收到信号。

我最后解决问题的方法很简单,不过也非常局限于我当前的问题,即通过 pkill 发送信号给我启动的bash test.sh进程的直接子线程。

1
2
3
4
5
6
7
8
# start bash process
p = subprocess.Popen(["bash", "test.sh"])

# send SIGINT to direct children of the bash process
subprocess.call("pkill --signal SIGINT -P {}".format(p.pid), shell=True)

# wait process exit
p.communicate()

要注意的是,此处在python中启动 test.sh 必须用列表命令的方式,而不能用字符串加参数shell=True,否则 python 又会再起一个 线程作为bash test.sh 的父线程,这样我们拿到的 pid 的会又多了一层进程层级关系,这个方法又不work了。

其实,改动./test.sh让它能够独立妥善处理 SIGINT 才是最优雅和最不容易埋坑的做法。

额外提一点,关于孤儿进程

问:如果父进程退出了,而子进程还正常运行会怎么样呢?
答:被所有进程之母收养(systemd 或 init)

参考4. Killing a process and all of its descendants

参考资料

  1. Why Bash is like that: Signal propagation
  2. Why is SIGINT not propagated to child process when sent to its parent process?
  3. SIGINT Propagation Between Parent and Child Processes
  4. Killing a process and all of its descendants
  5. Process groups and sessions

事情的起因是这样子的,10月1日上午,我又一次在出行前收拾东西到最后一刻,并且成功的错过了火车,并且还发现自己背包里仍然忘了几样东西。在改签到晚上的车票之后,我平白在火车站多出了半天思考人生的时间。于是我痛定思痛,又一次萌发了写一个小程序,帮助自己解决出行前打包东西的困难。在我想象中这个程序应该大概是这样的:

  1. 启动程序,程序会问你一系列的问题,如出行几天,开车为主还是步行为主,飞机行李额多少等,需要用户回答这些问题
  2. 根据问题的答案,自动生成一份打包清单

我网上搜索了一番,想看看有没有人已经设计过类似的程序,然后我搜到了这个
Knapsack problem algorithms for my real-life carry-on knapsack。这篇文章从生活中的故事出发,循序渐进,并且画了很多生动的图,不过实际上还是介绍了经典的0-1背包问题的解法,对我的“出行清单生成系统”没什么帮助,这个系统以后另说,先看看这个算法。

解决0-1背包问题的关键还是能够有意识的构建如下的状态转移方程:

dp(i,w)=max{dp(i1,w),dp(i1,wwi)+vi}

函数dp(i,w)的含义为在可以放前 i 个物品,并限制重量为 w 时,能够获取的最大价值。而这一函数是存在递推关系的,从状态与决策的角度来讲,在处理第 i 个物品的时候,我可以选择 拿 或 不拿,如果 拿 的话,这一步决策带来的价值,等于 上一步决策(i1)w 减去我当前物品重量的限制条件 下的最优价值加上本物品的价值,如果 不拿 的话,这一步决策带来的价值,则完全等同于上一步决策 (i1)w限制条件下 带来的价值。对于这一步决策,只要取最大值就是这一步决策的最优选择。

上面这段话有些绕,而且这里面似乎涉及到一些更理论的知识(如多阶段决策的最优化原理),这里我也是似懂非懂。总之一旦有了这个递推公式,问题就好解了,一般就两种解法:

  1. 正着解,从上往下解,层层递归,到边界条件上再层层返回结果,值得注意的是,递归方法的一个问题是在不同的递归分支里可能会对同一状态访问多次,造成重复计算,这里一般通过构造一个记忆内存去存储已经计算过的结果。
  2. 反着解,从边界条件开始(这里是 i = 0,第一个物件),层层往上推。

我看的文章基本上都是讲的第二种解法,通过构造 dp 矩阵(行为物品,列为重量),一层层往上推。然后就是根据递推公式中dp[i,j] 只与上一行(i - 1)中列数小于或等于 j 的元素有关 的特性,使用滚动数组的方法,使空间使用从 dp 矩阵[N * W] 降为 dp 数组 [W]。很多文章中之所以提到要“逆序”更新,也是和刚刚说的特性相关。因为如果正序更新,无法保证后面的 w 更新的时候,所依赖的值,仍旧是上一行的值。

我没怎么刷过题,LeetCode做过的题也基本还处在easy阶段,以前对动态规划也只有耳闻,在火车站又查了很多关于动态规划和背包问题的文章,并且自己动手写了一遍才感觉理解到位。算法的练习放在了
我的Github 法练习仓库/dynamic-programming。其中我自己觉得,似乎还是递归方法更为实用和优雅一些,因为构建dp矩阵,从边界条件开始往上推似乎存在着以下问题:

  1. 这样做似乎穷尽了决策状态,是否有必要?
  2. 当背包载重很大或物品质量差距大时,dp矩阵的列会变得非常庞大

还有就是大部分讲背包问题的文章,都只是求解出最优的价值,但没有涉及到如何把最优的打包结果里包含的物品清单输出来。上面的英文文章介绍了dp矩阵的方法中如何输出打包清单。另外我自己在递归的方法中也加入了打包清单的输出,思路有点类似于在通过图搜索进行路径规划的过程中,找到目标点后反向生成路径的方法。

具体的实现过程中,我把每一个 dp(i,w) 视为一个状态节点,在每一次计算得到节点价值的过程中,同时登记当前选择的结果(放/不放),和对应的上一个状态节点是谁。最后,再从 dp(N, W)开始一路倒推,得到整个最优决策树上的每一步选择。

背包问题的变种似乎还有很多,未来遇到时再继续学习。

以下文章是我在申请到小米CyberDog体验之后在小米社区交出的体验报告。

CyberDog工程探索版众测 - 在初次遛狗的日子里对未来有新的期待

CyberDog在小米发布会上作为“One More Thing” 亮相之后,当时即圈粉无数。作为一个机器人行业从业者,对四足机器人并不感到十分新奇。但是CyberDog完成度极高的工业设计,几乎没有妥协的硬件平台,相对业界如此低廉的价格,和致力构建开源社区的态度,还是让我感到非常的激动。于是我抱着试一试的态度提交了申请资料,然后在埋头工作的某一天,被电话通知自己成为了首批CyberDog众测的体验用户。

1. Lucky dog meet CyberDog - 初见铁蛋

Lucky Dog 是我,因为抽中CyberDog是我久违的觉得幸运和喜悦的时刻。从下单那刻起,我不停的刷新着物流进度,期待着狗蛋的到来。而我对于机器人开箱的幻想,一直来源于一部电影《机器管家》,机器人Andrew能自己睁开眼睛,并且从包装盒里走出来。

CyberDog并不能自己走出包装,但小米为其定制的拉杆箱和狗骨头的纪念牌让人惊喜,CyberDog漆面的光泽也让人赏心悦目。后来的体验中发现买狗送拉杆箱这个设定,其实是非常实用且必须的。

铁蛋的开箱和试用被同事强力围观,先是让他被迫表演了传统的艺能,跳个舞,撒个欢儿,然后就是行走稳定性的测试,同事调皮的给铁蛋创造各种各样的阻碍,包括行走时推动它,和故意用障碍物拌他的脚。铁蛋除了有一次直接被推趴在地上,大部分时候都能顽强的调整身体姿态,努力保持自己不摔倒。对我来说,铁蛋在干扰下努力保持身体平衡的样子,是最吸引我的,也是让我觉得“机器人最具有生命力”的时刻。

总的来说,CyberDog的运动稳定性尚可,但是步态的优雅性,以及爬上楼梯或障碍物的能力,离业界顶峰波士顿动力还是有很大差距。手机App中除了遥控之外,还提供了一些智能化的功能,如跟随,避障,建图,导航。但是体验下来,这些功能都只提供了一个非常初级的版本,只能做非常简单的演示,对于现实场景中复杂环境的适应性还远远不够。

开箱过程中其他的一些意见和建议包括:

  • 纸质包装箱的塑料把手承载力不够,塑料把手容易和纸箱脱离
  • 箱子内的泡沫支撑比较实用,但是稍显粗糙
  • CyberDog的骨头名牌上,如果能印上米粉ID的话,或许能给到米粉额外的惊喜。
  • 狗的下巴似乎没有加固保护,考虑到那个位置有非常重要的传感器,还是存在一定风险的

2. Let’s Go Farm - 让我们去农场溜溜

当仿生机器人闯入大自然,碰见真实的生物,会产生怎样有趣的反应?带着这个好奇,我们策划了一次农场之行,目的地是浙江省湖州市的一个名叫 GoFarm 的农场体验俱乐部。去体验之前,我先对狗进行了简单的改装,打印了一个简单的结构转接件,来支持狗背上 1/4 螺纹接口设备的挂载(如运动相机,激光雷达,投影仪等)。CyberDog为了整体工业设计的完成度,并没有像别家狗一样提供一个大平背和众多的装载装置用于功能扩展,这一定程度上给开发者带来了一些麻烦。

带狗蛋出门拉杆箱基本上是必备的,CyberDog的续航大概在 40 分钟左右(包含较多的运动),没电的时候铁蛋就化身成将近30斤的铁疙瘩,此外拉杆箱内减震材料可以在狗蛋乘车和搬运过程中提供足够的保护,而狗蛋的拉杆箱基本能将一般家用车的行李箱占据一半以上。

带着铁蛋我们驱车到达农场门口,工作人员表示真狗不允许入场,但是机器狗并没有规定,于是我们开箱放狗,铁蛋似乎闻到了自由的气息。在草坪上奔跑的铁蛋,看起来要比在水泥地上舒服的多。

农场中开放区域四处走着悠闲的羊群,铁蛋的闯入似乎让它们有一些警觉,本来四散的羊群主动聚拢起来,并且和铁蛋展开了对峙。铁蛋为了主动打破僵局,主动往前踏出两步,羊群立刻转身逃窜。看来和羊交朋友是不行了,或许找份牧羊犬的工作倒也不错。铁蛋牧羊的完整视频可以戳这里看 牧羊犬铁蛋

人类的幼崽则完全不害怕铁蛋,而是面对铁蛋表现出了浓厚的好奇,有的试图去摸铁蛋,和铁蛋打招呼,还有的挥舞拳头吓唬铁蛋。铁蛋被围住的时候我基本不太敢操作,毕竟铁蛋皮儿太硬了,真的磕一下小朋友还是很疼的。

铁蛋穿过草地,来到了柯基的园区,围栏里的柯基远远的看到铁蛋了,突然都跑到围栏边上,对着铁蛋群起而吠之,吓的铁蛋一动不敢动。于是我们也没敢让铁蛋进围栏和柯基亲密照面。不过后来听饲养员姐姐说,把铁蛋放进来柯基肯定就怂了,狗子们就是隔着围栏才那么有气势。当前的铁蛋还是让大多数动物感到有些警觉的(除了一只野鸡完全对他爱答不理),特别是动起来时快速嗒嗒嗒的步伐很容易吓到小动物。不知道是否在四足生物之间,也存在类似的恐怖谷效应?

3. What’s Next ? - 关于未来

在申请CyberDog的问卷中,有一个问题是说说你对仿生机器人的看法,我是这样回答的:

“从实用价值的角度来说,现代工业社会是建立在各种高效率,高专业度的人造装置之上的,这些人造装置牺牲了适应性,换取了在单一任务上的极高效率。而仿生机器人,更多的是代表了人类对自己,对世界,对造物主的好奇心和探索欲。”

所以四足仿生机器人究竟有什么用?一个确定的答案是四足的特点的确让其对复杂的路况有着更好的适应性,因此他可以代替人类去探索危险,复杂,未知的区域,也许不远的将来就会有机器狗登上火星。

另外一个不确定的答案是,四足仿生机器人能否对普通人进行陪伴甚至进行日常的协助。在带铁蛋出门的这次旅行中,我从铁蛋的视角的录像里捕捉到了很多有意思的镜头,我想他也的确可以帮我驼东西,可以从一个第三者视角帮我记录下一些我日常生活中的片段。

然而,当前即使装备了最昂贵和精密的传感器和计算单元,运行了世界上最先进算法的机器人,其表现出的智能程度和能给人带来的亲和感可能依然不如一只1岁大的小狗。自然生物的那种对外界环境风吹草动入微的体察,和做出的大致相同却又充满着无穷细节变化的反应,以及随着经历和记忆不断变化的行为特点。这样的表现,在我认知范围内,想要通过编写程序去实现似乎是一项不可能完成的任务。

《机器管家》中,Andrew刚刚到达主人家时,家里两个小女孩对Andrew的第一印象分别是“可怕” 和 “蠢”。直到今天,大部分人对于仿生机器人的看法也大抵如此。在这个“人工智能”成为热点的时代里,这些机器人更多的被戏称为“人工智障”。

我曾跟朋友调侃过,作为机器人行业从业者,使命之一就是“让天下不再有智障机器人”。小米通过CyberDog提供了一个价格较为低廉,配置丝毫不拉垮的机器人硬件平台,同时试图打造活跃的机器人开发者社区生态。目的就是想让更多对机器人有想法,有期待的开发者加入到机器人开发的行列,共同推动机器人技术的发展。大家都希望有一天,人们对机器人的印象不再是“可怕”和“蠢”,而是“可爱”和“机灵”。

而我们离这一天,也许并没有想象的那么远。

这个月初动手拆了一个GoPro8,并且改造成了裸狗(FPV圈里的黑话,指的是不带电池和后屏幕的GoPro),挂在我的Cinelog25小飞机上。飞机挂GoPro是为了更好的视频画质,但是极大的增加了炸机成本,算下来到今天我FPV入坑差不多3个月,感觉平飞已经可以驾驭住了,于是忍不住要搞一个裸狗玩玩。

格普(GEPRC)家刚好前一阵子推出了裸狗8的产品,挂在淘宝店上可以直接购买,就是价格不是很美丽,改造好的裸狗8直接卖到了 2800 元。而某鱼上成色一般的二手GoPro8价格差不多在 1500~1600,加上格普家的BEC和外壳改造套件卖 270 元,直接成本差不多1800元。而自己改造的话,主要要考虑:

  1. 学习,拆机,折腾的时间成本
  2. 一旦拆机过程中不小心损坏GoPro,直接经济损失还是挺疼的

在网络上大致搜索浏览了一遍拆机和改造教程之后,我心理大致对GoPro8的构造有了一些数。权衡了成本之后心理天平倾向于多花钱,少折腾。但是我发现在油管上有两个还比较详细的介绍拆解改造过程的视频,但是在B站上居然一个视频也没有找到,于是我萌生了自己折腾一遍,并且把过程拍成视频的想法,毕竟如果是中文首发的视频,对圈里和想入坑的爱好者们还是有些意义的。

以下就是我最终发出来的改造视频:

关于本次改造的一些总结

  1. 手工活讲究用对的工具,用巧劲儿不用蛮劲,才能事半功倍。我在拆机中间一度着急用力,结果把自己手扎破了。
  2. 发生糟糕的事情还能保持心态是一种能力。我在拆机过程中拆碎液晶的时候心态一度有些小崩,好在快速调整了心态,接着把所有活儿搞定了,并且也没有中断视频录制,后来发现这一次失误的代价不过是某鱼50块钱的事。

关于视频

这次GoPro拆解的视频剪辑我原本给自己计划的时间是 3 个小时,最后实际上大约花了 9 个小时。对自己做事情的时间估计又一次的出现了大偏差。主要预期外的时间花在了制作字幕,补拍个别镜头,寻找和转换背景音乐,制作封面上。这次算是第一次比较用心的制作视频,制作的过程中我也不断提醒自己不要完美主义,每次制作视频只要有一个方面有所进步,或有一个新鲜的IDEA就足矣。平时如果没有足够的积累,还强迫性的完美主义要出结果,最后很可能是白白消耗大量的时间和精力。

关于对这个视频最好的预期,就是被希望自己动手改造裸狗的爱好者收藏,当作有效的拆机指导。

遗留问题

这次改造除了拆前液晶屏幕的时候碎了之外,还有一些不太符合预期的情况,虽然不影响现在的使用,我还是把他们记录下来:

  1. 刚改造完,前几次通过电池启动GoPro的时候,手机还可以搜索到GoPro,并且可以通过手机调整参数,控制开关(GoPro应该是蓝牙常开的,通过蓝牙可以再控制开启Wifi,达到手机预览画面的效果),但是再后来就再也无法通过手机搜索到GoPro了
  2. 格普家的BEC板子,似乎延伸出来的几个按钮(模式/开关键,录像键)都不能正常工作,我尝试将BEC板子的电源档位拨至非自动开机档位,结果无法通过手动按开关键开机,并且也从未通过手动按录像键成功启动过录像。我不知道这跟我改造的过程有没有关系。

坐长长的地铁回家路上,恰巧听到了重轻 - 不在场的播客S2E2 平凡故事。这期是关于李剑青的《仍是异乡人》这张专辑,播客开头《平凡故事》的音乐一响起,就一下子把我带回了毕业前夕,坐在往返学校与老师公司的大巴车上,塞着耳机不断循环这张专辑的那个夏日里。音乐神奇的一点是在某一个时间段反复听的歌,仿佛能够在当时的时空里建起一扇传送门,或是定格了那时的情绪留下一张快照。可惜的是工作以来似乎音乐越来越被当作降噪和助眠来用,越来越少去用心的听一首歌了。

第一次知道李剑青是在某年的一个新年Apple广告中,传统的过年歌(每条大街小巷)被李宗盛和他的朋友们(其中有李剑青,后来从播客里知道他们是师徒关系)改编和演绎,熟悉的曲调下突然出现了一段从未听过的婉转而略带忧伤的旋律,一下子把我抓住了。

你的思念,是久治不愈顽疾。你的乡音,如母亲给的胎记。归来吧游子,功名沉浮不必提,稚志初衷,别忘记
– 李剑青《在家乡》

之前听的时候,一直以为最后一句是,“只是初衷别忘记”,今天仔细看歌词时才知道是”稚志初衷“,我想大概是在别人的眼里或者在多年后往回看的自己的眼里,年少的志向大都是不成熟(幼稚)的吧。

专辑里我还很喜欢的一首歌是《姥姥》:

这三十二年来,你在我身体里走路,咳嗽,歇息,直到今天和明天,所有的日子里。

这首歌感觉结构很新奇,内容富有变化,包括背景的和声哼唱和老年人的念白,还融入了一些京剧的调调,总之我很喜欢。

这张专辑的主题是“乡愁”,如今的乡愁已经无关地理的距离,在大城市里打拼多年的年轻人,在大城市里找不到归属感,而回到家中和亲戚,邻里,街坊似乎又已经产生无形的心理隔阂,那他们的乡愁又应该安放在哪里?

我在对自己2020年的年度计划中,其中有一项是“从统计收入和支出做起,开始做财务规划“,到年末总结的时候发现我对这项目标付出的行动仅仅是记了两个月不到的帐。在目标未达成的愧疚感的督促下,我在2021年的一个周日的白天,努力思考记账于我的意义,并且在网上搜索,看大家都是如何记账,都使用了什么样的方法和工具。最终我选择了一个看起来有些“复古”的记账方式(纯文本复式记账),并开始使用beancount这一开源软件。没想到这一用,不知不觉已经过了半年,而我在 2021-01-03 到 2021-08-13 期间的所有财务事件(收入,支出,基金交易),都被完整的记录了下来。

我本身认为自己是一个注意力很容易转移,很难持续的坚持一件事情的人,我甚至从小到大几乎没有从头到尾写完过一本日记本或笔记本。而记账这件事情能坚持半年,我已经想给自己鼓掌了。

为什么要记账

我从小缺乏财务方面的教育,也从未对金融或经济产生过兴趣。对我来说,了解金融知识和进行财务计算,是无聊和让人头大的。我总觉得自己只要全心投入到自己喜欢做的事情上,以我个人的资质和努力程度,在经济状况方面,应该不用担忧。于是我在各种人生选择上都懒于考虑财务上的损耗与收益,比如买东西的时候不讨价还价,博士毕业找工作的时候懒得去解读各地补贴政策,不关心五险一金是什么东西,所有工资静静的躺在银行活期。

而时间像变戏法一样流逝,逐渐感到自己的精力似乎没有以前充沛,而财务和能力积累却不远如预期,关于未来的可能性一点点在无情的缩减,身边的同龄人经济水平,生活状态已经开始明显分化。我最近也更切实的体会到”你不理财,财不理你“这句老话的真谛,我觉得记账于我,主要有下面的几点意义。

  1. 无论你愿不愿意,你都是商业社会当中的一个玩家,逃避这个事实只会让自己被席卷和收割,我不求兴风作浪的本事,只求防身之能。通过记账,可以保持对自己财务状态的了解,并更真实的体会到人生的各种选择和事件对自己的财务状况带来的影响。
  2. 有句话说金融就是“跨越时间的价值交换”。通过记录自己在时间轴上的影响财务状况的动作,有助于从更长的时间维度上审视自己,从而更加了解自己,尽量避免重蹈覆辙。

记账软件的选择之路

之前还在上学的时候就陆陆续续使用过一些记账软件,其中使用最多的是随手记和网易有钱两款App,随手记是当时我觉得记账交互体验比较好的一款软件,但始终难以坚持下来,究其原因,我觉得主要是记账动作太琐碎,而且一旦产生漏记或对不上的账目(哪怕只有几块钱),记账热情都会大打折扣。而网易有钱,当时是我一度看好的记账软件,得益于国内移动支付的普及度,所有交易都在网上有记录可查,当时有钱App可以同步微信,支付宝,银行卡账单,并且根据账目内容自动进行分类(如餐饮,购物等)。无奈金融这件事情过于敏感,各方不可能给有钱App开通API,因此同步要求用户输入账户的用户名密码,然后通过模拟用户请求完成同步。这就带来了很大的安全隐患,也侵犯到了别的金融服务商的利益,因此有钱App不停的被抵制和封杀。而网易有钱也在2021年4月正式终止服务。

对我来说,一个完美的记账软件应该满足以下几点特性:

  1. 无不可记之帐(消费,借贷,甚至资产折旧),其中数据始终能如实的反应我实际账户的情况,最好一分钱都不要差。
  2. 支持一定程度的自动化,个人大部分的生活模式和消费都是重复的模式,理应自动化记录。
  3. 原始数据最好完全属于我,并且是我可以识别的格式,或者至少可以不丢失任何信息的进行导出。
  4. 有基本的统计与可视化,能快速了解时间轴上的收入和支出趋势,以及某一时间段内的支出类型比例

在年初的那个周日白天,我在搜索中逐渐锁定了beancount这个开源软件,它似乎满足以上的所有要求,但是又显得如此的另类(对非程序员并不友好),于是我看了很多相关的文章,并且用一下午的时间完成了基础概念的学习,基础语法的学习,项目目录的构建,并记下了第一笔账目,然后将项目同步到我的github私人仓库。在beancount中记账类似于下面这个样子:

开设账户,xxxx是我的交通银行卡后4位,这里隐去了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
; Assets
2021-01-03 open Assets:Bank:BOCM:xxxx CNY

; Liabilities
2021-01-03 open Liabilities:Internet:JDs CNY

; Expenses
2021-01-03 open Expenses:Food:Cook

; Incomes
2021-01-03 open Income:Salary

; Equity
2021-01-03 open Equity:OpenBalance

账户初始化

1
2
3
2021-01-03 * "交通银行储蓄卡初始化"
Assets:Bank:BOCM:5836 10000.00 CNY
Equity:OpenBalance

支出记录

1
2
3
2021-01-03 * "叮咚买菜"
Expenses:Food:Cook 85.28 CNY
Assets:Bank:BOCM:xxxx

beancount的数据查询,统计,可视化相关的内容可以看参考资料中的链接,我还未充分挖掘。

我的beancount记账工作流

这半年里,我逐渐形成了一个简单的记账工作流,我每周一般会在周末用15分钟左右进行集中记账。为了提高自动化程度,但同时又避免过多的为了追求自动化导致的开发工作,我将微信和支付宝的默认支付方式绑定在同一张银行卡上,并且保证平日90%以上的琐碎消费都会从这一张卡中支出。我当前的记账工作流大概是这样子的:

  1. 打开银行网页,登陆账户,查询本周的账目明细,并下载导出txt文件(其实是逗号分隔符csv)
  2. 使用bean-extract命令,自动读取txt账单,并且转换成对应的账目,并粘贴到beancount账本文件中
  3. 在账本中,对未能自动分类的账目,手动分配相应的账户(如消费类型)
  4. 查询自己当前主要的几个账户的余额,并且在账本文件的末尾,通过balance语法将余额写下
  5. vscode在安装beancount插件后,如果交易记录和balance余额不匹配,该balance记录会自动标黄提示,并且提示出金额差了多少,这时我会手动翻阅账单补全记录,确保没有错账和漏帐
  6. 运行bean-file命令,将txt账单归档到项目文件夹下

每周的记账对于我来说已经是一种舒适而愉悦的过程了,因为时间不长,流程清晰,而且每次我都可以保证自己没有任何遗漏(这可能是让人心旷神怡的最主要原因),同时这一周做过的一些事情因为账目产生联想很自然的又在我脑子中回放了一遍。

记账半年,我的变化?

其实没有太大变化,但是对财务状况更加清晰:

  • 我清楚的了解到我当前每月大致的盈余和支出比例分布
  • 我清楚的知道我今年在玩无人机这一兴趣爱好上的投入花费了 12587.08 元, 并且其中各项支出均有清晰的记录
  • 我清楚的知道我上一趟去深圳旅游花费了 3589.54 元,并且其中各项支出均有清晰的记录
  • 我直观的看到了银行活期的收益是何时发放的,比例有多少

也做出了一些行动:

  • 我开始规划个人的投资理财系统,并且逐步开始了实践

其他

关于一些工程项目配置,自动导入插件,以及beancount记账技巧等内容,考虑后续进行分享

参考资料