从 OTA 应用实现看系统架构设计和限制
2022 年 08 月 12 日
OTA(Over-The-Air)是什么,应该不用做过多解释了。只要不是打算做一锤子买卖,每个系统都会带一个;不管是为了修正错误,还是增加功能,有了它,才有后续持续改善(和收费)的可能性。
OTA 的特别之处
相对于其他应用,OTA 其实是有一点特别的。
广义上的 OTA 系统
从广义上,或者说从一个比较高的角度上去看,OTA 其实是一套完整的系统。
flowchart LR
tool[[固件包制作工具]] --> |固件包|cloud[[云端的 OTA 服务]]
app[[主系统中的 OTA 应用]] --> |请求|cloud
cloud --> |固件包|app
app -.-> |固件包本地路径|recovery[[recovery 中的升级应用]]
tool[[固件包制作工具]] -.-> |固件包|storage[外部存储] -.-> |固件包路径|recovery
固件包制作工具
通过一定的规则生成固件包,并提交到云端的 OTA 服务
,或者放到外部存储
的设备。服务接受主系统中的 OTA 应用
发来的请求,用固件包响应。OTA 应用将固件包下载到本地后,将固件包本地路径传递给recovery 中的升级应用
(有多种方式,如写入文件,或者像 A/B 升级那样,直接调用接口)。升级应用通过路径访问固件包,并将升级包写入 flash。
狭义的 OTA
在广义上的 OTA 系统中,工具 / 服务 / 升级应用
这三部分都相对固定,因此一般都会有现成的解决方案提供。只有主系统中的 OTA 应用
,受限于系统本身的限制,有着不同的实现方案。因此,狭义上的 OTA 指的就是这一部分。但即使是这一部分,相对于主系统中其他的应用而言,也有其自己的特点。
flowchart LR
user((用户)) --> ui[界面]
system([系统]) --> service[服务]
ui --> |接口|main[应用主体]
service --> |接口/集成|main
main --> protocol[协议]
main --> localfile([本地文件系统])
protocol -.-> main
protocol --> cloud((云端服务))
cloud -.-> protocol
main --> systemapi([系统底层接口])
systemapi -.-> recovery([升级应用])
classDef external fill:gray,color:white
classDef sibling fill:white,color:black
classDef internal fill:red,color:white
class ui,service,api,main,protocol internal
class system,localfile,systemapi,recovery sibling
class user,cloud external
主系统中的 OTA 应用包含下面几部分:
- 界面:用户通过界面调用应用接口,使用应用主体。
界面不一定是 native 应用,也可以是 web 服务。 - 服务:有些系统需要定时启动 OTA,因此需要常驻的系统服务通过调用接口来使用应用主体。
根据系统设计,应用主体有可能被集成于服务中。 - 应用主体:接受外部接口请求,通过协议与云端交互,通过系统底层接口(例如重启)升级系统。
对于无法连接云端的系统,可以直接从本地文件系统升级。 - 协议:与云端交互的规则。 对于部分协议,云端可以反向控制设备做一些预设的行为。
可以看出,一个完整的 OTA 应用,既要做普通应用的工作(提供界面 / 服务;与云端交互),又要做系统应用的工作(调用底层接口)。可以说是麻雀虽小,五脏俱全。
OTA 和系统架构限制的关系
因为狭义的 OTA 应用的特殊性,在不同的系统中,就需要采用不同的方式实现。其问题本质,就是各个模块功能的取舍,以及实现各模块时,对其所属层次的选择。
模块功能的取舍其实很容易理解。比如:
- 没有屏幕的系统,自然就不需要界面;当然,也可以选择用 web 服务实现,例如 OpenWRT。
- 不需要定期检查更新的系统,很可能不需要服务。
- 无法连接云端的系统,也就不需要协议。如果从本地文件系统直接升级,那么连应用主体都可以省略。
但对于模块所属层次而言,就完全由系统架构限制决定了。
下面通过我了解过的几个不同系统中的例子,讨论一下系统架构是如何限制 OTA 应用实现的。
Android
众所周知,Android 的 OTA 通常都是 Turnkey 方案。那么,为什么 Android 可以做到?
答案很简单,Android 系统允许应用做的事情,实在是太全面了。
界面 / 服务 / 云端交互 / 系统签名 / 打包。所有你想要的,都能在一个应用包里面做到。那还要啥自行车?
从系统架构上来说,由于 Android framework 提供了你需要的所有接口。因此 Android 上的实现,应该是最轻松的。这里就不再多说了。
KaiOS 2.0/2.5
KaiOS 2.x 是直接从 Firefox OS 演变来的,属于 Web OS。这类系统的一个重大限制就是,应用层能用的 API,是以 Web 标准定义为基础的。下面是 Firefox OS 的系统架构图。
从架构图可以看出,系统提供的 API,都是通过 Gecko Runtime 提供的。一些扩展的 API,实际都是作为 Gecko 中的 DOM 实现而扩展出来。
对于 OTA 应用来说,系统提供的 API 肯定是不足的。随便举几个例子:
- 文件系统:web 无法直接操作文件系统,最多只能写到浏览器提供的 localstorage。但 recovery 必须要从文件系统中获取升级包。
- http request:在应用中试图直接发送 http 请求,必然会遇到跨域问题。
- 服务:没有现成的实现服务的机制,甚至连 service worker 也没有(KaiOS 2.x 用的是 gecko48,service worker 功能尚未完善)。其实即使有 service worker 也没太大意义,毕竟那不是真正的 service。
虽然可以扩展,但扩展也是有风险的。比如假如直接放开文件系统 API,那么第三方应用乱来怎么办?
因此,在 KaiOS 2.x 上,OTA 必须被分为至少三部分:
- 位于 app 层的界面(也只能是界面)。
- 位于 app 层中的特殊的 system app 中的服务。
- 位于 Gecko 层,使用 DOM 方式实现的的应用主体;以及对应的 IDL 接口定义。
flowchart TD
system[system app] --> |OTA调用|Gecko
app --> |OTA调用|Gecko
系统在编译时,会将应用主体的 IDL 接口自动编译成胶水代码,从而和应用主体一起编译到 Gecko 引擎中。
另外,由于 KaiOS 设计中,界面应用是在子进程中启动的,这导致它的 DOM 与 system app 所使用的 DOM 位于不同进程。因此,需要使用 IPDL 处理一些子进程和主进程的交互问题。这也是系统架构限制影响应用开发的一个典型例子。
KaiOS 3.0
KaiOS 3.0 相对于 2.X 系列,在架构上最大的变化,就是将扩展的 API,提取到了一个[专用的容器(api-daemon)(https://github.com/kaiostech/api-daemon) 去实现。容器和扩展的部分都是用 rust 实现的。
flowchart TD
system[system app] .-> Gecko
app .-> Gecko
system --> |OTA调用|daemon[api-daemon]
app --> |OTA调用|daemon
这样设计是有原因的。Gecko 作为 Mozilla 的产品,升级频率非常高。原有的扩展 API 的实现依赖于 Gecko,如果 Gecko 发生变化,很可能会使得扩展 API 的部分出错。新的设计,实际上使得系统各部分的变化更加独立。
容器所使用的接口定义采用了一种自定义的 DSL 语言,叫做 sidl。编译容器时,将接口定义转换成使用 protobuf 通信的的一个自定义协议实现,生成包括了 rust 和 js 两端的代码。由于容器本身就是 web 服务,因此如果更新了 rust 一侧的接口,web 应用一侧也可以通过容器提供的同一版本的 js 文件获取到接口的更新,这一设计避免了接口两端代码不适配的问题。
从架构上来看,这对 OTA 的实现产生的影响并不大。仍然是原来的三部分,区别仅仅只是原来在 Gecko 层的 DOM 实现,被移到了 api-daemon 容器中。由于容器有着独立的进程,因此也不需要考虑原来的子进程和主进程交互问题。对于开发者来说,整体的心智负担反而减轻了。
Open Harmony
本以为工作安排上会有这部分的研究,但由于各种原因,阴差阳错的,一直没机会实际看一下,很有可能以后也不会看了。
根据之前大概扫了几眼的印象,Open Harmony 的做法其实是类似 KaiOS 2.x 的做法,由于 Open Harmony 的 SA(系统能力 / System Ability)的设计,开发者有可能会对底层服务过度拆分。
目前这个章节只能留空,看看以后有没有机会或者必要补全了。
总结
广义的 OTA 涉及范围很广。包括了系统应用、PC 端工具和云端服务等。
狭义的 OTA 相当于一个普通的系统应用,需要适配不同操作系统架构的限制,做出不同的设计和实现。