diff --git a/docs/Application_guide/zh/media/solutions/poc/listen.png b/docs/Application_guide/zh/media/solutions/poc/listen.png index 6c1f9815385f09fb6425b11e72940e7cbb185472..a5dd2c60b9fe5ae51eccedd4f1ac2108afe8df45 100644 Binary files a/docs/Application_guide/zh/media/solutions/poc/listen.png and b/docs/Application_guide/zh/media/solutions/poc/listen.png differ diff --git a/docs/Application_guide/zh/media/solutions/poc/poc.png b/docs/Application_guide/zh/media/solutions/poc/poc.png deleted file mode 100644 index f870590c6fa42f5c736fd529dd74bc966514ef0b..0000000000000000000000000000000000000000 Binary files a/docs/Application_guide/zh/media/solutions/poc/poc.png and /dev/null differ diff --git a/docs/Application_guide/zh/media/solutions/poc/software_design.png b/docs/Application_guide/zh/media/solutions/poc/software_design.png index 42740b014703150d75c7728dc6898407411c9eed..51149392a2221d2d042215cee3efbf813461568f 100644 Binary files a/docs/Application_guide/zh/media/solutions/poc/software_design.png and b/docs/Application_guide/zh/media/solutions/poc/software_design.png differ diff --git a/docs/Application_guide/zh/solutions/poc/quick_start.md b/docs/Application_guide/zh/solutions/poc/quick_start.md index 6c1b4c5bf7bab879446fe449b51aef70e76085db..0b656c53ade960a7688fcca4a4b0f13b95b46644 100644 --- a/docs/Application_guide/zh/solutions/poc/quick_start.md +++ b/docs/Application_guide/zh/solutions/poc/quick_start.md @@ -56,7 +56,7 @@ ### 烧录固件包 -参考[此章节](https://python.quectel.com/doc/Application_guide/zh/dev-tools/QPYcom/qpycom-dw.html#%E4%B8%8B%E8%BD%BD%E5%9B%BA%E4%BB%B6),烧录固件包 [EC600MCNLER06A01M08_POC_XBND_OCPU_QPY_BETA0117.zip](https://github.com/QuecPython/solution-POC/releases/download/v.1.0.0/EC600MCNLER06A01M08_XBND_OCPU_QPY_BETA0802.zip) 至开发板。 +参考[此章节](https://python.quectel.com/doc/Application_guide/zh/dev-tools/QPYcom/qpycom-dw.html#%E4%B8%8B%E8%BD%BD%E5%9B%BA%E4%BB%B6),烧录固件包 [EC600MCNLER06A01M08_POC_XBND_OCPU_QPY_BETA0117.zip](https://github.com/QuecPython/solution-POC/releases/download/v2.0.1/EC600MCNLER06A01M08_POC_XBND_OCPU_QPY_BETA0117.zip) 至开发板。 ### 脚本导入与运行 @@ -113,4 +113,4 @@ **对方呼叫图示:** - + diff --git a/docs/Application_guide/zh/solutions/poc/software_design.md b/docs/Application_guide/zh/solutions/poc/software_design.md index bca3d80c26de65ed64c8953b41dab8bce2031ecb..f03c0c978280d1d18d18743313f7d8463aeecb9b 100644 --- a/docs/Application_guide/zh/solutions/poc/software_design.md +++ b/docs/Application_guide/zh/solutions/poc/software_design.md @@ -6,122 +6,873 @@ +- 按键模块 + - KeyManger:用于管理按键功能。 +- 服务模块 + - DevInfoService:设备信息服务,用于查询设备 IMEI、ICCID、固件版本信息等; + - MediaService:媒体服务,用于管理 tts 播报、mic 和 audio 音频处理; + - NetService:网络服务,用于管理网络状态和心跳检测; + - PocService:poc 服务,用于 poc 登录、群组获取、成员获取和对讲管理等。 +- 界面模块 + - MenuBar:状态栏,用于显示信号、时间、电量以及对讲图标; + - PromptBox:提示框,用于显示消息弹窗信息; + - Screen:UI 屏幕,用于显示各种界面信息,用户可自定义; + - PocUI:用于管理用户定义的 Screen。 +- 事件管理模块: + - EventMap:用于事件的发送与绑定。 + ### 业务系统启动流程 ![](../../media/solutions/poc/start.png) ## 代码讲解 +### 核心业务模块(POC) + +> 有关 poc 库的 API 函数可参考[POC-公网对讲机](https://python.quectel.com/doc/API_reference/zh/industry/poc.html)说明文档。 + +- #### 账户登录 + + 通过注册 `poc.login()` 回调函数,监听登录状态,并根据登录结果执行相应的操作。 + 具体功能如下: + + 1. 登录回调:通过 `poc.login()` 注册回调函数,监听登录状态(`param` 参数表示登录成功或失败)。 + 2. 登录成功处理:如果登录成功(`param == 1`),更新网络状态为正常,并根据平台类型(标准平台或其他平台)清理或存储安全数据。 + 3. 登录失败处理:如果登录失败,启动定时器定期检查网络连接状态,并标记网络错误状态。 + 4. 网络状态检查:通过定时器定期检查网络连接状态,确保设备能够重新尝试登录。 + 5. 界面更新:登录成功后,通知欢迎界面(`WelcomeScreen`)更新登录状态,并查询群组信息。 + + 该功能模块确保设备能够正确登录 POC 平台,并在登录失败时自动尝试重新连接。 + + ```python + class PocService(AbstractLoad): + ... + + # 登录回调 + poc.login(self.__poc_login_cb) + + def __poc_login_cb(self, param): + EventMap.send("welcomescreen__check_cloud_status", param) # 登录成功首页显示已登录,且去查询组群信息 + # 已登录 + if param == 1: + self.net_error = False + if self.__platform_dict.get(self.__platform) == 'std': + self.__securedata_xin_clear() + else: + self.__securedata_xin_store() + # 未登录 + else: + self.__cloud_check_timer.start(5*1000, 1, lambda arg: self.__check_cloud_connect()) + self.net_error = True + ``` + +- #### 进入群组 + + 通过注册 `poc.register_join_group_cb()` 回调函数,监听设备入组事件,并根据入组状态更新界面和播放提示音。 + 具体功能如下: + + 1. 注册入组回调:通过 `poc.register_join_group_cb()` 注册回调函数,监听设备是否成功进入群组。 + 2. 获取群组信息:通过 `poc.group_getbyid()` 查询当前群组的详细信息。 + 3. TTS 语音播报:根据入组状态和群组类型(临时群组或普通群组),生成相应的提示信息并通过 TTS 语音播报。 + 4. 状态管理:更新当前群组名称、登录状态和发言权限状态,确保设备状态与群组信息同步。 + 5. 临时群组处理:如果进入的是临时群组,启动定时器,在指定时间后自动退出临时群组。 + + 该功能模块确保用户在进入群组时能够及时收到语音提示,并正确处理群组切换和状态更新。 + + ```python + class PocService(AbstractLoad): + ... + + # 注册入组回调 + poc.register_join_group_cb(self.__poc_join_group_cb) + + def __poc_join_group_cb(self, param): + """ + 入组回调, 二次入相同群组, 不需要播报提示, + """ + PrintLog.log("PocService", "poc join group = {}".format(param)) + if not param[-1]: + return + group = poc.group_getbyid(0) + if isinstance(group, list): + now_group_name = group[1] + if not group[2]: + self.__last_join_group = group + self.__call_time_status = False + self.__call_member_timer.stop() + if not self.__group_name: + self.__group_name = now_group_name + else: + if self.__group_name == now_group_name: + self.tts_play_enable = False + else: + self.tts_play_enable = True + self.__group_name = now_group_name + if self.__login_status: + if group[2]: + tts_msg = "进入" + "临时群组" + self.__group_name + else: + tts_msg = "进入群组" + self.__group_name + else: + tts_msg = self.__get_user_info() + "已登录" + "进入群组" + self.__group_name + self.tts_play_enable = True + if not self.__login_status: + self.__login_status = True + if self.tts_play_enable: + EventMap.send("mediaservice__tts_play", (tts_msg, 1)) + if not self.__rocker_arm: + if not self.speak_close_first: + self.speak_close_first = True + EventMap.send("pocservice__close_speaker",None ,EventMap.MODE_ASYNC) + if group[2]: + self.__call_time_status = True + self.__call_member_timer.start(self.__call_quit_time * 1000, 0, lambda arg: self.__call_member_exit()) + ``` + +- #### 获取群组、成员列表 + + 通过调用 POC 接口获取群组和成员数据,并将数据返回给界面层进行渲染。 + 具体功能如下: + + 1. 获取群组列表:通过 `poc.get_groupcount()` 获取当前账号加入的群组数量,并通过 `poc.get_grouplist()` 获取群组列表数据。 + 2. 获取成员列表:通过 `poc.group_getbyid()` 获取当前群组信息,然后使用 `poc.get_membercount()` 和 `poc.get_memberlist()` 获取成员数量和成员列表数据。 + 3. 异常处理:如果群组或成员数据无效(如返回值为 -1 或数量为 0),则返回空数据,界面层会显示相应的提示信息。 + + 该功能模块为群组管理和成员列表界面提供了数据支持,确保用户能够查看和操作群组及成员信息。 + + ```python + class PocService(AbstractLoad): + ... + + EventMap.bind("group_get_list", self.__get_group_list) + EventMap.bind("member_get_list", self.__get_member_list) + + def __get_group_list(self, event=None, msg=None): + """获取群组列表""" + group_count = poc.get_groupcount() # 获取群组个数 + group_list = poc.get_grouplist(0, group_count) # 根据群组个数获取群组列表 + return group_count, group_list + + def __get_member_list(self, event=None, msg=None): + """获取成员列表""" + group = poc.group_getbyid(0) # 通过 gid 查询群组信息 + if -1 == group: + return -1, None + member_count = poc.get_membercount(group[0]) + if -1 == member_count or 0 == member_count: + return -1, None + member_list = poc.get_memberlist(group[0], 0, member_count) + return member_count, member_list + ``` + +- #### POC 对讲 + + 此部分是解决方案的核心功能模块,负责处理对讲状态的管理、网络状态检查以及界面提示的更新。 + 具体功能如下: + + 1. 开启对讲:当长按开发板 KEY1 键时,唤醒 LCD 屏幕并开启对讲功能。同时检查网络状态和当前群组状态,确保对讲功能正常使用。 + 2. 关闭对讲:当松开 KEY1 键时,结束对讲并释放相关资源,更新界面状态。 + 3. 网络状态检查:如果网络异常(如 SIM 卡问题),提示用户更换 SIM 卡。 + 4. 群组状态检查:如果当前群组无效,提示用户选择有效群组。 + 5. 界面提示更新:在对讲过程中,显示`讲话中...`提示框,并更新状态栏的对讲图标。 + 6. 音频管理:在对讲开启和关闭时,启用或禁用降噪功能,并播放提示音。 + + 该功能模块确保了对讲功能的稳定性和用户体验的流畅性。 + + ```python + class PocService(AbstractLoad): + ... + + EventMap.bind("pocservice__speaker_enable", self.__speaker_enable) + + def __speaker_enable(self, event, msg=None): + # 开启Poc对讲 + PrintLog.log("PocService", "speaker enable: {}".format(msg)) + if msg: + EventMap.send("poc_play_status", True) # 唤醒LCD + if self.__speaker_status: + EventMap.send("mediaservice__noise_reduction_enable", 1) + poc.speak(1) + if self.net_error: + if 3 != EventMap.send("welcomescreen__get_net_status"): + EventMap.send("mediaservice__tts_play", ("请更换卡", 1)) + EventMap.send("load_msgbox", "请更换sim卡") + return False + + # 检测当前群组 + curr_group = poc.group_getbyid(0) + if -1 == curr_group: + EventMap.send("mediaservice__tts_play", (self.__group_name_default, 1)) + EventMap.send("load_msgbox", self.__group_name_default) + else: + if not self.__rocker_arm: + EventMap.send("update_session_info", "您已被关闭发言") + else: + EventMap.send("load_msgbox", "讲话中...") # 加载消息提示框 + EventMap.send("menubar__update_poc_status", 1) # 更新状态栏图标 + + else: + EventMap.send("mediaservice__audio_tone") + return True + # 关闭Poc对讲 + else: + if self.__speaker_status: + EventMap.send("mediaservice__noise_reduction_enable", 0) + poc.speak(0) + utime.sleep_ms(100) + if not self.__rocker_arm: + pass + else: + EventMap.send("close_msgbox") + EventMap.send("menubar__update_poc_status", 0) + EventMap.send("poc_play_status", False) + ``` + +- #### 对方呼叫回调 + + 通过注册 `poc.register_audio_cb()` 回调函数,监听对方的呼叫信息,并根据呼叫状态更新设备状态和界面提示。 + 具体功能如下: + 1. 注册音频回调:通过 `poc.register_audio_cb()` 注册回调函数,监听对方的呼叫信息(`params` 参数包含语音状态、用户 ID、用户名和打断标志)。 + 2. 呼叫状态处理: + - 如果对方正在呼叫(`params[0] == self.BAND_CALL`),更新设备状态为“主动呼叫”。 + - 如果对方开始播放语音(`params[0] == self.BND_LISTEN_START`),更新设备状态为“呼叫结束”,并根据打断标志设置发言权限。 + - 如果对方停止播放语音(`params[0] == self.BND_LISTEN_STOP` 或 `params[0] == self.BND_SPEAK_STOP`),处理打断逻辑并更新设备状态。 + + 3. 界面更新:在对方呼叫时,显示消息提示框,唤醒 LCD 屏幕,并更新状态栏的对讲图标。 + 4. 状态管理:根据呼叫状态更新设备的主叫状态、发言状态和会话信息,确保设备状态与呼叫信息同步。 + + 该功能模块确保设备能够正确处理对方的呼叫信息,并实时更新界面和状态。 + + ```python + class PocService(AbstractLoad): + ... + + # 注册音频回调 + poc.register_audio_cb(self.__poc_audio_cb) + + def __poc_audio_cb(self, params): + PrintLog.log("PocService", "poc audio: {}".format(params)) + if params[0] == self.BAND_CALL: + self.main_call_end_state = self.CALL_STATE.IN_CALL + self.__speaker_status = 3 + self.last_audio = params[0] + + elif params[0] == self.BND_LISTEN_START: + self.last_audio = params[0] + self.main_call_end_state = self.CALL_STATE.CALL_END + if params[-1] == 0: + self.__speaker_status = 0 # 不允许打断 + else: + self.__speaker_status = 2 + self.__session_info = params[2] + state_msg = self.__session_info + EventMap.send("load_msgbox", state_msg) # 加载消息提示框 + EventMap.send("poc_play_status", True) # 唤醒 LCD 屏幕 + EventMap.send("menubar__update_poc_status", 2) # 更新状态栏图标 + EventMap.send("pocservice__call_member_status", 1) + + elif params[0] == self.BND_LISTEN_STOP or params[0] == self.BND_SPEAK_STOP: + # 需要判断是否是高等级打断播放 + if params[0] == self.BND_LISTEN_STOP and self.main_call_end_state == self.CALL_STATE.IN_CALL: + return + if params[0] == self.BND_LISTEN_STOP: + self.__speaker_status = params[-1] + self.__error_ptt_handler(params) + else: + pass + ``` + +### UI 界面 + +- #### 状态栏(MenuBar) + + MenuBar 类通过绑定、发送事件更新状态栏中的各个组件,确保用户能够实时查看设备的关键信息。 + 具体功能如下: + + 1. 更新信号强度:通过事件获取信号强度并显示对应的信号图标和网络类型(如4G)。 + 2. 更新时间:通过事件获取当前时间并显示在状态栏中。 + 3. 更新电量:通过事件获取当前电量并更新电量图标。 + 4. 更新对讲状态:根据对讲状态(对讲、播放)显示对应的图标,并控制图标的可见性。 + + 状态栏的大小为 240 × 40,位于 LCD 屏幕的上方。 + + ```python + class MenuBar(AbstractLoad): + NAME = "MenuBar" + ... + + def __update_time(self, arg=None): + time = EventMap.send("devinfoservice__get_time") + if time: + self.lab_time.set_text(time[1]) + + def __update_battery(self, arg=None): + battery = EventMap.send("screen_get_battery") + if battery: + self.img_battery.set_src(battery) + + def __update_signal(self, arg=None): + sig = EventMap.send("screen_get_signal") + if 0 < sig <= 31: + self.img_signal.set_src('U:/img/signal_' + str(int(sig * 5 / 31)) + '.png') + self.lab_signal.set_text("4G") + else: + self.img_signal.set_src("U:/img/signal_0.png") + self.lab_signal.set_text("x") + + def __update_poc_status(self, event, msg): + """ + 0 停止 1 对讲 2 播放 + """ + PrintLog.log(MenuBar.NAME, "poc status: {}".format(msg)) + if 0 == msg: + self.img_poc.add_flag(lv.obj.FLAG.HIDDEN) + elif 1 == msg: + self.img_poc.clear_flag(lv.obj.FLAG.HIDDEN) + self.img_poc.set_src("U:/img/poc_speaking.png") + elif 2 == msg: + self.img_poc.clear_flag(lv.obj.FLAG.HIDDEN) + self.img_poc.set_src("U:/img/poc_play.png") + ``` + +- #### 消息提示框(PromptBox) + + PromptBox 类通过弹窗的形式展示消息内容,并支持动态更新和关闭弹窗。 + 具体功能如下: + + 1. 显示消息弹窗:根据传入的消息内容(`msg`)和元数据(`meta`),创建一个弹窗并居中显示在屏幕上。 + 2. 动态更新消息:如果弹窗已存在,则在显示新消息前关闭旧弹窗,确保消息的实时性。 + 3. 关闭弹窗:提供关闭弹窗的功能,释放资源并隐藏弹窗。 + + 弹窗的大小为 180 × 90,消息内容支持自动换行,并居中显示。 + + ##### 定义 + + ```python + class PromptBox(AbstractLoad): + NAME = "PromptBox" + ... + + def __close(self, event=None, msg=None): + if self.prompt_box is not None: + self.prompt_box.delete() + self.prompt_box = None + + def __show(self, event, msg): + if self.prompt_box is not None: + self.prompt_box.delete() + self.prompt_box = None + + meta = msg.get("meta") + show_msg = msg.get("msg") + + self.prompt_box = lv.msgbox(meta, "PromptBox", "", [], False) + self.prompt_box.set_size(180, 90) + self.prompt_box.align(lv.ALIGN.CENTER, 0, 0) + self.prompt_label = lv.label(self.prompt_box) + self.prompt_label.set_pos(0, 0) + self.prompt_label.set_size(140, 50) + self.prompt_label.add_style(FontStyle.consolas_12_txt000000_bg2195f6, lv.PART.MAIN | lv.STATE.DEFAULT) + self.prompt_label.set_text(show_msg) + self.prompt_label.set_long_mode(lv.label.LONG.WRAP) + self.prompt_label.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0) + ``` + + ##### 使用 + + ```python + class PocUI(AbstractLoad): + ... + + def load_msgbox(self, event, msg): + """ + 加载消息框, 注意msg的格式: + { + "type": "promptbox", # 默认提示框 + "title": "[promptbox]" + "msg": "hello world", + "mode": 0 + } + """ + if isinstance(msg, dict): + _type = msg.get("type", PromptBox.NAME) # 默认提示框 + _type = "{}__show".format(type.lower()) + _msg = { + "meta":self.curr_screen.meta, + "msg": msg.get("msg", "[promptbox]"), + "mode": msg.get("mode", 0) + } + EventMap.send(_type, _msg) + else: + _msg = { + "meta":self.curr_screen.meta, + "title": "[promptbox]", + "msg": msg, + "mode": 0 + } + EventMap.send("promptbox__show", _msg) + + def close_msgbox(self, event, msg): + """ + 在这里对所有消息框发送关闭消息 + """ + EventMap.send("promptbox__close") + ``` + +- #### UI 屏幕 + + > 以 MemberScreen 为例介绍。 + + MemberScreen 类继承自 `Screen`,用于在 LCD 屏幕上显示成员信息,大小为 240 × 200,与状态栏共同构成完整的 LCD 显示区域。 + 具体功能如下: + + 1. 加载成员列表:从事件中获取成员列表数据,并动态创建列表项显示在屏幕上。 + 2. 管理选中状态:通过高亮背景色和滚动效果,显示当前选中的成员项。 + 3. 按键操作:支持按键事件(如单击、长按)来切换选中项或返回主界面。 + 4. 异常处理:如果成员列表为空或无成员,显示提示弹窗并返回主界面。 + 5. 界面更新:在成员列表发生变化时,动态更新列表内容。 + + 该类的设计实现了成员列表的动态加载和交互管理,确保用户能够方便地查看和操作成员信息。 + + ##### 加载并添加样式 + + ```python + class MemberScreen(Screen): + NAME = "MemberScreen" + + def __init__(self): + ... + + self.meta = lv.obj() # lvgl meta object + self.meta.add_style(CommonStyle.default, lv.PART.MAIN | lv.STATE.DEFAULT) + # 列表------------------------------------------------------------------------------------------ + self.list_menu = lv.list(self.meta) + + def load_before(self): + EventMap.bind("get_member_check_list", self.__get_member_check_list) + EventMap.bind("send_select_member_list", self.__send_select_member_list) + EventMap.bind("update_member_info", self.update_member_info) + + def load(self): + self.__load_member_list() + self.__member_screen_list_create() + self.__load_group_cur() + if self.member_list is None or self.member_list == -1 or not len(self.member_list): + EventMap.send("load_msgbox", "此群组无成员") + return False + if self.cur >= 0: + self.clear_state() + self.cur = 0 + self.add_state() + + def add_state(self): # 添加选中状态 + currBtn = self.list_menu.get_child(self.curr_idx) + currBtn.set_style_bg_color(lv.color_make(0xe6, 0x94, 0x10), lv.PART.MAIN | lv.STATE.DEFAULT) + currBtn.set_style_bg_grad_color(lv.color_make(0xe6, 0x94, 0x10), lv.PART.MAIN | lv.STATE.DEFAULT) + self.btn_list[self.curr_idx][2].set_long_mode(lv.label.LONG.SCROLL_CIRCULAR) + currBtn.scroll_to_view(lv.ANIM.OFF) + + def clear_state(self): # 清除选中状态 + currBtn = self.list_menu.get_child(self.curr_idx) + currBtn.set_style_bg_color(LVGLColor.BASE_COLOR_WHITE, lv.PART.MAIN | lv.STATE.DEFAULT) + currBtn.set_style_bg_grad_color(LVGLColor.BASE_COLOR_WHITE, lv.PART.MAIN | lv.STATE.DEFAULT) + self.btn_list[self.curr_idx][2].set_long_mode(lv.label.LONG.SCROLL_CIRCULAR) + currBtn.scroll_to_view(lv.ANIM.OFF) + + # 设置 key2 按键功能 + def key2_once_click(self, event=None, msg=None): + self.clear_state() + self.curr_idx = self.next_idx(self.curr_idx, self.count) + self.add_state() + + def key2_long_press(self, event=None, msg=None): + EventMap.send("close_msgbox") + EventMap.send("load_screen",{"screen": "MainScreen"}) + if self.curr_idx > 0: + self.clear_state() + self.curr_idx = 0 + ``` + + ##### 创建成员列表并显示成员信息 + + ```python + class MemberScreen(Screen): + ... + + def __member_screen_list_create(self): + """成员界面列表重新创建""" + # 把之前的list删掉 + if self.member_update_flag: + self.list_menu.delete() + # 再创建list + self.list_menu = lv.list(self.meta) + self.list_menu.set_pos(0, 40) + self.list_menu.set_size(240, 200) + self.list_menu.set_style_pad_left(0, 0) + self.list_menu.set_style_pad_right(0, 0) + self.list_menu.set_style_pad_top(0, 0) + self.list_menu.set_style_pad_row(1, 0) + self.list_menu.add_style(CommonStyle.container_bgffffff, lv.PART.MAIN | lv.STATE.DEFAULT) + self.list_menu.add_style(MainScreenStyle.list_scrollbar, lv.PART.SCROLLBAR | lv.STATE.DEFAULT) + self.list_menu.add_style(MainScreenStyle.list_scrollbar, lv.PART.SCROLLBAR | lv.STATE.SCROLLED) + if self.count: + self.add_member_msg(0, self.count) + self.member_update_flag = False + else: + EventMap.send("load_msgbox", "无成员") + + def add_member_msg(self, index, end): + self.member_btn_list = [] + for each in self.member_list[index:end]: + btn = lv.btn(self.list_menu) + btn.set_pos(20, 0) + btn.set_size(240, 47) + btn.add_style(MainScreenStyle.btn_group, lv.PART.MAIN | lv.STATE.DEFAULT) + img = lv.img(btn) + img.align(lv.ALIGN.LEFT_MID, 10, 0) + img.set_size(32, 32) + img.set_src('U:/img/number_{}.png'.format(each[4] + 1)) + lab = lv.label(btn) + lab.align(lv.ALIGN.LEFT_MID, 50, 13) + lab.set_size(210, 40) + lab.set_text(each[1]) + self.btn_list.append((btn, img, lab)) + self.add_state() + + def __load_group_cur(self): + ret = EventMap.send("get_group_name") + if ret: + print(ret) + EventMap.send("load_msgbox", '当前群组: {}'.format(ret)) + self.msgbox_close_timer.start(self.msgbox_close_time * 1000, 0, lambda arg: EventMap.send("close_msgbox")) + ``` + +- #### 加载 UI 屏幕 + + PocUI 类通过事件驱动的方式加载指定的屏幕,并处理屏幕切换时的资源管理和状态更新。 + 具体功能如下: + + 1. 屏幕切换:根据传入的屏幕名称(`msg["screen"]`),从屏幕列表中找到对应的屏幕并加载。 + 2. 资源管理:在加载新屏幕前,释放当前屏幕的资源并更新状态。 + 3. 状态栏显示:如果加载的屏幕不是欢迎界面(`WelcomeScreen`),则显示状态栏。 + 4. 屏幕生命周期管理:调用屏幕的 `load_before()`、`load()` 和 `load_after()` 方法,确保屏幕的初始化逻辑正确执行。 + 5. 图像缓存优化:在屏幕加载后,刷新图像缓存并设置缓存大小,以优化性能。 + + 该类是 UI 屏幕加载的核心逻辑,确保界面切换的流畅性和资源的高效管理。 + + ```python + class PocUI(AbstractLoad): + ... + + def load_screen(self, event, msg): + """ + 加载UI屏幕 + """ + for scr in self.screen_list: + if scr.NAME != msg["screen"]: + continue + if self.curr_screen: + if scr.NAME != self.curr_screen.NAME: + scr.set_last_screen(self.curr_screen.NAME) + self.curr_screen.deactivate() + self.curr_screen = scr + + PrintLog.log("PocUI", "load screen:{}".format(scr.NAME)) + + # 加载屏幕之前先加载屏幕栏 + if self.curr_screen.NAME != "WelcomeScreen": + EventMap.send("menubar__show", self.curr_screen.meta) + + scr.load_before() + scr.load() + scr.load_after() + lv.img.cache_invalidate_src(None) + lv.img.cache_set_size(8) + lv.scr_load(self.curr_screen.meta) # load lvgl meta object + ``` + +### 按键模块 + +- #### 功能描述 + + 通过按键实现屏幕的滚动、选择,以及开启对讲服务。 + + - KEY1 + 长按:开启对讲服务。 + - KEY2 + 单击:选择框往下滚动; + 双击:进入所选择屏幕; + 长按:返回上一级界面。 + +- #### 实现原理 + + ```python + class KeyManger(object): + + def __init__(self): + ... + + # 按键中断初始化 + self.key1 = ExtInt(ExtInt.GPIO13, ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_PU, self.__key1_event_handler) + self.key1.enable() # key1 + + self.key2 = ExtInt(ExtInt.GPIO12, ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_PU, self.__key2_event_handler) + self.key2.enable() # key2 + + def __key1_event_handler(self, event): + if event[1] == 1: + self.__key1_press_handle() + else: + self.__key1_up_handle() + + def __key1_press_handle(self): + self.__key1_long_timer.start(500, 0, self.__key1_long_handle) + + def __key1_long_handle(self, arg): + self.__key1_long_timer_flag = True # key1键 长按标志 + EventMap.send("ppt_press") + + def __key1_up_handle(self): + self.__key1_long_timer.stop() + + if self.__key1_long_timer_flag: + self.__key1_long_timer_flag = False + EventMap.send("ppt_release") + return + + def __key2_event_handler(self, event): + if event[1] == 1: + self.__key2_press_handle() + else: + self.__key2_up_handle() + + def __key2_press_handle(self): + self.__key2_long_timer.start(1500, 0, self.__key2_long_handle) + + def __key2_long_handle(self, arg): + self.__key2_long_timer_flag = True # key2键 长按标志 + EventMap.send("key2_long_press") + + def __key2_up_handle(self): + """key2键 抬起""" + self.__key2_long_timer.stop() + + if self.__key2_long_timer_flag: + self.__key2_long_timer_flag = False + return + self.__key2_count += 1 + + # 判断是否准备双击 + if not self.__key2_double_timer_flag: + self.__key2_double_timer_flag = True + self.__key2_double_timer.start(300, 0, self.__key2_up_timer) + + def __key2_up_timer(self, args): + if 2 <= self.__key2_count: + EventMap.send("key2_double_click") + else: + EventMap.send("key2_once_click") + self.__key2_count = 0 + self.__key2_double_timer_flag = False + ``` + ### 事件管理 -各个模块之间通过 `EventMap` 事件管理类进行协作。`EventMap` 事件管理类提供事件的绑定、取绑以及消息的同步、异步发送。其定义如下: - -```python -class EventMap(object): - """===example=== - - import EventMap - - def time_out(event=None, msg=None): - pass - - EventMap.bind("time_out", time_out) - - EventMap.send("time_out") - """ - __event_map = dict() - __event_log = None - - MODE_SYNC = 0 - MODE_ASYNC = 1 - - def __init__(self): - pass - - @classmethod - def bind(cls, event, callback): - """ - :param event: event name - :param callback: event callback - """ - if None == event or "" == event: - return - cls.__event_map[event] = callback - - @classmethod - def unbind(cls, event): - """ - :param event: event name - """ - if None == event or "" == event: - return - cls.__event_map.pop(event, None) - - @classmethod - def send(cls, event, msg=None, mode=MODE_SYNC): - """ - :param event: event name - :param msg: event message - :param mode: send mode, sync or async - """ - if event not in cls.__event_map: - return - - if cls.MODE_SYNC == mode: - res = None - try: - if event in cls.__event_map: - res = cls.__event_map[event](event, msg) - except Exception as e: - if cls.__event_log: - cls.__event_log.info("ERROR executed (event) -> {} (params) -> {} (result) -> {}".format(event, msg, res)) - usys.print_exception(e) - if cls.__event_log: - cls.__event_log.info("SYNC executed (event) -> {} (params) -> {} (result) -> {}".format(event, msg, res)) - return res - - elif cls.MODE_ASYNC == mode: - try: - _thread.start_new_thread(cls.__event_map[event], (event, msg)) - except Exception as e: - if cls.__event_log: - cls.__event_log.info("ERROR executed (event) -> {} (params) -> {} (result) -> {}".format(event, msg, res)) - usys.print_exception(e) - if cls.__event_log: - cls.__event_log.info("ASYNC executed (event) -> {} (params) -> {} (result) -> {}".format(event, msg, None)) - -``` - -### 抽象加载 - -`common.py` 提供了一个名为 `AbstractLoad` 的抽象加载基类,方便用户理解界面加载的过程,定义如下: - -```python -class AbstractLoad(object): - def load_before(self, *args, **kwargs): - """加载前调用""" - pass - - def load(self, *args, **kwargs): - """加载时调用""" - pass - - def load_after(self, *args, **kwargs): - """加载后调用""" - pass - - def instance_after(self, *args, **kwargs): - """实例化后调用""" - pass - - def deactivate(self, *args, **kwargs): - """失效""" - pass -``` - -`AbstractLoad` 抽象加载基类会被定义的 UI 界面和 `services` 所继承,在加载 UI 界面或加载服务时可以调用对应的抽象类方法,实现页面和服务的加载。 - -比如在主界面的定义中,可以在 `load` 方法中添加相应的代码实现主界面的布局与样式,这样在进入主界面后就能显示出设置的 UI 界面了。 +- EventMap 类通过维护一个事件映射表(`__event_map`),实现事件的发送和绑定,支持同步和异步两种消息发送模式。 + 具体功能如下: + + 1. 事件绑定:通过 `bind()` 方法将事件名称与回调函数关联,存储在 `__event_map` 中。 + 2. 事件解绑:通过 `unbind()` 方法移除指定事件的回调函数。 + 3. 事件发送: + - 同步发送(`MODE_SYNC`):在当前线程中直接执行回调函数,并返回执行结果。 + - 异步发送(`MODE_ASYNC`):在新线程中执行回调函数,不阻塞当前线程。 + + 4. 错误处理:在执行回调函数时捕获异常,并记录错误日志(如果启用了日志功能)。 + 5. 日志记录:支持记录事件的执行信息,便于调试和问题排查。 + +- 该功能模块是系统中各个模块之间通信的核心,确保事件能够高效、可靠地传递和处理。 + + ```python + class EventMap(object): + """===example=== + + import EventMap + + def time_out(event=None, msg=None): + pass + + EventMap.bind("time_out", time_out) + + EventMap.send("time_out") + """ + __event_map = dict() + __event_log = None + + MODE_SYNC = 0 + MODE_ASYNC = 1 + + @classmethod + def bind(cls, event, callback): + """ + :param event: event name + :param callback: event callback + """ + if None == event or "" == event: + return + cls.__event_map[event] = callback + + @classmethod + def unbind(cls, event): + """ + :param event: event name + """ + if None == event or "" == event: + return + cls.__event_map.pop(event, None) + + @classmethod + def send(cls, event, msg=None, mode=MODE_SYNC): + """ + :param event: event name + :param msg: event message + :param mode: send mode, sync or async + """ + if event not in cls.__event_map: + return + + if cls.MODE_SYNC == mode: + res = None + try: + if event in cls.__event_map: + res = cls.__event_map[event](event, msg) + except Exception as e: + if cls.__event_log: + cls.__event_log.info("ERROR executed (event) -> {} (params) -> {} (result) -> {}".format(event, msg, res)) + usys.print_exception(e) + if cls.__event_log: + cls.__event_log.info("SYNC executed (event) -> {} (params) -> {} (result) -> {}".format(event, msg, res)) + return res + + elif cls.MODE_ASYNC == mode: + try: + _thread.start_new_thread(cls.__event_map[event], (event, msg)) + except Exception as e: + if cls.__event_log: + cls.__event_log.info("ERROR executed (event) -> {} (params) -> {} (result) -> {}".format(event, msg, res)) + usys.print_exception(e) + if cls.__event_log: + cls.__event_log.info("ASYNC executed (event) -> {} (params) -> {} (result) -> {}".format(event, msg, None)) + + ``` + +### 主程序 + +- APP 类采用模块化设计,通过统一的管理接口实现各组件的动态加载和生命周期管理。 + 具体功能如下: + + 1. 核心组件管理: + - 管理 UI 界面(`set_ui()`)、按键模块(`add_key()`)、状态栏(`add_bar()`)、消息框(`add_msgbox()`)和屏幕(`add_screen()`) + - 通过类型检查确保添加的组件符合 AbstractLoad 抽象类规范 + + 2. 服务管理: + - 通过 `add_service()` 添加后台服务(如网络服务、音频服务) + - 调用服务的 `instance_after()` 进行初始化,维护服务列表(`__service_list`) + + 3. 应用启动: + - 在 `exec()` 方法中依次启动 UI 组件(`__ui.start()`)和所有服务 + - 调用 LVGL 的任务处理器(`lv.task_handler()`)维持 GUI 运行 + +- 该类的设计实现了应用程序的模块化架构,为大型嵌入式 GUI 应用提供了可扩展的框架基础。 + + ```python + class App(object): + __service_list = [] + __ui = None + __key = None + + @classmethod + def set_ui(cls, ui): + cls.__ui = ui + + @classmethod + def add_key(cls, key): + cls.__key = key + + @classmethod + def add_bar(cls, bar:AbstractLoad): + """ + 这里只负责向UI添加屏幕栏, 屏幕栏由UI进行管理 + """ + try: + if isinstance(bar, AbstractLoad): + cls.__ui.add_bar(bar) + except Exception as e: + raise Exception("[App](abort) add_bar error: ", e) + return cls + + @classmethod + def add_msgbox(cls, msgbox:AbstractLoad): + """ + 这里只负责向UI添加消息框, 消息框由UI进行管理 + """ + try: + if isinstance(msgbox, AbstractLoad): + cls.__ui.add_msgbox(msgbox) + except Exception as e: + raise Exception("[App](abort) add_msgbox error: ", e) + return cls + + @classmethod + def add_screen(cls, screen:AbstractLoad): + """ + 这里只负责向UI添加屏幕, 屏幕由UI进行管理 + """ + if None == cls.__ui: + raise Exception("UI is None.") + try: + if isinstance(screen, AbstractLoad): + cls.__ui.add_screen(screen) + except Exception as e: + raise Exception("[App](abort) add_screen error: ", e) + return cls + + @classmethod + def add_service(cls, service:AbstractLoad): + """ + 添加服务 + """ + try: + if isinstance(service, AbstractLoad): + service.instance_after() # 初始化服务 + cls.__service_list.append(service) + except Exception as e: + raise Exception("[App](abort) add_service error: ", e) + return cls + + @classmethod + def exec(cls): + """ + 启动App + """ + if None == cls.__ui: + raise Exception("[App](abort) exec interrupt, UI is null.") + try: + # start ui + cls.__ui.start() + + import lvgl as lv + lv.task_handler() + + # start services + for service in App.__service_list: + service.load_before() + service.load() + service.load_after() + except Exception as e: + print("[App] exec error: ", e) + ``` + +## UML 类图 ### 服务模块 @@ -141,63 +892,7 @@ class AbstractLoad(object): ### 界面模块 -`ui.py` 定义了一个 `Screen` 基类,该基类被定义的 UI 界面继承,用于约束 UI 界面的接口定义,其定义如下: - -```python -class Screen(AbstractLoad): - class Type(): - Init = "init" - Normal = "normal" # 默认 - MenuBar = "menubar" - ToolBar = "toolbar" - StatusBar = "statusbar" - - def __init__(self): - self.meta = None # lvgl meta object - self.meta_info = {} - self.last_screen = None - - def set_last_screen(self, name): - self.last_screen = name - - def load_before(self): - pass - - def load(self): - pass - - def load_after(self): - pass - - def instance_after(self): - pass - - def deactivate(self): - pass - - def key2_once_click(self): - pass - - def key2_double_click(self): - pass - - def key2_long_press(self): - pass - - def prev_idx(self, now_idx, count): - cur_idx = now_idx - 1 - if cur_idx < 0: - cur_idx = count - 1 - return cur_idx - - def next_idx(self, now_idx, count): - cur_idx = now_idx + 1 - if cur_idx > count - 1: - cur_idx = 0 - return cur_idx -``` - -在 `ui.py` 中,还定义了多个 UI 界面,如: +在 `ui.py` 中,定义了多个 UI 界面,如: 1. `PocUI`:主 UI,提供 `MenuBar`、`PromptBox` 和 `Screen` 的管理以及按键事件的响应处理 2. `MenuBar`:菜单栏(用于显示网络状态、时间、电量以及其他图标,一直显示在屏幕上方,大小为 240×20) @@ -209,58 +904,3 @@ class Screen(AbstractLoad): ![](../../media/solutions/poc/ui_uml.png) > 如用户需添加 `Screen`,可参考已有 `Screen` 样式进行添加,并添加到 `poc_main.py` 中对应的位置即可。 - -### 群组管理 - -在 `ui.py` 的 `GroupScreen` 里面实现了群组管理的功能,在页面加载之前通过 `EventMap` 事件管理,向 `Services` 发送对应事件,获取当前账号所加入的所有群组,并通过列表将其显示出来。 - -```python -class GroupScreen(Screen): - def load_before(self): - EventMap.bind("update_group_info", self.__update_group_info) # 更新获取群组信息 - - def load(self): - self.__load_group_list() # 判断是否获取到组群列表 - self.__group_screen_list_create() # 创建群组界面列表 - self.__load_group_cur() -``` - -### APP管理 - -`poc_main.py` 中使用一个 `APP` 类进行管理,用户添加或修改如按键、消息框和服务等操作,调用相应的函数添加即可。 - -```python -#=== 1.添加按键 === -App.add_key(KeyManger()) - -#=== 2.添加主UI === -App.set_ui(PocUI()) - -#=== 3.添加屏幕栏 === -App.add_bar(MenuBar()) - -#=== 4.添加消息框 === -App.add_msgbox(PromptBox()) - -#=== 5.添加UI屏幕 === -App.add_screen( MenuBar()) \ - .add_screen( MainScreen()) \ - .add_screen( WelcomeScreen() ) \ - .add_screen( PromptBox() ) \ - .add_screen( MemberScreen() ) \ - .add_screen( GroupScreen() ) \ - .add_screen( SettingScreen() ) \ - .add_screen( DeviceScreen() ) \ - .add_screen( ICCIDScreen() ) \ - .add_screen( IMEIScreen()) \ - .add_screen( FirmwareScreen() ) - -#=== 6.添加服务 === -App.add_service( NetService()) \ - .add_service( PocService()) \ - .add_service( MediaService()) \ - .add_service( DevInfoService() ) - -#=== 7.运行App === -App.exec() -```