2021-09-03:Android7关机充电流程

背景

为了修改关机充电中的显示效果,因此学习一下Android 7关机充电的流程是怎么样的。

以msm8909为例。

[   94.741021] charger: [94654] animation starting
[ 94.744542] charger: animation starting cur_frame =0

参考:

动画的初始化

main

// system/core/healthd/healthd.cpp

struct healthd_mode_ops *healthd_mode_ops;

static struct healthd_mode_ops charger_ops = {
.init = healthd_mode_charger_init,
.preparetowait = healthd_mode_charger_preparetowait,
.heartbeat = healthd_mode_charger_heartbeat,
.battery_update = healthd_mode_charger_battery_update,
}; // main函数根据参数赋值不同ops给healthd_mode_ops
int main(int argc, char **argv) {
int ch;
int ret; klog_set_level(KLOG_LEVEL);
healthd_mode_ops = &android_ops; if (!strcmp(basename(argv[0]), "charger")) {
healthd_mode_ops = &charger_ops;
} else {
while ((ch = getopt(argc, argv, "cr")) != -1) {
switch (ch) {
case 'c':
healthd_mode_ops = &charger_ops;
break;
case 'r':
healthd_mode_ops = &recovery_ops;
break;
}
}
}
// 初始化、显示以及循环
ret = healthd_init(); periodic_chores();
healthd_mode_ops->heartbeat(); healthd_mainloop();
KLOG_ERROR("Main loop terminated, exiting\n");
return 3;
}

healthd_init

// system/core/healthd/healthd.cpp
static struct healthd_config healthd_config = {
.periodic_chores_interval_fast = DEFAULT_PERIODIC_CHORES_INTERVAL_FAST,
.periodic_chores_interval_slow = DEFAULT_PERIODIC_CHORES_INTERVAL_SLOW,
.batteryStatusPath = String8(String8::kEmptyString),
.batteryHealthPath = String8(String8::kEmptyString),
.batteryPresentPath = String8(String8::kEmptyString),
.batteryCapacityPath = String8(String8::kEmptyString),
.batteryVoltagePath = String8(String8::kEmptyString),
.batteryTemperaturePath = String8(String8::kEmptyString),
.batteryTechnologyPath = String8(String8::kEmptyString),
.batteryCurrentNowPath = String8(String8::kEmptyString),
.batteryCurrentAvgPath = String8(String8::kEmptyString),
.batteryChargeCounterPath = String8(String8::kEmptyString),
.batteryFullChargePath = String8(String8::kEmptyString),
.batteryCycleCountPath = String8(String8::kEmptyString),
.energyCounter = NULL,
.boot_min_cap = 0,
.screen_on = NULL,
}; static int healthd_init() {
// 创建epoll
epollfd = epoll_create(MAX_EPOLL_EVENTS);
if (epollfd == -1) {
KLOG_ERROR(LOG_TAG,
"epoll_create failed; errno=%d\n",
errno);
return -1;
} healthd_board_init(&healthd_config);
// 注册监听的句柄
healthd_mode_ops->init(&healthd_config);
wakealarm_init();
// 添加其他句柄(socket)
uevent_init();
gBatteryMonitor = new BatteryMonitor();
gBatteryMonitor->init(&healthd_config);
return 0;
}

healthd_mode_charger_init

// system/core/healthd/healthd_mode_charger.cpp
void healthd_mode_charger_init(struct healthd_config* config)
{
int ret;
// // charger的状态是charger_state
struct charger *charger = &charger_state;
int i;
int epollfd; // 初始化kernel log
dump_last_kmsg(); LOGW("--------------- STARTING CHARGER MODE ---------------\n"); healthd_board_mode_charger_init();
// 注册事件处理函数,包括监听输入,电池唤醒
ret = ev_init(input_callback, charger);
if (!ret) {
epollfd = ev_get_epollfd();
healthd_register_event(epollfd, charger_event_handler, EVENT_WAKEUP_FD);
} // 解析animation.txt文件的内容,初始化animation结构体,下面会详细分析
// (大家可以先阅读这个函数的内容是什么,然后再返回这里往下看)
struct animation* anim = init_animation();
charger->batt_anim = anim; // 看函数名及参数可知,用于创建电池状态出错的界面
ret = res_create_display_surface(anim->fail_file.c_str(), &charger->surf_unknown);
if (ret < 0) {
LOGE("Cannot load custom battery_fail image. Reverting to built in.\n");
ret = res_create_display_surface("charger/battery_fail", &charger->surf_unknown);
if (ret < 0) {
LOGE("Cannot load built in battery_fail image\n");
charger->surf_unknown = NULL;
}
}
#ifdef CHARGER_USER_ANIMATION
GRSurface* scale_frames[USER_IMAGE_NUM]; for(int i = 0; i<USER_IMAGE_NUM; i++){
ret = res_create_display_surface(anim->user_animation_file[i].c_str(), &scale_frames[i]);
if (ret < 0) {
LOGE("Cannot load custom %s image. Reverting to built in.\n",anim->user_animation_file[i].c_str());
}else{
anim->frames[i].surface = scale_frames[i];
LOGW("file is:[%s],anim->frames[%d].surface = charger->surf_unknown;\n",
anim->user_animation_file[i].c_str(),i);
}
}
#else
GRSurface** scale_frames;
int scale_count;
int scale_fps; // Not in use (charger/battery_scale doesn't have FPS text
// chunk). We are using hard-coded frame.disp_time instead.
// 从函数名可知,从png文件中创建多帧动画界面
// 这个函数会根据png图片头部信息,将一张png图切成多个frame
// 因为之前UI部门给的图片都是动画切出来的多张图片,所以这个函数很少用到
ret = res_create_multi_display_surface(anim->animation_file.c_str(),
&scale_count, &scale_fps, &scale_frames);
if (ret < 0) {
LOGE("Cannot load battery_scale image\n");
anim->num_frames = 0;
anim->num_cycles = 1;
} else if (scale_count != anim->num_frames) {
LOGE("battery_scale image has unexpected frame count (%d, expected %d)\n",
scale_count, anim->num_frames);
anim->num_frames = 0;
anim->num_cycles = 1;
} else {
for (i = 0; i < anim->num_frames; i++) {
anim->frames[i].surface = scale_frames[i];
}
}
#endif
ev_sync_key_state(set_key_callback, charger); charger->next_screen_transition = -1;
charger->next_key_check = -1;
charger->next_pwr_check = -1;
healthd_config = config;
charger->boot_min_cap = config->boot_min_cap;
}

创建epoll的fd,创建关机闹钟的处理线程(创建一个新的epoll,并让关机闹钟处理事件epoll_wait在上面,如果关机闹钟事件触发,那么直接以rtc的开机原因热重启)

init_animation

// system/core/healthd/healthd_mode_charger.cpp
animation* init_animation()
{
bool parse_success; std::string content;
// 读取animation.txt文件信息,animation_desc_path定义如下
// static constexpr const char* animation_desc_path="/res/values/charger/animation.txt";
if (base::ReadFileToString(animation_desc_path, &content)) {
// 解析读取到的animation文件的内容,该函数在下面分析
// 大家也可以往下拉看怎么分析的,再回来这里往下看
parse_success = parse_animation_desc(content, &battery_animation);
} else {
LOGW("Could not open animation description at %s\n", animation_desc_path);
parse_success = false;
} if (!parse_success) {
// 解析失败,使用默认的animation动画
LOGW("Could not parse animation description. Using default animation.\n");
battery_animation = BASE_ANIMATION;
#ifdef CHARGER_USER_ANIMATION
battery_animation.user_animation_file[0].assign("charger/battery00");
battery_animation.user_animation_file[1].assign("charger/battery01");
//battery_animation.user_animation_file[2].assign("charger/battery02");
//battery_animation.user_animation_file[3].assign("charger/battery03");
//battery_animation.user_animation_file[4].assign("charger/battery04");
//battery_animation.user_animation_file[5].assign("charger/battery05");
battery_animation.frames = user_animation_frames;
battery_animation.num_frames = ARRAY_SIZE(user_animation_frames);
#else
battery_animation.animation_file.assign("charger/battery_scale");
battery_animation.frames = default_animation_frames;
battery_animation.num_frames = ARRAY_SIZE(default_animation_frames);
#endif
}
if (battery_animation.fail_file.empty()) {
// 未定义电池信息未知动画,就采用默认的电池信息未知动画
battery_animation.fail_file.assign("charger/battery_fail");
} // 输出解析到的内容
LOGV("Animation Description:\n");
LOGV(" animation: %d %d '%s' (%d)\n",
battery_animation.num_cycles, battery_animation.first_frame_repeats,
battery_animation.animation_file.c_str(), battery_animation.num_frames);
LOGV(" fail_file: '%s'\n", battery_animation.fail_file.c_str());
LOGV(" clock: %d %d %d %d %d %d '%s'\n",
battery_animation.text_clock.pos_x, battery_animation.text_clock.pos_y,
battery_animation.text_clock.color_r, battery_animation.text_clock.color_g,
battery_animation.text_clock.color_b, battery_animation.text_clock.color_a,
battery_animation.text_clock.font_file.c_str());
LOGV(" percent: %d %d %d %d %d %d '%s'\n",
battery_animation.text_percent.pos_x, battery_animation.text_percent.pos_y,
battery_animation.text_percent.color_r, battery_animation.text_percent.color_g,
battery_animation.text_percent.color_b, battery_animation.text_percent.color_a,
battery_animation.text_percent.font_file.c_str()); // 输出每帧动画的信息,显示时间,最低显示电量和最高显示电量
for (int i = 0; i < battery_animation.num_frames; i++) {
LOGV(" frame %.2d: %d %d %d\n", i, battery_animation.frames[i].disp_time,
battery_animation.frames[i].min_level, battery_animation.frames[i].max_level);
} return &battery_animation;
}

parse_animation_desc

// system/core/healthd/AnimationParser.cpp
bool parse_animation_desc(const std::string& content, animation* anim) {
// 定义animation.txt文件可以解析的字段
static constexpr const char* animation_prefix = "animation: ";
static constexpr const char* fail_prefix = "fail: ";
static constexpr const char* clock_prefix = "clock_display: ";
static constexpr const char* percent_prefix = "percent_display: ";
static constexpr const char* frame_prefix = "frame: "; // 把帧动画 放入vector(动态数组)
std::vector<animation::frame> frames; // for循环逐行解析
// 以 animation: 3 2 charger/boot_charger_02 为例
for (const auto& line : base::Split(content, "\n")) {
animation::frame frame;
const char* rest; // 跳过空行和'#'开头的行
if (can_ignore_line(line.c_str())) {
continue;
// remove_prefix会根据第二个参数是否符合,上面的例子刚好符合这一项
// 符合对应xxx_prefix后,会将"animation: "之后的内容,即"3 2 charger/boot_charger_02"
// 返回给rest,remove_prefix函数下面也会分析
} else if (remove_prefix(line, animation_prefix, &rest)) {
int start = 0, end = 0;
// sscanf将3赋给anim->num_cycles,其余类似
// 需要说明的是%n用于获取sscanf到%n未知读取的字符数,然后赋给start或者end
// 这里"%n%*s%n"就是将"charger/boot_charger_02"字符串在line中的
// 起始结束位置赋给start、end
if (sscanf(rest, "%d %d %n%*s%n", &anim->num_cycles, &anim->first_frame_repeats,
&start, &end) != 2 ||
end == 0) {
LOGE("Bad animation format: %s\n", line.c_str());
return false;
} else {
// 如果上面解析成功,就将"charger/boot_charger_02"赋给animation_file
anim->animation_file.assign(&rest[start], end - start);
}
// 下面的内容类似
} else if (remove_prefix(line, fail_prefix, &rest)) {
anim->fail_file.assign(rest);
} else if (remove_prefix(line, clock_prefix, &rest)) {
if (!parse_text_field(rest, &anim->text_clock)) {
LOGE("Bad clock_display format: %s\n", line.c_str());
return false;
}
} else if (remove_prefix(line, percent_prefix, &rest)) {
if (!parse_text_field(rest, &anim->text_percent)) {
LOGE("Bad percent_display format: %s\n", line.c_str());
return false;
}
} else if (sscanf(line.c_str(), " frame: %d %d %d",
&frame.disp_time, &frame.min_level, &frame.max_level) == 3) {
frames.push_back(std::move(frame));
} else {
LOGE("Malformed animation description line: %s\n", line.c_str());
return false;
}
} if (anim->animation_file.empty() || frames.empty()) {
LOGE("Bad animation description. Provide the 'animation: ' line and at least one 'frame: ' "
"line.\n");
return false;
} anim->num_frames = frames.size();
anim->frames = new animation::frame[frames.size()];
std::copy(frames.begin(), frames.end(), anim->frames); return true;
}

因为公司使用的是将gif动画切出来的一张张独立的图,不像默认动画battery_scale.png图那样,一张图包含所有的充电动画surface信息,所以这里我们可以修改这个函数:

  • 在animation结构体里面的frame结构体中添加一个string类型的字段frame_file,用来记录对应的frame文件名
  • 修改frame: 这个prefix的解析内容,添加frame文件的文件名字段解析,例如:frame: 50 16 20 charger/charging_animation_09,将最后的charger/charging_animation_09内容记录到新添加的frame_file字段中
  • 根据frame_file指定的文件名,通过res_create_display_surface函数来为每一帧动画图创建对应的frame,因为animation的frames指针就是用来记录动画内容的,所以将创建的frame记录到frames中即可。
  • 需要注意注释掉healthd_mode_charger_init函数中通过charger/scale创建muti frame的代码
remove_prefix
bool remove_prefix(const std::string& line, const char* prefix, const char** rest) {
const char* str = line.c_str();
int start;
char c; // 经过解析后format为" animation: %n%c"
std::string format = base::StringPrintf(" %s%%n%%c", prefix);
// sscanf解析后,start为"animation: 3 2 charger/boot_charger_02"字符串中3的所在位置
if (sscanf(str, format.c_str(), &start, &c) != 1) {
return false;
} // rest为"3 2 charger/boot_charger_02"字符串
*rest = &str[start];
return true;
}
open_png

到这里,动画的初始化过程就基本讲完了,不过还有一个需要注意的地方就是,在调用res_create_display_surface的时候会调用open_png函数来打开png图片,在这个函数里面会判断png图片是否符合要求

会检查位深

if (bit_depth == 8 && *channels == 3 && color_type == PNG_COLOR_TYPE_RGB) {
// 8-bit RGB images: great, nothing to do.
} else if (bit_depth <= 8 && *channels == 1 && color_type == PNG_COLOR_TYPE_GRAY) {
// 1-, 2-, 4-, or 8-bit gray images: expand to 8-bit gray.
png_set_expand_gray_1_2_4_to_8(*png_ptr);
} else if (bit_depth <= 8 && *channels == 1 && color_type == PNG_COLOR_TYPE_PALETTE) {
// paletted images: expand to 8-bit RGB. Note that we DON'T
// currently expand the tRNS chunk (if any) to an alpha
// channel, because minui doesn't support alpha channels in
// general.
png_set_palette_to_rgb(*png_ptr);
*channels = 3;
} else {
fprintf(stderr, "minui doesn't support PNG depth %d channels %d color_type %d\n",
bit_depth, *channels, color_type);
result = -7;
goto exit;
}

从上面一段代码可以看出,animation动画图支持3通道的RGB图,4通道的RGBA类型的图是支持不了的,这里的A指的是Alpha通道;另外就是单通道灰阶图也是支持的。

与动画有关的事件注册与处理

healthd_register_event

事件通过这个接口注册的,底层是epoll实现的。

// system/core/healthd/healthd.cpp

// int healthd_register_event(int fd, void (*handler)(uint32_t), EventWakeup wakeup = EVENT_NO_WAKEUP_FD);
int healthd_register_event(int fd, void (*handler)(uint32_t), EventWakeup wakeup) {
struct epoll_event ev; ev.events = EPOLLIN; if (wakeup == EVENT_WAKEUP_FD)
ev.events |= EPOLLWAKEUP; ev.data.ptr = (void *)handler;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
KLOG_ERROR(LOG_TAG,
"epoll_ctl failed; errno=%d\n", errno);
return -1;
} eventct++;
return 0;
}

注册的事件如下

healthd_mode_android.cpp:55:        if (healthd_register_event(gBinderFd, binder_event))
healthd.cpp:262: if (healthd_register_event(uevent_fd, uevent_event, EVENT_WAKEUP_FD))
healthd.cpp:285: if (healthd_register_event(wakealarm_fd, wakealarm_event, EVENT_WAKEUP_FD))
healthd_mode_charger.cpp:956: healthd_register_event(epollfd, charger_event_handler, EVENT_WAKEUP_FD);

定时事件:wakealarm_event

if (healthd_register_event(wakealarm_fd, wakealarm_event, EVENT_WAKEUP_FD))

设置定时刷新电池电量的闹钟(默认10 min刷新一次)

static void wakealarm_event(uint32_t /*epevents*/) {
unsigned long long wakeups; if (read(wakealarm_fd, &wakeups, sizeof(wakeups)) == -1) {
KLOG_ERROR(LOG_TAG, "wakealarm_event: read wakealarm fd failed\n");
return;
} periodic_chores();
}

IPC通信:binder_event

和安卓系统各个部分进行相互通知,这里不展开了。

#define LOG_TAG "healthd-android"

#include <healthd/healthd.h>
#include "BatteryPropertiesRegistrar.h" #include <binder/IPCThreadState.h>
#include <binder/ProcessState.h>
#include <cutils/klog.h>
#include <sys/epoll.h> using namespace android; static int gBinderFd;
static sp<BatteryPropertiesRegistrar> gBatteryPropertiesRegistrar; void healthd_mode_android_battery_update(
struct android::BatteryProperties *props) {
if (gBatteryPropertiesRegistrar != NULL)
gBatteryPropertiesRegistrar->notifyListeners(*props); return;
} int healthd_mode_android_preparetowait(void) {
IPCThreadState::self()->flushCommands();
return -1;
} static void binder_event(uint32_t /*epevents*/) {
IPCThreadState::self()->handlePolledCommands();
} void healthd_mode_android_init(struct healthd_config* /*config*/) {
ProcessState::self()->setThreadPoolMaxThreadCount(0);
IPCThreadState::self()->disableBackgroundScheduling(true);
IPCThreadState::self()->setupPolling(&gBinderFd); if (gBinderFd >= 0) {
if (healthd_register_event(gBinderFd, binder_event))
KLOG_ERROR(LOG_TAG,
"Register for binder events failed\n");
} gBatteryPropertiesRegistrar = new BatteryPropertiesRegistrar();
gBatteryPropertiesRegistrar->publish(gBatteryPropertiesRegistrar);
}

内核事件:uevent_event

监听来自内核的供电子系统(POWER_SUPPLY)的事件

#define UEVENT_MSG_LEN 2048
static void uevent_event(uint32_t /*epevents*/) {
char msg[UEVENT_MSG_LEN+2];
char *cp;
int n; n = uevent_kernel_multicast_recv(uevent_fd, msg, UEVENT_MSG_LEN);
if (n <= 0)
return;
if (n >= UEVENT_MSG_LEN) /* overflow -- discard */
return; msg[n] = '\0';
msg[n+1] = '\0';
cp = msg; while (*cp) {
if (!strcmp(cp, "SUBSYSTEM=" POWER_SUPPLY_SUBSYSTEM)) {
healthd_battery_update();
break;
} /* advance to after the next \0 */
while (*cp++)
;
}
}

唤醒处理:charger_event_handler

由于是EVENT_WAKEUP_FD的事件,代表了能够唤醒系统的输入源(键(例如音量键,电源键));

向下分发了ev,处理用户的输入。

static void charger_event_handler(uint32_t /*epevents*/)
{
int ret; ret = ev_wait(-1);
if (!ret)
ev_dispatch();
}

输入事件处理:input_callback

例如音量键,电源键。

static int input_callback(int fd, unsigned int epevents, void *data)
{
struct charger *charger = (struct charger *)data;
struct input_event ev;
int ret; ret = ev_get_input(fd, epevents, &ev);
if (ret)
return -1;
update_input_state(charger, &ev);
return 0;
}

input_callback是这样子执行的。

注册、分发:

void healthd_mode_charger_init(struct healthd_config* config)
{
struct charger *charger = &charger_state;
// ...
// 创建了一个epoll
ret = ev_init(input_callback, charger);
if (!ret) {
epollfd = ev_get_epollfd(); // 返回这个epoll的fd
// 将这个epoll的fd放到了healthd里面的healthd_mainloop中的epoll中统一进行监听
healthd_register_event(epollfd, charger_event_handler, EVENT_WAKEUP_FD);
}
} static void charger_event_handler(uint32_t /*epevents*/)
{
int ret; ret = ev_wait(-1);
if (!ret)
ev_dispatch();
} // vendor/qcom/proprietary/fastmmi/libmmi/events.cpp
void ev_dispatch(void)
{
int n;
int ret; for (n = 0; n < npolledevents; n++) {
struct fd_info *fdi = (fd_info *)(polledevents[n].data.ptr);
ev_callback cb = fdi->cb;
if (cb)
cb(fdi->fd, polledevents[n].events, fdi->data);
}
}

因此input_callback就通过这样子的方式执行了:

charger_event_handler
->ev_dispatch
->input_callback
->update_input_state
->set_key_callback
update_input_state

获取key的输入事件,对其他类型的输入事件不作处理。

static void update_input_state(struct charger *charger,
struct input_event *ev)
{
if (ev->type != EV_KEY)
return;
set_key_callback(ev->code, ev->value, charger);
}
set_key_callback
static int set_key_callback(int code, int value, void *data)
{
struct charger *charger = (struct charger *)data;
int64_t now = curr_time_ms();
int down = !!value; if (code > KEY_MAX)
return -1; /* ignore events that don't modify our state */
if (charger->keys[code].down == down)
return 0; /* only record the down even timestamp, as the amount
* of time the key spent not being pressed is not useful */
if (down)
charger->keys[code].timestamp = now;
charger->keys[code].down = down;
charger->keys[code].pending = true;
if (down) {
LOGV("[%" PRId64 "] key[%d] down\n", now, code);
} else {
int64_t duration = now - charger->keys[code].timestamp;
int64_t secs = duration / 1000;
int64_t msecs = duration - secs * 1000;
LOGV("[%" PRId64 "] key[%d] up (was down for %" PRId64 ".%" PRId64 "sec)\n",
now, code, secs, msecs);
} return 0;
}

将按键的code码和按下和抬起的状态,pending状态存入charger->keys中。

由于按键唤醒了healthd进程,处理完按键事件后,会再次调用healthd_mode_charger_heartbeat之后再进入poll_wait状态

healthd_mode_charger_heartbeat

还记得吗,在动画的循环中会执行这个函数进行处理:

void healthd_mode_charger_heartbeat()
{
struct charger *charger = &charger_state;
int64_t now = curr_time_ms();
// 处理接收进来的输入事件
handle_input_state(charger, now);
handle_power_supply_state(charger, now); /* do screen update last in case any of the above want to start
* screen transitions (animations, etc)
*/
update_screen_state(charger, now);
}
handle_input_state
static void handle_input_state(struct charger *charger, int64_t now)
{
// 处理按键,关键函数
process_key(charger, KEY_POWER, now);
process_key(charger, KEY_BACK, now); if (charger->next_key_check != -1 && now > charger->next_key_check)
charger->next_key_check = -1;
}

调用process_key时,传入了KEY_POWERKEY_BACK,代表需要处理的值,这个函数很重要,重点关注。

process_key

针对KEY_POWER的处理是这样子的:

  • 1、如果上次电源键按下设置的2s超时时间到了,且当前的按键依然是按下状态,那么判定用户是希望重启系统而不是点亮屏幕继续保持关机充电。
  • 2、如果在上次按下电源键的2s时间内有抬起的动作,那么判定用户的意图只是希望看下当前充电的状态(电量)
  • 3、如果用户是在屏幕亮起的状态下按了电源键,我们认为用户是想要灭屏

针对KEY_BACK的处理与KEY_POWER类似,只不过少了针对第一点(用户希望重启系统而不是点亮屏幕)的重启系统判断

  • 1、如果在上次按下电源键的2s时间内有抬起的动作,那么判定用户的意图只是希望看下当前充电的状态(电量)
  • 2、如果用户是在屏幕亮起的状态下按了电源键,我们认为用户是想要灭屏
static void process_key(struct charger *charger, int code, int64_t now)
{
struct animation *batt_anim = charger->batt_anim;
struct key_state *key = &charger->keys[code]; if (code == KEY_POWER) {
if (key->down) {
int64_t reboot_timeout = key->timestamp + POWER_ON_KEY_TIME; /*make sure backlight turn off before fb ready for display*
*when press the power key */
if (charger->charger_power_key == false) {
healthd_board_mode_charger_set_backlight(false);
gr_fb_blank(true);
charger->charger_power_key = true;
} // 如果上次电源键按下设置的2s超时时间到了,且当前的按键依然是按下状态,
// 那么我们判定用户是希望重启系统而不是点亮屏幕继续保持关机充电。
if (now >= reboot_timeout) {
/* We do not currently support booting from charger mode on
all devices. Check the property and continue booting or reboot
accordingly. */
if (property_get_bool("ro.enable_boot_charger_mode", false)) {
LOGW("[%" PRId64 "] booting from charger mode\n", now);
property_set("sys.boot_from_charger_mode", "1");
} else {
if (charger->batt_anim->cur_level >= charger->boot_min_cap) {
LOGW("[%" PRId64 "] rebooting\n", now);
android_reboot(ANDROID_RB_RESTART, 0, 0);
} else {
LOGV("[%" PRId64 "] ignore power-button press, battery level "
"less than minimum\n", now);
}
}
} else {
/* if the key is pressed but timeout hasn't expired,
* make sure we wake up at the right-ish time to check
*/
// 第一次按下按键会先进入这儿
// 意义:更新poll_wait的超时时间为2s
set_next_key_check(charger, key, POWER_ON_KEY_TIME);
}
} else {
if (key->pending) {
/* If key is pressed when the animation is not running, kick
* the animation and quite suspend; If key is pressed when
* the animation is running, turn off the animation and request
* suspend.
*/
if (!batt_anim->run) {
// 如果在上次按下电源键的2s时间内有抬起的动作,
// 那么判定用户的意图只是希望看下当前充电的状态(电量)
kick_animation(batt_anim);
request_suspend(false);
} else {
// 如果用户是在屏幕亮起的状态下按了电源键,我们认为用户是想要灭屏
reset_animation(batt_anim);
charger->next_screen_transition = -1;
healthd_board_mode_charger_set_backlight(false);
gr_fb_blank(true);
if (charger->charger_connected)
request_suspend(true);
}
}
}
}
else if(code == KEY_BACK)
{
if (key->down) {
/*make sure backlight turn off before fb ready for display*
*when press the power key */
if (charger->charger_power_key == false) {
healthd_board_mode_charger_set_backlight(false);
gr_fb_blank(true);
healthd_board_mode_charger_set_backlight(true);
charger->charger_power_key = true;
} } else {
if (key->pending) {
/* If key is pressed when the animation is not running, kick
* the animation and quite suspend; If key is pressed when
* the animation is running, turn off the animation and request
* suspend.
*/
if (!batt_anim->run) {
kick_animation(batt_anim);
request_suspend(false);
} else {
reset_animation(batt_anim);
charger->next_screen_transition = -1;
healthd_board_mode_charger_set_backlight(false);
gr_fb_blank(true);
if (charger->charger_connected)
request_suspend(true);
}
}
} } key->pending = false;
}

只有在按键抬起的时候,batt_anim->run的状态才会发生改变,kick_animation会将batt_anim->run置为true,从而使能动画显示。

之后在处理完本次按键事件后,会通过调用healthd_mode_ops->heartbeat()(即healthd_mode_charger_heartbeat)开启动画的显示,随后更新超时时间,进入三轮动画的显示流程。

内核主动通过uevent上报事件

uevent_event
->healthd_battery_update
->BatteryMonitor->update()
->healthd_mode_charger_battery_update()

healthd_mode_charger_battery_update函数很简单,只是简单的将从内核获取的prop信息传递给batt_prop

随后代码流程会回到healthd_mainloop中:

healthd_mainloop:
healthd_mode_ops->heartbeat();
mode_timeout = healthd_mode_ops->preparetowait();
if (timeout < 0 || (mode_timeout > 0 && mode_timeout < timeout))
timeout = mode_timeout;
nevents = epoll_wait(epollfd, events, eventct, timeout);
...

显然之后还会CALL到heartbeat,之后再更新epoll的超时时间heartbeat在关机充电的时候指向healthd_mode_charger_heartbeat

动画的循环

main:
periodic_chores();
healthd_mode_ops->heartbeat();

healthd_mainloop

在主循环中,会定期去处理

// system/core/healthd/healthd.cpp
static void healthd_mainloop(void) {
int nevents = 0;
while (1) {
struct epoll_event events[eventct];
int timeout = awake_poll_interval;
int mode_timeout; /* Don't wait for first timer timeout to run periodic chores */
if (!nevents)
periodic_chores(); // 执行 healthd_mode_charger_heartbeat
healthd_mode_ops->heartbeat(); // 执行 healthd_mode_charger_preparetowait
mode_timeout = healthd_mode_ops->preparetowait();
if (timeout < 0 || (mode_timeout > 0 && mode_timeout < timeout))
timeout = mode_timeout;
nevents = epoll_wait(epollfd, events, eventct, timeout);
if (nevents == -1) {
if (errno == EINTR)
continue;
KLOG_ERROR(LOG_TAG, "healthd_mainloop: epoll_wait failed\n");
break;
} // 执行通过healthd_register_event注册过的事件
for (int n = 0; n < nevents; ++n) {
if (events[n].data.ptr)
(*(void (*)(int))events[n].data.ptr)(events[n].events);
}
} return;
}

periodic_chores

static void periodic_chores() {
healthd_battery_update();
}

直接调用了healthd_battery_update,那么继续看下去

healthd_battery_update

调用

void healthd_battery_update(void) {
// Fast wake interval when on charger (watch for overheat);
// slow wake interval when on battery (watch for drained battery). // 执行了BatteryMonitor::update
int new_wake_interval = gBatteryMonitor->update() ?
healthd_config.periodic_chores_interval_fast :
healthd_config.periodic_chores_interval_slow; if (new_wake_interval != wakealarm_wake_interval)
wakealarm_set_interval(new_wake_interval); // During awake periods poll at fast rate. If wake alarm is set at fast
// rate then just use the alarm; if wake alarm is set at slow rate then
// poll at fast rate while awake and let alarm wake up at slow rate when
// asleep. if (healthd_config.periodic_chores_interval_fast == -1)
awake_poll_interval = -1;
else
awake_poll_interval =
new_wake_interval == healthd_config.periodic_chores_interval_fast ?
-1 : healthd_config.periodic_chores_interval_fast * 1000;
}
插入USB进行关机充电会直接显示三轮动画

gBatteryMonitor->update()实际上会执行BatteryMonitor::update

从而导致healthd_mode_charger_battery_update的执行

healthd_mode_charger_battery_update
void healthd_mode_charger_battery_update(
struct android::BatteryProperties *props)
{
struct charger *charger = &charger_state; charger->charger_connected =
props->chargerAcOnline || props->chargerUsbOnline ||
props->chargerWirelessOnline; //第一次进入have_battery_state为false,之后一直为true
if (!charger->have_battery_state) {
charger->have_battery_state = true;
charger->next_screen_transition = curr_time_ms() - 1;
reset_animation(charger->batt_anim);
kick_animation(charger->batt_anim);
}
batt_prop = props;
}

通过kick_animation使能动画显示anim->run = true;之后再进入healthd_mode_ops->heartbeat();从而进入动画的显示流程。

static void kick_animation(struct animation *anim)
{
anim->run = true;
}

healthd_mode_charger_heartbeat

void healthd_mode_charger_heartbeat()
{
struct charger *charger = &charger_state;
int64_t now = curr_time_ms();
// 处理接收进来的输入事件
handle_input_state(charger, now);
handle_power_supply_state(charger, now); /* do screen update last in case any of the above want to start
* screen transitions (animations, etc)
*/
// 刷新屏幕显示
update_screen_state(charger, now);
}

如果是uevent事件,那么按键处理相关是不会触发的,同时动画相关的屏幕显示其实也不会触发。

原因在于batt_anim->run,并没有被置1。那么这里还有一种情况就是在三轮动画的显示流程中,如果有来自于内核的uevent电源事件到来怎么处理。

healthd_mode_charger_preparetowait

int healthd_mode_charger_preparetowait(void)
{
struct charger *charger = &charger_state;
int64_t now = curr_time_ms();
int64_t next_event = INT64_MAX;
int64_t timeout; LOGV("[%" PRId64 "] next screen: %" PRId64 " next key: %" PRId64 " next pwr: %" PRId64 "\n", now,
charger->next_screen_transition, charger->next_key_check,
charger->next_pwr_check); if (charger->next_screen_transition != -1)
next_event = charger->next_screen_transition;
if (charger->next_key_check != -1 && charger->next_key_check < next_event)
next_event = charger->next_key_check;
if (charger->next_pwr_check != -1 && charger->next_pwr_check < next_event)
next_event = charger->next_pwr_check; if (next_event != -1 && next_event != INT64_MAX)
timeout = max(0, next_event - now);
else
timeout = -1; return (int)timeout;
}

动画显示过程

update_screen_state

动画显示无非就是判断当前电池的状态,然后选择到对应的frame,再通过healthd_draw显示到屏幕上面。

对于动画,每轮显示会进入update_screen_state多次,与下面的2个变量有关;

  • batt_anim->num_cycles :充电完整动画重复显示的次数
  • batt_anim->num_frames :每次充电完整动画需要分解为几次显示

其中每显示一帧,都会进入update_screen_state一次,最后灭屏进入suspend,细分的话有2种场景:

1、按键唤醒或者插入充电器的时候,当前电量没到最后一个frame的范围,那么连续显示当前电量的frame到电量满的frame动画,并重复三次,其中每轮动画中,当前电池电量对应的frame显示1.5秒,除此之外的每个frame默认显示时间为750ms。

2、按键唤醒或者插入充电器的时候,当前电量已经达到最后一个frame的范围,那么显示充电满的frame,同样连续显示3次,每次1.5s。

// system/core/healthd/healthd_mode_charger.cpp
static void update_screen_state(struct charger *charger, int64_t now)
{
struct animation *batt_anim = charger->batt_anim;
int disp_time; /*
1. batt_anim->run如果设置为false,则意味着不显示动画,就此返回
2. 如果当前时间小于下一次动画的触发时间,就此返回
*/
/* 如果是uevent事件,那么按键处理相关是不会触发的,同时动画相关的屏幕显示其实也不会触发。
原因在于batt_anim->run,并没有被置1。
那么这里还有一种情况:
在三轮动画的显示流程中,如果有来自于内核的uevent电源事件到来时。
由于当前时间小于下一次的frame显示时间而返回
*/
if (!batt_anim->run || now < charger->next_screen_transition) return; // 如下一段代码主要是为了点亮屏幕,如果无法点亮屏幕就退出
if (!minui_inited) {
if (healthd_config && healthd_config->screen_on) {
if (!healthd_config->screen_on(batt_prop)) {
LOGV("[%" PRId64 "] leave screen off\n", now);
batt_anim->run = false;
charger->next_screen_transition = -1;
if (charger->charger_connected)
request_suspend(true);
return;
}
}
//初始化显示
gr_init();
//获取字符显示的长和宽
gr_font_size(gr_sys_font(), &char_width, &char_height);
init_status_display(batt_anim); #ifndef CHARGER_DISABLE_INIT_BLANK
healthd_board_mode_charger_set_backlight(false);
gr_fb_blank(true);
#endif
minui_inited = true;
} /* animation is over, blank screen and leave */
// 如果本轮显示的次数达到了num_cycles(默认3次,4.5秒),则清屏,关背光,进入suspend
if (batt_anim->num_cycles > 0 && batt_anim->cur_cycle == batt_anim->num_cycles) {
reset_animation(batt_anim);
charger->next_screen_transition = -1;
charger->charger_power_key = false;
healthd_board_mode_charger_set_backlight(false);
gr_fb_blank(true);
LOGV("[%" PRId64 "] animation done\n", now);
if (charger->charger_connected)
request_suspend(true);
return;
} // 获取该帧显示的时间,第一次进入,batt_anim->cur_frame是为0的
// 取出第0帧的图像需要显示的时间(750ms)
disp_time = batt_anim->frames[batt_anim->cur_frame].disp_time; if (batt_anim->cur_cycle == 0 && batt_anim->cur_frame == 0)
redraw_screen(charger); /* animation starting, set up the animation */
// 动画开始,根据读到的电池百分比,选择当前应该显示的图片,并重新计算距离下次显示需要等待的最小时间间隔
if (batt_anim->cur_frame == 0) {
LOGV("[%" PRId64 "] animation starting\n", now);
if (batt_prop) {
// // 从battery service中获取当前点亮和电池状态
batt_anim->cur_level = batt_prop->batteryLevel;
batt_anim->cur_status = batt_prop->batteryStatus;
if (batt_prop->batteryLevel >= 0 && batt_anim->num_frames != 0) {
/* find first frame given current battery level */
// 找到第一张符合对应电量要求的帧
for (int i = 0; i < batt_anim->num_frames; i++) {
if (batt_anim->cur_level >= batt_anim->frames[i].min_level &&
batt_anim->cur_level <= batt_anim->frames[i].max_level) {
batt_anim->cur_frame = i;
break;
}
} LOGE("animation starting cur_frame =%d \n", batt_anim->cur_frame); // repeat the first frame first_frame_repeats times
// 动画第一帧可以设置重复显示次数,所以需要更新第一帧显示的时间
disp_time = batt_anim->frames[batt_anim->cur_frame].disp_time *
batt_anim->first_frame_repeats;
}
}
} /* unblank the screen on first cycle */
// 如果是本轮显示的第一次,则显示屏幕内容点亮背光灯
if (batt_anim->cur_cycle == 0) {
gr_fb_blank(false);
gr_flip();
healthd_board_mode_charger_set_backlight(true);
} /* draw the new frame (@ cur_frame) */
//开始显示动画
redraw_screen(charger); /* if we don't have anim frames, we only have one image, so just bump
* the cycle counter and exit
*/
// 如果没有动画资源,则自增圈数,更新距离下次显示的最小时间间隔,然后就此返回
if (batt_anim->num_frames == 0 || batt_anim->cur_level < 0) {
LOGW("[%" PRId64 "] animation missing or unknown battery status\n", now);
charger->next_screen_transition = now + BATTERY_UNKNOWN_TIME;
batt_anim->cur_cycle++;
return;
} /* schedule next screen transition */
// 计算下一张动画的切换时间
charger->next_screen_transition = now + disp_time; /* advance frame cntr to the next valid frame only if we are charging
* if necessary, advance cycle cntr, and reset frame cntr
*/
// 下面的逻辑是:只要充电的电量没有达到最后一个frame的阶段,那么直接取出下一次显示需要的frame用于显示。
// 如果还插着充电器
if (charger->charger_connected) { LOGE("update screen state cur cycle=%d \n", batt_anim->cur_cycle);
LOGE("update screen state cur_frame=%d \n", batt_anim->cur_frame);
LOGE("update screen state cur_level=%d \n", batt_anim->cur_level); // 记录下一帧动画的索引
batt_anim->cur_cycle++; /* don't reset the cycle counter, since we use that as a signal
* in a test above to check if animation is over*/
// 如果当前的电量已经没有落在下一帧中,那么需要调整下一帧的动画到合理的位置
while (batt_anim->cur_frame < batt_anim->num_frames &&
(batt_anim->cur_level < batt_anim->frames[batt_anim->cur_frame].min_level ||
batt_anim->cur_level > batt_anim->frames[batt_anim->cur_frame].max_level)) {
batt_anim->cur_frame++;
}
// 如果当前已经是最后一帧,那么将下一帧重置为0
if (batt_anim->cur_frame >= batt_anim->num_frames) {
batt_anim->cur_cycle++;
batt_anim->cur_frame = 0; } } else {
/* Stop animating if we're not charging.
* If we stop it immediately instead of going through this loop, then
* the animation would stop somewhere in the middle.
*/
// 拔出了充电器,重置变量
batt_anim->cur_frame = 0;
batt_anim->cur_cycle++;
}
}

redraw_screen

// system/core/healthd/healthd_mode_charger.cpp
static void redraw_screen(struct charger *charger)
{
struct animation *batt_anim = charger->batt_anim; clear_screen(); /* try to display *something* */
// 首先判断电池状态和动画内容是否有问题
if (batt_anim->cur_level < 0 || batt_anim->num_frames == 0)
draw_unknown(charger); // 有问题则显示battery_fail动画
else
draw_battery(charger); // 否则,显示电池充电动画
// 刷新屏幕
gr_flip();
} static void draw_battery(const struct charger* charger)
{
const struct animation& anim = *charger->batt_anim;
const struct animation::frame& frame = anim.frames[anim.cur_frame]; if (anim.num_frames != 0) {
// 如函数名,将动画显示到屏幕中间
draw_surface_centered(frame.surface);
LOGV("drawing frame #%d min_cap=%d time=%d\n",
anim.cur_frame, frame.min_level,
frame.disp_time);
}
// 如果有设置显示时钟和电池百分百,则同时描绘
draw_clock(anim);
draw_percent(anim);
}

所以,动画的连续显示也显得比较简单。如果想要做修改,理清显示流程即可。

另外,看上面的流程可以知道,如果需要显示电量百分比,可以提供对应的font字体文件,当然更简单的就是提供110和百分号的png图,然后按照上面解析动画的流程一样,将百分比对应的图片也加载进来,最后显示即可。

附录:关机充电时,按下按键的有关log

[ 1191.430816] qpnp_kpdpwr_irq into!
[ 1191.434579] PMIC input: code=116, sts=0x0
[ 1191.440183] healthd: void healthd_mainloop():epoll_wait is up
[ 1191.447273] charger: int input_callback(int, unsigned int, void*) into!
[ 1191.455508] healthd: void healthd_mainloop() ->heartbeat()
[ 1191.462325] charger: void healthd_mode_charger_heartbeat() into!
[ 1191.469792] charger: void process_key(charger*, int, int64_t) into! code=116
[ 1191.478251] charger: void process_key(charger*, int, int64_t) into! key->down=0
[ 1191.487067] charger: void process_key(charger*, int, int64_t) key->pending=1
[ 1191.495594] charger: void process_key(charger*, int, int64_t) !batt_anim->run
[ 1191.504184] charger: void update_screen_state(charger*, int64_t) into!
[ 1191.512167] charger: void update_screen_state(charger*, int64_t) into! gr_fb_blank(false) do_fb_ioctl ->fb_blank->fb_notifier_call_chain ->fb_notifier_callback

附录:ev_init、ev_get_input有关的库

在高通fastmmi中用到的一个用来处理输入事件的库events,本质是也是一个epoll_ctl

int ev_init(ev_callback input_cb, void *data)
{
DIR *dir;
struct dirent *de;
int fd;
struct epoll_event ev;
bool epollctlfail = false; epollfd = epoll_create(MAX_DEVICES + MAX_MISC_FDS);
if (epollfd == -1)
return -1; dir = opendir("/dev/input");
if(dir != 0) {
while((de = readdir(dir))) {
unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)]; // fprintf(stderr,"/dev/input/%s\n", de->d_name);
if(strncmp(de->d_name,"event",5)) continue;
fd = openat(dirfd(dir), de->d_name, O_RDONLY);
if(fd < 0) continue; /* read the evbits of the input device */
if (ioctl(fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) < 0) {
close(fd);
continue;
} /* TODO: add ability to specify event masks. For now, just assume
* that only EV_KEY and EV_REL event types are ever needed. */
if(!test_bit(EV_KEY, ev_bits) && !test_bit(EV_REL, ev_bits) && !test_bit(EV_SW, ev_bits)) {
close(fd);
continue;
} else if(test_bit(EV_ABS, ev_bits) && test_bit(EV_REL, ev_bits)) {
close(fd);
continue;
} ev.events = EPOLLIN | EPOLLWAKEUP;
ev.data.ptr = (void *)&ev_fdinfo[ev_count];
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) {
close(fd);
epollctlfail = true;
continue;
} ev_fdinfo[ev_count].fd = fd;
ev_fdinfo[ev_count].cb = input_cb;
ev_fdinfo[ev_count].data = data;
ev_count++;
ev_dev_count++;
if(ev_dev_count == MAX_DEVICES) break;
}
}
if (epollctlfail && !ev_count) {
close(epollfd);
epollfd = -1;
return -1;
} return 0;
}

Android7关机充电流程的更多相关文章

  1. 【转】android 电池(二):android关机充电流程、充电画面显示

    关键词:android 电池关机充电 androidboot.mode charger关机充电 充电画面显示 平台信息:内核:linux2.6/linux3.0系统:android/android4. ...

  2. android 电池(二):android关机充电流程、充电画面显示【转】

    本文转载自:http://blog.csdn.net/xubin341719/article/details/8498580 上一篇我们讲了锂电池的充放电的流程和电池的一些特性,这一节我们重点说一下a ...

  3. android 关机充电流程

    点击打开链接 0.主要流程 usb插入通过传递cmdline给init解析从而启动充电进程 1. LK lk\app\aboot\aboot.c update_cmdline ---------- i ...

  4. Android 电池关机充电

    android 电池(一):锂电池基本原理篇 android 电池(二):android关机充电流程.充电画面显示 android 电池(三):android电池系统 android电池(四):电池 ...

  5. MT6575 充电流程

    1,目前充电主要包括Power Off Charging(关机充电) .IPO Charging(休眠充电)和 OS Charging(开机充电) 三个部分 2,Power Off Charging ...

  6. 展讯7731C_M Android6.0 充电指示灯实现(一)------关机充电实现【转】

    本文转载自:https://blog.csdn.net/m0_37870649/article/details/80566131 前言: 在手机充电中常常使用充电指示灯来观察手机充电状态,比如说将手机 ...

  7. Android 8.1 关机充电动画(一)模式选择

    system:Android 8.1 platform:RK3326/PX30 uboot kernel Android 8.1 关机充电动画(一)模式选择 Android 8.1 关机充电动画(二) ...

  8. android关机充电

    1.关机充电其实是进入adb shell很快的方式! 2.手机关机时候插入USB,手机将进入关机充电模式,那么这个模式究竟是怎么进行的,这里分析如下! (1)uboot:这里代码大概浏览了一下:u-b ...

  9. 关机充电如何实现短按pwrkey灭屏

    目前关机充电PWRKEY实现长按开机和短按亮屏功能,灭屏是根据BL_SWITCH_TIMEOUTS时间,自动灭屏的:如果需要实现PWRKEY主动灭屏,请按照如下方法修改:     alps/media ...

  10. Android5.1关机充电界面尺寸修改

    Android5.1关机充电界面尺寸修改 因为项目的屏幕尺寸和一般的手机屏幕不一样,因此关机充电界面在设备上运行后严重变形,就需要自己修改这个界面了,废话不多说了,开打开打! 首先要说明这里是以And ...

随机推荐

  1. Unsortbin attack原理及分析

    Unsortbin attack原理 ️条件:首先要实现Unsortbin attack前提是可以控制Unsortbin attack chunk的bk指针 ️目的:我们可以实现修改任意地址为一个比较 ...

  2. 003_Orcad菜单讲解与偏好设置

    003_Orcad菜单讲解与偏好设置 菜单栏用的比较多的是File和Options项. 网格建议用lines,比较方便对齐. Auto Reference和Intertool Commuication ...

  3. Linux 备忘

    ls 通配符 匹配 ? 一个字符 * >=0个任意字符 [ai] a 或者 i [a-i] a/b/c/d...i [!a] 除了a cat cat -n test #加上行号 cat -b t ...

  4. 网络协议分析与抓包 TCP/IP UDP等

    学习地址: https://www.bilibili.com/video/BV1hV411U74y?p=4 https://www.bilibili.com/video/BV1S7411R7kF?p= ...

  5. layui.js

    目录 用法: 1.在base.js里导入layui插件 2.在使用的html页面里引入 base.js lucky.js index.html 用法: 1.在base.js里导入layui插件 2.在 ...

  6. 介绍几种常用的Oracle客户端工具

    首发微信公众号:SQL数据库运维 原文链接:https://mp.weixin.qq.com/s?__biz=MzI1NTQyNzg3MQ==&mid=2247485212&idx=1 ...

  7. linux下nginx安装和配置

    一.软件安装 1.环境说明 操作系统:CentOS 7.4 64位 nginx版本:1.16.1 安装日期:2019/10/01 安装用户:root 2.安装运行库 yum -y install gc ...

  8. TCP协议分析工具TcpEngine V1.2.0使用教程

    概述 目前主流的网络数据分析工具主要有两类,一类是http协议分析工具,如fiddler,这类工具擅长对字符串类型协议分析:另一类是原始网络数据包的监听分析,如Wireshark,这类工具擅长分析网络 ...

  9. AIRIOT答疑第2期|如何使用物联网平台的数据采集与控制引擎?

    任性用!   作为AIRIOT物联网低代码平台的五大核心能力引擎之一,数据采集与控制引擎具备极强的系统集成能力,提供丰富的接口,具备海量工业设备驱动库,分布式采集,稳定性高,实现快速的设备接入.报警. ...

  10. aardio桌面软件开发 简单,打包后文件小,支持 .net python 和 众多插件

    aardio 编程语言 - 官网 aardio  专注于桌面软件开发,17年一直保持非常活跃地更新( 更新日志 ),aardio 被多年用于生产项目实践,久经测试和锤炼.aardio 在诞生之初就设计 ...