- 配置是“<key>=<value>”格式,它以着文本存储在一个叫preferences的文件,存放在小程序的userdata目录根下。
- 在C代码,是用着trose_prefs封装配置。如果要在非主线程读取配置,为和主线程可能的写同步,须要先申请prefs.mutex锁。
C:/Users/ancientcc/我的文档/RoseApp/launcher/aplt_leagor_basic__documents/preferences ------ base_product="wheeltec" base_serial="COM5" base_serial_baudrate=115200 laser_product="N10" laser_serial="COM3" laser_serial_baudrate=230400 moveit_serial="COM5" moveit_serial_baudrate=115200 [signature] [/signature]
以上是一个配置文件示例。signature是个必须存在的空块,trose_prefs要用它判断此个preferences文件是否有效。
一、lua代码
1.1 在main.lua定义preferences_def变量
aplt_leagor_basic = { ... preferences_def = { laser_product = "", laser_serial = "", laser_serial_baudrate = 115200, ... }; };
启动小程序时,首先会从aplt_leagor_basic__documents/preferences文件读出配置,然后加载main.lua,解析当中的preferences_def,来同步配置。这同步表现在几个方面。
- 字段名(key)一样。告知存在哪些配置字段,新版本可以用这功能添加新字段,或删除不用了的字段。如果preferences_def有,preferences文件没有,那要加上,加上时,用的值就是这里指定的值。相应地,preferences_def没有,preferences文件有,那要删除。换句话说,在析解preferences_def后,对已放到内存中的配置,它存储的key和preferences_def一模一样。
- 值(value)类型一样。新版本可以用这功能更改值类型。解析preferences_def时,如果该key两处都存在,除非值类型发生变化,否则不替换。举个例子,laser_serial字段之前存的是string类型,现在改为integer,解析preferences_def时,发现前、后类型发生变化,会用此刻的integer值替换掉原值。注意,这里不区分float和integer,原因么,preferences文件是以文本存储值,文本“1239”,你无法确定它是float还是integer。
preferences_def中“def”是默认(default)的缩写。默认含义体现在两处,1)该字段在preferences_def存在,在preferences文件不存在,用这默认值作为值。2)值类型发生变化时,也将用这默认值。
preferences_def必须写全小程序会用到的所有字段。不论读取(_value)还是设置(_set_value),要是用到一个这里没出现字段,那都会报错。
1.2 读
local laser_product, laser_serial = aplt.preferences:_value("laser_product", "laser_serial");
aplt指的是小程序主表,像aplt_leagor_basic。preferences是C代码为lua环境创建的一个表,lua代码可用这个表中提供的_value方法读取配置值。参数是要读出的key,一次调用可以读多个key。
1.3 写
aplt.preferences:_set_value("laser_product", "N10", "laser_serial_baudrate", 115200);
_set_value用于写配置。参数是要写的[key, value]对,一次调用可以写多个key。
对value,不能是nil。如果想从配置删除该key,用preferences_def机制。
二、C代码
在C代码,是用着trose_prefs封装配置。小程序在运行时,全局变量aplt::curr_aplt指向一个class tapplet,当中就有个类型是trose_prefs字段prefs。
const trose_prefs& aplt_prefs = aplt::curr_aplt->prefs; std::string path = aplt_prefs.get_str("base_serial"); int baudrate aplt_prefs.get_int("base_serial_baudrate", nposm);
上面是一个典型的在C代码读取配置逻辑。get_str用于读取一个string类型的值。get_int读取int类型,第二个参数是当“base_serial_baudrate”这个key不存在时,要返回的值。当然,由于preferences_def机制,理论上不会出现key不存在情况,不过rose处理app配置用的也是trose_prefs,那里就须要第二个参数。
三、多线程同步
- 主线程。读不需要加锁,写需要。
- 非主线程。读需要加锁,不允许写。
不论lua,还是C,归根用的都是class trose_prefs。trose_prefs采用的同步机制就决定了配置的同步要求。
只能在主线程更改配置。在lua,窗口肯定运行在主线程。所以对要更改配置,推荐方式是给用户提供个窗口界面。如果必须要在C代码,那确保在主线程。
若想在非主线程读取配置,为和主线程可能的写同步,须要先申请prefs.mutex锁。至于在主线程中读,因为写肯定在主线程,天然同步,不须要锁。
四、lua修改了配置后,如何尽快让c++代码知道配置改变了
lua修改了配置,意味着已把新配置写到preferences文件。这里说的是,让c++尽快知道哪些配置的值可能变了,以便c++知道后,去preferences文件读这几个配置,用到本地变量。
这里涉及到的两情况,都只是通知到正运行的小程序任务,即使这样任务,还要考虑它此刻没在运行。因而,任何小程序任务在开始运行时,都得有段读“全部”配置到本地变量的代码。
这里分两种情况,一是不须要通知,c++代码会在某个阶段去读取。二是lua调用send_cpp_id,告知c++,配置变了。
不建议c++以着时间片方法去轮询配置值。考虑到主线程(lua)修改配置不是个常用操作,用时间片轮询,太耗资源。
4.1 不须要通知,c++代码会在某个阶段去读取
目前符合的就这几个配置:底盘驱动、雷达驱动、机械臂驱动的型号、串口路径和波特率。
以底盘驱动为例。底盘驱动所在小程序在前台运行,修改了或型号,或串口路径,或波特率。退出,这时launcher会判断型号、串口路径和波特率是否有发生改变。一旦有改变,它就会重启底盘驱动节点。
4.2 lua调用send_cpp_id,告知c++,配置变了
以kHome小程序改变了“正坐(正面)”相关配置为例,说下这过程。
1、(lua)posture.lua以参数cpp_id填aplt.cpp_id_save_sf_6fields,调用send_cpp_id
<aplt_leagor_khome>/lua/posture.lua ------ function aplt_leagor_khome__posture.save_pref_fields(current_layer) local aplt = aplt_leagor_khome; local this = aplt_leagor_khome__posture; local var = this.var; if (current_layer == this.SIT_FRONT_LAYER) then var.window_:send_cpp_id(aplt.cpp_id_save_sf_6fields); elseif (current_layer == this.SIT_SIDE_LAYER) then var.window_:send_cpp_id(aplt.cpp_id_save_ss_4fields); elseif (current_layer == this.SIT_SHARE_LAYER) then var.window_:send_cpp_id(aplt.cpp_id_save_sshare_3fields); end return true; end
小程序对可能参数进行分类,用一个int值标识每一种类。和“正坐(正面)”相关的配置归为一类,数值是aplt.cpp_id_save_sf_6fields(100)。
send_cpp_id是window表的一个操作。
2、(c++)向可能正运行着小程序任务,调用它的fg_aplt_send_cpp_id方法
C++处理send_cpp_id,会向正运行着小程序任务发fg_aplt_send_cpp_id。因为有个规则,当运行前台小程序时,不会运行后台小程序。于是会发向的任务只可能是7个:6个驱动和底盘子任务。
fg_aplt_send_cpp_id是任务的方法,而不是小程序。如果小程序A实现了底盘、雷达驱动,并且都正在运行,那此个send_cpp_id,小程序A就会收到两次fg_aplt_send_cpp_id,当然,这是发向在不同任务。这里建议把每个任务归为一套或多套配置,然后用cpp_id进行区分。尽量不要让某个配置涉及多个任务。
3、(c++)特定任务读配置,应用到本地变量
void tkpose::fg_aplt_send_cpp_id(const std::string& window_id, int cpp_id) { if (cpp_id == cpp_id_save_sf_6fields || cpp_id == cpp_id_save_ss_4fields || cpp_id == cpp_id_save_sshare_3fields) { threading::lock lock(variable_mutex_); reload_pref_fields(cpp_id); } }
这是在主线程读配置,不须要加配置锁。上面这锁是同步tkpose内部变量。当这些配置赋值给内部变量,非主线程可能正要用这些变量,所以须要加锁。
window_id是lua代码发出send_cpp_id的window_,指示发出的是哪个窗口。示例:aplt_leagor_khome__posture。
4.3 在send_cpp_id机制下,小程序要做的几件事
1、对配置进行分类,用一个int值标识某类别。
小程序可使用的最小值是rosek.cpp_id_aplt_min(100)。对小于100的,保留给系统使用。
2、在lua代码,修改了配置后,在适当时机调用send_cpp_id
时机一般分在两个。
时机一:是个多tab窗口,要切到另外tab
function aplt_leagor_khome__posture.did_navigation_pre_change(report, from, to) local this = aplt_leagor_khome__posture; local var = this.var; this.save_pref_fields(from:at()); return true; end
时机二:所在窗口要被关闭
function aplt_leagor_khome__posture.post_show() local this = aplt_leagor_khome__posture; local var = this.var; this.save_pref_fields(var.current_layer_); var = {}; end
两种时机时,都要调用send_cpp_id。
3、在c代码,重载fg_aplt_send_cpp_id,保存配置到本地变量
要注意,send_cpp_id只是发向了此时正运行着的小程序任务。对该任务来说,还要考虑到它没在运行时,配置被lua改了。因而任务在开始运行,依旧是得有段读配置到本地变量的代码。
tkpose::tkpose(const aplt::tapplet::ttask& cfg_task, ttask_vars& task_vars) { ... reload_pref_fields(cpp_id_save_sf_6fields); reload_pref_fields(cpp_id_save_ss_4fields); reload_pref_fields(cpp_id_save_sshare_3fields); ... }
在构造kpose时,读配置到本地变量的代码。