从 OTA 应用实现看系统架构设计和限制

2022 年 08 月 12 日

OTA(Over-The-Air)是什么,应该不用做过多解释了。只要不是打算做一锤子买卖,每个系统都会带一个;不管是为了修正错误,还是增加功能,有了它,才有后续持续改善(和收费)的可能性。

OTA 的特别之处

相对于其他应用,OTA 其实是有一点特别的。

广义上的 OTA 系统

从广义上,或者说从一个比较高的角度上去看,OTA 其实是一套完整的系统。

固件包
请求
固件包
固件包本地路径
固件包
固件包路径
固件包制作工具
云端的 OTA 服务
主系统中的 OTA 应用
recovery 中的升级应用
外部存储

固件包制作工具通过一定的规则生成固件包,并提交到云端的 OTA 服务,或者放到外部存储的设备。服务接受主系统中的 OTA 应用 发来的请求,用固件包响应。OTA 应用将固件包下载到本地后,将固件包本地路径传递给recovery 中的升级应用(有多种方式,如写入文件,或者像 A/B 升级那样,直接调用接口)。升级应用通过路径访问固件包,并将升级包写入 flash。

狭义的 OTA

在广义上的 OTA 系统中,工具 / 服务 / 升级应用这三部分都相对固定,因此一般都会有现成的解决方案提供。只有主系统中的 OTA 应用,受限于系统本身的限制,有着不同的实现方案。因此,狭义上的 OTA 指的就是这一部分。但即使是这一部分,相对于主系统中其他的应用而言,也有其自己的特点。

接口
接口/集成
用户
界面
系统
服务
应用主体
协议
本地文件系统
云端服务
系统底层接口
升级应用

主系统中的 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 接口定义。
OTA调用
OTA调用
system app
Gecko
app

系统在编译时,会将应用主体的 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 实现的。

OTA调用
OTA调用
system app
Gecko
app
api-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 相当于一个普通的系统应用,需要适配不同操作系统架构的限制,做出不同的设计和实现。

Top