搜索
bottom↓
回复: 0
打印 上一主题 下一主题

《ESP32-S3使用指南—MicroPython版 V1.0》第七章 MicroPython组件扩展

[复制链接]

出0入234汤圆

跳转到指定楼层
1
发表于 2024-8-16 15:09:46 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 正点原子 于 2024-8-16 15:09 编辑


1)实验平台:正点原子ESP32S3开发板
2)购买链接:https://detail.tmall.com/item.htm?id=768499342659
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-347618-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子手把手教你学ESP32S3快速入门视频教程:https://www.bilibili.com/video/BV1sH4y1W7Tc
6)正点原子FPGA交流群:132780729



第七章 MicroPython组件扩展


       使用原生的MicroPython进行开发时,可能会感觉束手束脚,就像在狭窄的小路上行走,前方有着各种陡峭的山坡和深邃的沟渠。有时,你会觉得官方提供的功能不足以满足你的需求,或者你发现这些功能并不能完全符合你的工作场景。这时,你就可以选择亲手打造自己的 C 模块,将其融入MicroPython中,仿佛在狭窄的小路上开辟出一条自己的道路,前方是开阔的平原和明亮的阳光。你可以按照自己的想法和需求,设计和实现适合自己的Python函数调用,让它们像你手中的利剑一样,挥舞在代码的世界中。
       本章分为如下几个小节:
       7.1组件扩展原理
       7.2 组件扩展辅助工具
       7.3 正点原子的扩展组件
       7.4 添加C模块驱动

       7.1 组件扩展原理

       7.1.1 组件扩展方式
       很多人会疑惑,C语言与Python是两种不同的语言,MicroPython如何调用C语言实现的函数在Python下调用的呢?这个问题关键在于,如何使用C语言的形式在MicroPython源代码中表述函数的入参与出参,比如Python实现一个A变量与B变量相加函数,它的实现代码如下:
  1. def add(a, b):
  2.     return a + b
复制代码
       这个函数有两个入参和一个返回参数,此时如果使用C语言表示该函数的输入输出参数,就可以使用C函数对接到MicroPython当中。在我们讲解C模块原理之前,我们先了解MicroPython C模块的组件扩展方式。
       MicroPython C模块有两种组件扩展方式,它们分别为模块扩展,模块+类扩展,这两种形式有什么区别呢?下面作者使用一个示意图来讲解。

图7.1.1.1 组件扩展方式

       从上图可知,在main.py文件下可调用两种类型的Python引擎,它们分别为模块扩展,模块+类扩展,下面作者详细说明这两种组件扩展有何区别,如下所示:

       ①:模块扩展:在这种方式下,扩展组件以模块的形式提供,用户在使用时直接导入模块即可。这种方式比较简单,只需要在模块中定义所需的函数和变量。模块扩展调用示例如下所示:
  1. import cexample
  2. cexample.cppfunc()        # 调用模块的方法
复制代码
       这种Python引擎类似于在Python环境下新建一个cexample.py脚本,然后,在脚本下利用“de cppfunc”方式(Python定义函数的流程)定义模块的方法,接着,在main.py脚本下导入cexample模块,最后,引导这个模块的方法(函数)。

       ②:模块+类扩展:在这种方式下,扩展组件通过模块和类的组合进行扩展。用户在使用时需要通过模块导入特定的类,然后使用这个类提供的功能。这种方式相对于模块扩展更加灵活,可以更好地组织代码和提供面向对象的功能。模块+类扩展调用示例如下所示:
  1. from cexample import Timer
  2. tr = Timer()    # 实例化Timer类对象
  3. tr. time()      # 调用Timet对象的方法
复制代码
       这种Python引擎类似于在Python环境下新建一个cexample.py脚本。然后,在脚本下定义一个Timer类对象,在这个类中定义属性与方法,接着,在main.py脚本下引入cexample模块中的Timer类,并实例化Timer类对象,最后,使用实例化Timer对象调用该类的方法与属性。
       这两种扩展方式的主要区别在于代码组织和使用方式上。模块扩展方式更简单直接,适合提供一些基础的功能和函数。而模块+类扩展方式更加灵活,可以更好地组织和封装代码,提供更高级的功能和接口。需要注意的是,这两种扩展方式并不是互斥的,可以根据需要同时使用。例如,可以在一个扩展模块中同时使用模块扩展和模块+类扩展方式,以提供更加丰富和灵活的功能。

       7.1.2 组件扩展原理解析
       到了这里,我们已经了解了C模块组件的扩展方式。接下来,将重点讲解MicroPython提供的三个C模块实例,以便用户将来编写自己的C模块组件。这些实例位于micropython\examples\usercmodule目录下,它们分别是cexample、cppexample和subpackage C模块。
       下面,就以cexample C模块为例。首先,打开micropython\examples\usercmodule\cexample目录。该目录包含三个文件,它们共同构成了一个简单的MicroPython C模块。如下图所示。

图7.1.2.1 cexample文件夹目录

       上图中的micropython.mk是一个包含此模块的Makefile片段的脚本文件。其中,USERMOD_DIR可用作micropython.mk模块目录的路径。在Makefile中,它应该被扩展为本地make变量,例如:CEXAMPLE_MOD_DIR:=( USERMOD_DIR)。同时,需要将模块源文件添加到SRC_USERMOD的扩展副本中,例如:SRC_USERMOD += $(CEXAMPLE_MOD_DIR)/examplemodule.c。如果存在自定义的“CFLAGS”设置或包括文件夹来定义,这些应该添加到“CFLAGS_USERMOD”中。
  1. # USERMOD_DIR模块目录的路径,如cexample/
  2. CEXAMPLE_MOD_DIR := $(USERMOD_DIR)

  3. # 将所有C文件添加到 SRC_USERMOD
  4. SRC_USERMOD += $(CEXAMPLE_MOD_DIR)/examplemodule.c

  5. # 如果有自定义编译器选项(例如 -I 添加目录以搜索头文件),
  6. # 则应将这些选项添加到C代码的CFLAGS_USERMOD和C++代码的CXXFLAGS_USERMOD
  7. CFLAGS_USERMOD += -I$(CEXAMPLE_MOD_DIR)
复制代码
       在上面的源码中,我们看到了一个链接命令,它链接了cexample对象文件,最终生成了一个的共享库。这个共享库可以被MicroPython解释器加载和使用。总的来说,micropython.mk对于MicroPython C模块的作用主要是定义构建规则和编译选项,以用于构建和编译C模块。
       micropython.cmake是一个CMake配置文件,用于构建MicroPython模块。它包含了构建模块所需的CMake配置指令,例如定义库、添加源文件、设置编译选项等。通过使用CMake,可以方便地构建和管理MicroPython模块的构建过程。如下代码所示:
  1. # 创建一个INTERFACE库
  2. add_library(usermod_cexample INTERFACE)

  3. # 源文件添加到库中
  4. target_sources(usermod_cexample INTERFACE
  5.     ${CMAKE_CURRENT_LIST_DIR}/examplemodule.c
  6. )

  7. # 将当前目录添加为包含目录
  8. target_include_directories(usermod_cexample INTERFACE
  9.     ${CMAKE_CURRENT_LIST_DIR}
  10. )

  11. # 将我们的INTERFACE库链接到usermod目标
  12. target_link_libraries(usermod INTERFACE usermod_cexample)
复制代码
       在CMake配置文件micropython.cmake中,需要定义一个INTERFACE库并将源文件关联起来,然后将其链接到usermod目标。该文件对于MicroPython C模块的作用是定义CMake配置,以确保C模块能够正确地编译和链接成可执行文件或库,并能够在MicroPython环境中正确地运行。
       examplemodule.c文件使用了模块和模块+类两种组件扩展方式。下面,作者分别来讲解这两种方式的实现原理。

       1,模块扩展实现原理
       examplemodule.c文件模块扩展部分代码如下所示:
  1. /* 第一部分:添加所需API的头文件 */
  2. #include "py/runtime.h"
  3. #include "py/mphal.h"

  4. /* 第二部分:实现功能 */
  5. STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
  6.     /* 通过 Python 获取的第一个整形参数 arg_1 */
  7.     int a = mp_obj_get_int(a_obj);
  8.     /* 通过 Python 获取的第二个整形参数 arg_2 */
  9.     int b = mp_obj_get_int(b_obj);

  10.     /* 处理入参 arg_1 和 arg_2,并向 python 返回整形参数 ret_val */
  11.     return mp_obj_new_int(a + b);
  12. }
  13. /* 使用MP_DEFINE_CONST_FUN_OBJ_2宏将函数添加到模块中 */
  14. STATIC MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints);

  15. /* 第三部分:将模块注册到模块列表 */
  16. STATIC const mp_rom_map_elem_t example_module_globals_table[] = {
  17.     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_cexample) },
  18.     { MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&example_add_ints_obj) },
  19. };
  20. /* 把模块注册列表注册到example_module_globals字典对象当中 */
  21. STATIC MP_DEFINE_CONST_DICT(example_module_globals,
  22.                             example_module_globals_table);
  23. /* 第四部分:定义模块对象 */
  24. const mp_obj_module_t example_user_cmodule = {
  25.     .base = { &mp_type_module },
  26.     .globals = (mp_obj_dict_t *)&example_module_globals,/* 指向字典对象 */
  27. };

  28. /* 第五部分:注册模块以使其在Python中可用 */
  29. MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
复制代码
       上述是examplemodule.c文件的部分代码,它主要阐述了简单MicroPython C模块的模块扩展架构,首先作者把它这个架构划分为四个部分,如下解析:

       第一部分:在.c文件下添加microPython相关头文件,可用来引用相关的API函数。

       第二部分:加法函数,使用C语言的方式实现功能,但需要使用MicroPython API函数对入参和出参进行转换,入参转换完成后,才能让C语言识别,出参转换成功后才能被MicroPython调用。最后在MP_DEFINE_CONST_FUN_OBJ_2宏的作用下,将函数添加到模块中,以便在 MicroPython 环境中调用。

       第三部分:将模块注册到模块列表,然后在MP_DEFINE_CONST_DICT宏的作用下在C 语言中创建一个常量字典,并将其添加到 MicroPython 模块中,以便在 Python 环境中直接访问和使用这些字典。这对于需要在 MicroPython 模块中定义和共享常量字典的情况非常有用。大家不妨回顾一下字典的作用,它通过键来访问字典中的值(这个值可用来指向某个函数的地址,这样我们根据这个地址调用函数了)。

       第四部分:创建一个模块对象,然后对象的成员变量globals指向字典对象,接着在MP_REGISTER_MODULE宏的作用下将模块注册到 MicroPython 系统中,使其可以在 Python 环境中被导入和使用。

       上述组件扩展方式是模块扩展,我们可在Py脚本下使用模块扩展的形式调用加法函数,如下示例所示:

  1. # 导入模块
  2. import cexample


  3. """
  4. * @brief       程序入口
  5. * @param       无
  6. * @retval      无
  7. """
  8. if __name__ == "__main__":

  9.     a = 5
  10.     b = 10
  11.     c = cexample.add_ints(a,b)  # 调用自定义模块的加法函数
  12.     print(c)                    # 输出C为15
复制代码
       这种模块扩展类似于自己定义一个cexample.py文件,然后,在此文件下实现多个函数,最后,在main.py文件下导入cexample模块,并以模块名引用函数。

       2,模块+类扩展原理
       examplemodule.c文件模块+类扩展部分代码如下所示:
  1. /* 添加所需API的头文件 */
  2. #include "py/runtime.h"
  3. #include "py/mphal.h"


  4. /* (2)定义Timer对象实例 */
  5. typedef struct _example_Timer_obj_t
  6. {
  7.     /* 所有对象的基础地址 */
  8.     mp_obj_base_t base;
  9.     /* 开始时间的变量 */
  10.     mp_uint_t start_time;
  11. } example_Timer_obj_t;

  12. /* (3)对象的实现方法(函数) */
  13. STATIC mp_obj_t example_Timer_time(mp_obj_t self_in)
  14. {
  15.     /* 获取Timer句柄 */
  16.     example_Timer_obj_t *self = MP_OBJ_TO_PTR(self_in);

  17.     /* 获取经过的时间,并将其作为MicroPython整数返回 */
  18.     mp_uint_t elapsed = mp_hal_ticks_ms() - self->start_time;
  19.     return mp_obj_new_int_from_uint(elapsed);
  20. }
  21. /* 使用MP_DEFINE_CONST_FUN_OBJ_1宏将函数添加到模块中 */
  22. STATIC MP_DEFINE_CONST_FUN_OBJ_1(example_Timer_time_obj, example_Timer_time);

  23. /* 构造函数 */
  24. STATIC mp_obj_t example_Timer_make_new(const mp_obj_type_t *type, size_t n_args,
  25.                                        size_t n_kw, const mp_obj_t *args) {
  26.     /* 分配新对象并设置类型 */
  27.     example_Timer_obj_t *self = mp_obj_malloc(example_Timer_obj_t, type);

  28.     /* 获取开始时间 */
  29.     self->start_time = mp_hal_ticks_ms();

  30.     /* 返回Timer句柄 */
  31.     return MP_OBJ_FROM_PTR(self);
  32. }

  33. /* 将模块注册到对象的模块列表 */
  34. STATIC const mp_rom_map_elem_t example_Timer_locals_dict_table[] = {
  35.     { MP_ROM_QSTR(MP_QSTR_time), MP_ROM_PTR(&example_Timer_time_obj) },
  36. };
  37. /* 把字典转换成对象 */
  38. STATIC MP_DEFINE_CONST_DICT(example_Timer_locals_dict,
  39.                             example_Timer_locals_dict_table);

  40. /* (1)定义了Timer类 */
  41. MP_DEFINE_CONST_OBJ_TYPE(
  42.     example_type_Timer,
  43.     MP_QSTR_Timer,
  44.     MP_TYPE_FLAG_NONE,
  45.     make_new, example_Timer_make_new,
  46.     locals_dict, &example_Timer_locals_dict
  47.     );
  48.    
  49. /* 将模块注册到模块列表 */
  50. STATIC const mp_rom_map_elem_t example_module_globals_table[] = {
  51.     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_cexample) },
  52.     { MP_ROM_QSTR(MP_QSTR_Timer),    MP_ROM_PTR(&example_type_Timer) },
  53. };
  54. /* 把模块注册列表注册到example_module_globals字典对象当中 */
  55. STATIC MP_DEFINE_CONST_DICT(example_module_globals,
  56.                             example_module_globals_table);
  57. /* 定义模块对象 */
  58. const mp_obj_module_t example_user_cmodule = {
  59.     .base = { &mp_type_module },
  60.     .globals = (mp_obj_dict_t *)&example_module_globals,/* 指向字典对象 */
  61. };

  62. /* 注册模块以使其在Python中可用 */
  63. MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
复制代码
       上述代码也不难理解,我们知道Python语言是面向对象的语言。正因为如此,在Python中创建一个类和对象是很容易的。类是用来描述具有相同的属性(变量)和方法(函数)的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。明白了这点,我们可以去看上述的代码到底怎么实现一个Python类的。
       (1)它定义了一个名为“Timer”的类,这个类包含了一个对实例对象的操作example_Timer_make_new,以及一个类的方法(函数)example_Timer_locals_dict;(2)处定义了Timer对象实例,它用来获取开始的运行时间;(3)处就是类的实现方法example_Timer_time即运行的函数。如下是模组+类扩展调用示例,如下代码所示:
  1. from cexample import Timer


  2. """
  3. * @brief       程序入口
  4. * @param       无
  5. * @retval      无
  6. """
  7. if __name__ == "__main__":

  8.     tr = Timer()                # 对象实例(调用example_Timer_make_new函数)
  9.     timer = tr. time()          # 调用对象的方法example_Timer_time获取运行时间
  10.     print(timer)                # 打印运行的时间
复制代码
       从上述源代码可以看出,C模块的模块+类扩展实现方式类似于在定义一个cexample.py文件,并在该文件中实现一个Timer类,然后在该类中实现计算运行时间的方法,最后在main.py文件中导入cexample模块下的Timer类,并使用类名来引用该函数。

       7.2 组件扩展辅助工具
       R.T-Thread提供的MicroPython C绑定代码自动生成器是一个非常有用的工具,可以帮助开发者快速将C语言函数或模块集成到MicroPython环境中。通过这个工具,开发者可以轻松添加自己编写的C语言函数或者模块到MicroPython中,并被Python脚本调用。这大大扩展了原版MicroPython的能力,并且可以快速实现任何功能。
       使用这个工具,开发者只需要简单几步操作,即可实现添加C绑定的功能。自动生成的C代码形式可以在R.T-Thread的官方文档中找到。这个工具已经经过多次迭代,变得越来越完善,可以轻松加入到工程中。C 绑定代码自动生成器如下所示:

图7.2.1 RTT提供的MicroPython C绑定代码自动生成器

       这个辅助工具很简单,只需填写要实现的函数名、传入的参数和函数的返回值类型,即可生成一个MicroPython函数模板。当然,核心代码需要开发者自行编写。

       7.3 正点原子的扩展组件
       上述章节中,作者简单阐述了MicroPython组件扩展的原理及编写流程,还介绍了R.T-Thread提供的MicroPython C绑定代码自动生成器。通过这个工具,开发者可以轻松添加自己编写的C语言函数或者模块到MicroPython中。下面作者介绍正点原子提供的扩展组件有哪些,如下表所示:

表7.3.1 正点原子提供MicroPython 扩展组件功能描述

       在前面的讨论中,我们已经了解到,当MicroPython官方实现的功能无法满足开发者的需求时,或者当这些实现不符合工作需求时,可以使用扩展组件的方式设计开发者的功能。为了满足正点原子ESP32-S3开发板的特定外设以及让读者更好地熟悉C模块,正点原子制定了一套自己的扩展组件,以适应开发板的需求。这些扩展组件是根据板载器件的特点和需求而设计的。
       在前面的章节中,作者已经介绍了扩展组件的架构,并阐述了实现原理。正点原子提供的扩展组件代码也是基于这个框架进行编写的。因此,在这里作者不再重复讲解。这些扩展组件已经放置在资料盘中,有兴趣的读者可以参考借鉴。

       7.4 添加C模块驱动
       在上小节中,作者简单讲解了正点原子MicroPython扩展组件,那么我们怎么把这些组件编译到MicroPython固件当中呢?为了让读者更好地熟悉这个流程,作者以流程的方式来讲述。


       1,把扩展组件复制到MicroPython源代码
       正点原子的扩展组件在A盘6,软件资料1,软件2,MicroPython开发工具01-Windows2,正点原子MicroPython驱动CModules_Lib路径下找到,这些组件如下图所示。

图7.4.1 正点原子MicroPython扩展组件

       在上图中,我们复制CAMERA、ESP-WHO、IIC、LCD、RGBLCD、SENSOR文件夹至D:\Ubuntu_WSL\rootfs\root\micropython\examples\usercmodule\BSP目录下,如下图所示。

图7.4.2 添加扩展组件(BSP文件夹需用户创建)

       2,修改usercmodule目录下的micropython.cmake
       修改micropython.cmake文件,添加编译扩展组件,如下代码所示。
  1. # This top-level micropython.cmake is responsible for listing
  2. # the individual modules we want to include.
  3. # Paths are absolute, and ${CMAKE_CURRENT_LIST_DIR} can be
  4. # used to prefix subdirectories.

  5. # Add the C example.
  6. #include(${CMAKE_CURRENT_LIST_DIR}/cexample/micropython.cmake)

  7. # Add the CPP example.
  8. #include(${CMAKE_CURRENT_LIST_DIR}/cppexample/micropython.cmake)

  9. # 添加 IIC驱动
  10. include(${CMAKE_CURRENT_LIST_DIR}/BSP/IIC/micropython.cmake)

  11. # 添加 CAMERA驱动
  12. include(${CMAKE_CURRENT_LIST_DIR}/BSP/CAMERA/micropython.cmake)

  13. # 添加 SPI LCD驱动
  14. include(${CMAKE_CURRENT_LIST_DIR}/BSP/LCD/micropython.cmake)

  15. # 添加ESP_WHO乐鑫AI驱动
  16. #include(${CMAKE_CURRENT_LIST_DIR}/BSP/ESP-WHO/micropython.cmake)

  17. # 添加 SENSOR驱动
  18. include(${CMAKE_CURRENT_LIST_DIR}/BSP/SENSOR/micropython.cmake)

  19. # 添加 RGBLCD驱动
  20. include(${CMAKE_CURRENT_LIST_DIR}/BSP/RGBLCD/micropython.cmake)
复制代码
       请注意,当用户添加正点原子扩展C模块组件时,有一个重要的点需要关注:ESP_WHO和LCD扩展组件不能同时编译。这是因为ESP_WHO已经包含了LCD驱动,所以这两者不能同时使用。考虑到ESP_WHO编译出来的固件体积较大,作者提供了两种固件:一种是未使用AI库的固件,另一种是包含AI库的固件。

       3,添加乐鑫摄像头驱动库和乐鑫AI库
       前面我们知道,MicroPython固件是从ESP-IDF库的基础上编译出来的,由于正点原子ESP32-S3开发板板载了摄像头接口及又想实现AI检测,所以MicroPython固件需编译ESP-IDF摄像头驱动库和AI库。这两个库,开发者可在Github仓库搜索找到。如下图所示:


图7.4.3 摄像头驱动库和AI库

       这两个库下载完成之后,把它们移植到子系统esp-idf库的components目录中,如下图所示。

图7.4.4 添加驱动库

       值得注意的是,作者对上图的modules文件夹进行了删减。读者可以对比原始的modules文件,在esp-dl/include/路径下,添加esp_timer.h文件,以防止编译报错。该文件可以在esp-idf/components/esp_timer/include/路径下找到。
       上图的五个文件夹已经放置在A盘6,软件资料1,软件2,MicroPython开发工具01-Windows3,摄像头与AI库目录下,开发者可直接解压并拷贝到自己的子系统esp-idf/components目录下。

       3,编译固件
       首先,打开Ubuntu 22.04.2 LTS子系统,然后使用cd命令进入micropython/ports/esp32/目录,如下图所示。

图7.4.5 进入esp32编译目录

       然后在此目录下输入“make USER_C_MODULES=~/micropython/examples/usercmodule/micropython.cmake BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT”命令编译固件。编译成功后如下图所示。

图7.4.6 添加C模块固件编译完成

       4,测试C模块
       把固件下载至开发板,下载流程请看前面的章节。下载完成后,导入C模块驱动库,如下图所示。

图7.4.7 测试C模块是否导入成功

       从上图可以看到,在Thonny软件Shell交互环境下调用正点原子C模块组件并没有提示错误,证明这些C模块的方法及变量可在程序中使用了。

阿莫论坛20周年了!感谢大家的支持与爱护!!

曾经有一段真挚的爱情摆在我的面前,我没有珍惜,现在想起来,还好我没有珍惜……
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片。注意:要连续压缩2次才能满足要求!!】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-8-25 07:15

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表