App需求:保存物模型

什么是物模型

在项目开发中,用到了本地数据库,这个本地数据库记录了系统的物模型。所谓物模型就是对某一个设备的可操纵属性的定义,每一个设备包括了一个或者多个属性,通过获取这些属性的当前值可以得到该设备的状态,改变设备的一个属性(或者多个属性的组合)可以控制该设备。

例如一台伺服电机,它可能包含以下的属性:

  • 电源开关:这是一个开关型的属性,是可读写的,用一个布尔型数表示,1表示逻辑真、接通、打开,0表示逻辑非、断开、关闭
  • 旋转方向:这是一个枚举型属性,是可读写的,用一个枚举型数表示,1表示正转,0表示停止,-1表示反转
  • 转速:这是一个数值型的属性,是可读写的,用一个有取值范围的整数表示,0表示停转,10000 表示 10000RPM 的转速。它的取值范围是 0~10000 RPM。

设备的属性之间是有约束的。在电机这个例子中,属性之间的约束至少是:

  • 当电源开关处于关闭状态时,设置旋转方向和转速是没有意义的,只有在电源开关打开时,其它参数才有意义。
  • 旋转方向和转速一起确定了电机的运转状态。
  • 设置转速低于0,或者设置转速高于10000 是没有意义的。设备收到这类“错误”指令参数时,会采取不同的策略,通常是将参数“归化”到取值范围内,也有的设备是忽略这些错误参数,保持目前的工况不变。

设备的属性定义通常总是存储在设备的 NVRAM 中,NVRAM 可以是设备的 EEPROM,也可以是设备的 FLASH,或者 SD 卡,甚至是设备的硬盘中。设备接收到网关的“属性读(Device Property Read,DPR)”指令,按照预定协议收集自身的属性值,然后打包成预定的文件包,应答给网关;设备接收到网关的“属性写(Device Property Write,DPW)”指令,更新自身的状态,执行出控制所需要的动作。

物模型数据库

在网关上要保存其所支持的所有设备的物模型,设备(或者干脆叫做“子设备”,“网络终端单元”)只保存自己的物模型,网关和子设备上对于同一型号的设备,物模型保持相同。

子设备使用英文 Subset表示,网络终端单元用英文缩写 NTU 表示,英文全称是 Network Terminal Unit。后面的定义中会用到 Subset 和 NTU 这两个名词。

网关通常支持多种型号的子设备,需要在本地持久化。网关上的物模型可以根据需求增加,过时的物模型可以删除。为了做到更一般化,枚举型的属性还会有它的二级定义,即这个枚举型的取值。

从以上的说明可以设想到:物模型数据库必定有几个多级的表组成,是一个典型的一主多从的结构。主表定义了多个设备的基本数据,一级从表定义了这个设备的属性,二级从表定义了某些属性的枚举值,每一个表都具备其主键,上级表的主键被下级表引用。

扯了这么多物模型,对根结底还是三张表。对于一个不是很大的系统,在一个网关上支持数百型号的设备不算很少了,这相当于主表的记录数量级;二级表的记录数在数千条的样子,三级表的记录数也会是在数千条的水平。这种规模的表,使用 SQLite 正合适。

使用 SQLiteStudio 创建本地数据库文件

SQLiteStudio 已经升级到 3.4.4,下载地址:https://download.ihsdus.cn/down/2023down/12/27/SQLiteStudio-3.4.4.exe?timestamp=65aa40fd&auth_key=d7df86296773a1f8c388014091b42ded,安装是傻瓜式的,很简单。

使用 SQLiteStudio 创建了一个数据库文件:I2oT.db,保存在工程以外的目录,我干脆是在桌面创建了这个文件。在 I2oT.db 文件中创建了 4 个表。

GatewayProfile 记录网关访问的两个参数,SubsetModels 是上面所说的主表,它的结构如下:

二级表 NTUDefinitions 的结构如下:

三级表 NTUDefintionOptions 的结构如下:

这三张表是一个典型的主键外联引用(Foreign Key)关系。

这三张表中的数据是使用 SQLiteStudio 的 SQL 语句直接 INSERT 进去的。

将本地数据库文件添加到项目中

在已经创建好的 I2oT 项目中,将初始化好的 I2oT.db 文件拷贝粘贴到项目的 I2oT.Android/Assets 目录中,如下图所示。

按下 F4 键,在属性中设置“始终复制”。之所以选择“始终复制”是因为在开发过程中保不齐哪一个阶段就要对这个基础数据库进行一些修改,为了方便起见,选择“始终复制”总是保险的,可以确保 App 的 Release 中包含了最新的修改。

在应用中对其进行复制

从桌面粘贴到项目中的 I2oT.db 文件是一种“资产”——就像它存放在的 Assets 目录中一样,要把它复制到可读写文件夹才是运行时的“数据库”。要在运行时访问数据库文件,按照如下步骤做。

修改 MainActivity.cs 文件,在

global::Xamarin.Forms.Forms.Init(this, savedInstanceState);

LoadApplication(new App());

语句之间增加复制数据库文件的语句如下。

using System;using Android.App;using Android.Content.PM;using Android.Runtime;using Android.OS;using System.IO;using Xamarin.Forms;namespace I2oT.Droid{[Activity(Label = "I2oT", Icon = "@mipmap/icon", Theme = "@style/MainTheme",MainLauncher = true,ConfigurationChanges = ConfigChanges.ScreenSize |ConfigChanges.Orientation |ConfigChanges.UiMode |ConfigChanges.ScreenLayout |ConfigChanges.SmallestScreenSize )]public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity{protected override void OnCreate(Bundle savedInstanceState){base.OnCreate(savedInstanceState);Xamarin.Essentials.Platform.Init(this, savedInstanceState);global::Xamarin.Forms.Forms.Init(this, savedInstanceState);// Copy I2oT.db as an asset to access directory.string dbFileName = "I2oT.db";var dbFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData);var dbFile = Path.Combine(dbFolder, dbFileName);if (!File.Exists(dbFile)){FileStream writeStream = new FileStream(dbFile, FileMode.OpenOrCreate, FileAccess.Write);Android.App.Application.Context.Assets.Open(dbFileName).CopyTo(writeStream);}LoadApplication(new App());}public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults){Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);base.OnRequestPermissionsResult(requestCode, permissions, grantResults);}}}

不论 Android 的版本如何,将代码中的 dbFolder 设置为

System.Environment.SpecialFolder.LocalApplicationData

或者

System.Environment.SpecialFolder.Personal

总是安全的做法,这是因为当一个 App 安装到虚拟机或者真机后,在默认状态下,App 对上面提到的这两个文件夹总是有完全的访问权限。

上面的代码将判断文件是否存在作为是否重新拷贝的依据,这个依据有时并不充分。我们可以在 I2oT.db 中增加一个 MyVersion 的表,这个表可以由以下代码创建:

DROP TABLE IF EXISTS AppVersion;CREATE TABLE AppVersion(VersionTEXTNOT NULL DEFAULT "1.0.0.0",NeedCopy BOOLEAN NOT NULL DEFAULT 1);INSERT INTO AppVersion VALUES ("1.0.8.0",1);

创建之后,在每一次发布 Release 之前,手动地将 AppVersion 表的 Version 更新到编译的版本号,NeedCopy 设置为 True。在MainActivity.cs 中,除了判断数据库文件是否存在以外,读取一下 资产目录(xxx.Android/Assets)中的 I2oT.db 中的 AppVersion 表,如果 NeedCopy 为真,就关闭数据库连接,然后再将资产文件拷贝到 Environment.SpecialFolder.LocalApplicationData 文件夹中。

当然,还可以有更多的方法来判断是否需要拷贝数据库文件,这完全取决于 App 的功能需求。

实现的截图

在 App 的“设置”页中的“子设备物模型”是页面入口,点击进入到 SubsetModels 展示页,点击某一个产品型号,进入到物模型展示页,其后台的绑定数据来自于 NTUDefinitions 表,点击某一个属性,展示该属性的详情,如果该属性是枚举型的话,还通过 Picker 控件展示其可选的枚举值。

总结

虽然将本地数据库文件打包发布到 Android App 不是一个新话题了,但从某度上得到的搜索结果并不多,而且貌似都是从 stackoverflow 上翻译过来的,依托的版本也千差万别,在 Xamarin.Forms 环境中的使用例子更是少得可怜。

愿我的编码为这个问题增加一点可行的答案。欢迎指正,一同提升。