GSM + GPS模块A9G上的Micropython

这次,我考虑过将GPS追踪器藏在自行车中作为预防措施。 市场上有许多用于跟踪汽车,货物,自行车,行李,儿童和动物的自动装置。 他们中的绝大多数通过短信与用户互动。 更昂贵的选项提供“查找我的电话”功能,但与特定的在线服务相关。
理想情况下,我想完全控制跟踪器:在没有SMS和注册的便捷模式下使用它。 肤浅的Google给我带来了几个来自中国的模块,我订购了其中的一个模块(A9G布丁板)(〜15美元)。


模组


本文是关于我如何使python在此模块上工作的。


如果A9G是ESP的类似物(顺便说一句,制造商是相同的),那么布丁板本身就是NodeMCU板的类似物,只是布丁板没有内置的USB-UART转换器。 但是还有许多其他有趣的事情。 制造商规格:


  • 32位核心(RISC),最高312MHz
  • 29个GPIO(均已焊接,所有接口均包含在此编号中)
  • 手表和看门狗
  • 1个USB 1.1接口(我在那里找不到,但是可以从异地复制)和microUSB供电
  • 2个UART(+1服务)
  • 2x SPI(未尝试)
  • 3x I2C(未尝试)
  • 1个SDMMC(带有物理插槽)
  • 2个模拟输入(10位,锂电池控制器可能使用其中之一)
  • 4Mb闪存
  • 4Mb PSRAM
  • ADC(麦克风,物理存在于板上)和DAC(扬声器,不存在)
  • 电池充电控制器(本身没有电池)
  • 实际上,带有SMS,语音和GPRS的GSM(800、900、1800、1900 MHz)
  • GPS通过UART2连接(没有“ A9”模块)
  • SIM卡插槽(nanoSIM)
  • 两个按钮(一个复位,另一个-包含和可编程功能)
  • 两个LED

工作电压为3.3V,输入电压为5-3.8V(取决于连接)。 通常,该模块具有所有必需的硬件,以便根据其组装简单的按钮式移动设备。 但是从这些例子中,似乎中国人正在从老虎机或老虎机或类似的东西上购买它来出售。 该模块的替代品是相当流行的SIM800模块,不幸的是,该模块在公共领域中没有SDK(即,这些模块作为AT调制解调器出售)。


开发包


该模块随附了令人满意的英语SDK 。 在Ubuntu下安装,但是Windows和容器是首选。 一切都可以通过在GUI中戳来完成:此模块的ESPtool尚未还原。 固件本身由Makefile构建。 存在调试器:冻结之前,模块将堆栈跟踪抛出到服务端口中。 但就我个人而言,我无法将地址转换为代码行(gdb报告该地址与任何内容都不对应)。 可能是由于对Linux这样的支持不佳所致。 因此,如果您想修改模块-尝试在Windows下进行(并在github上退订)。 否则,这是Linux的说明 。 安装后,您需要检查.bashrc中路径的正确性,并删除(重命名)所有CSDTK/lib/libQt* :否则,由于可能与已安装的libQt发生冲突,刷新程序(即调试器)将无法启动。


闪光器


闪光灯上有一条指示


连接方式


一切都比在NodeMCU上复杂。 这些模块看起来很相似,但是布丁板上没有USB-TTY芯片,microUSB仅用于供电。 因此,您将需要3.3V的USB-TTY。 两种比较好:一种用于调试端口,一种用于UART1:第一种用于上传固件,第二种可以用作常规终端。 为了不将所有这些鼻涕拖到计算机上,我还购买了带两米电缆和外部电源的4端口USB分配器(必需)。 该套件与模块本身的总成本为25-30美元(不带电源:可通过电话使用)。


韧体


该模块随附AT固件:您可以连接到3.3V arduino,并通过UART1将其用作调制解调器。 他们的固件使用C语言编写。make创建了两个固件文件:一个缝制了大约一分钟,另一个缝制得足够快。 只能缝制以下文件中的一个:第一次较大,以后较小。 总的来说,在开发过程中,我在桌面上打开了中文SDK( coolwatcher ),用于管理模块,miniterm(作为stdio和代码编辑器)。


API


API的内容反映了上面的列表,类似于ESP8266的早期版本:我花了大约3个小时来启动HelloWorld。 不幸的是,用户可用的功能集非常有限:例如,无法访问SIM卡上的电话簿,有关连接到蜂窝网络的低级信息等等。 API文档甚至还不够完善,因此您必须依靠示例(其中有两个打样)并包含文件。 不过,该模块可以完成许多工作,直至SSL连接:显然,制造商专注于最优先的功能。


但是,必须喜欢通过中文API进行中文微控制器的编程。 对于其他所有人,制造商开始将 micropython移植到此模块。 我决定尝试一个开源项目,并继续进行这项出色的工作(本文结尾处的链接)。


微型蟒蛇


徽标


Micropython是一个将cPython移植到微控制器的开源项目。 开发是从两个方向进行的。 首先是对所有微控制器通用的核心库的支持和开发,这些库描述了如何使用python中的主要数据类型:对象,函数,类,字符串,原子类型等。 第二个实际上是端口:对于每个微控制器,有必要“教”该库以使用UART进行输入输出,为虚拟机选择堆栈,指定一组优化。 可选地,描述了与硬件一起使用:GPIO,电源,无线,文件系统。
所有这些都是用带有宏的纯C语言编写的:micropython有一组建议的配方,从在ROM中声明字符串到编写模块。 除此之外,还完全支持python自写模块(主要是不要忘记内存大小)。 该项目的策展人将发布dzhanga (带有面包的图片)的机会作为他们的目标。 作为广告:该项目为pyboard学生出售自己的开发板,但ESP8266和ESP32模块的端口也很受欢迎。


准备好固件并上载后-您只需通过UART连接到微控制器,然后进入Python REPL。


 $ miniterm.py /dev/ttyUSB1 115200 --raw MicroPython cd2f742 on 2017-11-29; unicorn with Cortex-M3 Type "help()" for more information. >>> print("hello") hello 

之后,您可以开始使用几乎普通的python3编写代码而不必担心内存限制。


A9G模块不受官方支持( micropython/ports提供了官方支持的模块列表,其中大约有十二个)。 尽管如此,钢铁制造商分叉了micropython并为A9G端口创建了环境: micropython/ports/gprs_a9 ,为此,他表示了很多谢意。 当我对这个问题感兴趣时,该端口已成功编译,微控制器向我致敬REPL。 但是,不幸的是,在第三方模块中,只能使用文件系统和GPIO:没有与无线网络和GPS相关的内容。 我决定修复此缺陷,并为自己设定了移植GPS跟踪器所需的所有功能的目标。 案例的官方文档过于简洁:因此,我不得不在代码中四处张望。


从哪里开始


首先,转到micropython/ports然后将micropython/ports/minimal复制到端口将位于的新文件夹中。 然后,为您的平台编辑main.c 请记住,所有好吃的东西都在main函数中,您需要在其中调用mp_init()初始化器,之前已mp_init()准备了微控制器和堆栈设置。 然后,对于事件驱动的API,您需要调用pyexec_event_repl_init()并将通过UART输入的字符提供给pyexec_event_repl_process_char(char)函数。 这将通过REPL提供互操作性。 第二个文件是micropython/ports/minimal/uart_core.c描述了阻止UART中的输入和输出。 我为那些懒惰的人带来了STM32的原始代码。


main.c


 int main(int argc, char **argv) { int stack_dummy; stack_top = (char*)&stack_dummy; #if MICROPY_ENABLE_GC gc_init(heap, heap + sizeof(heap)); #endif mp_init(); #if MICROPY_ENABLE_COMPILER #if MICROPY_REPL_EVENT_DRIVEN pyexec_event_repl_init(); for (;;) { int c = mp_hal_stdin_rx_chr(); if (pyexec_event_repl_process_char(c)) { break; } } #else pyexec_friendly_repl(); #endif //do_str("print('hello world!', list(x+1 for x in range(10)), end='eol\\n')", MP_PARSE_SINGLE_INPUT); //do_str("for i in range(10):\r\n print(i)", MP_PARSE_FILE_INPUT); #else pyexec_frozen_module("frozentest.py"); #endif mp_deinit(); return 0; } 

uart_core.c


 // Receive single character int mp_hal_stdin_rx_chr(void) { unsigned char c = 0; #if MICROPY_MIN_USE_STDOUT int r = read(0, &c, 1); (void)r; #elif MICROPY_MIN_USE_STM32_MCU // wait for RXNE while ((USART1->SR & (1 << 5)) == 0) { } c = USART1->DR; #endif return c; } // Send string of given length void mp_hal_stdout_tx_strn(const char *str, mp_uint_t len) { #if MICROPY_MIN_USE_STDOUT int r = write(1, str, len); (void)r; #elif MICROPY_MIN_USE_STM32_MCU while (len--) { // wait for TXE while ((USART1->SR & (1 << 7)) == 0) { } USART1->DR = *str++; } #endif } 

之后,您需要使用制造商的建议/编译器重写Makefile:此处的所有操作都是单独的。 理想情况下,所有一切都应该足够:我们收集,填写固件,然后在UART中查看REPL。
恢复micropython您需要照顾好它的健康:设置垃圾收集器,对Ctrl-D的正确反应(软复位)以及其他我不愿mpconfigport.h其他事项:请参阅mpconfigport.h文件。


创建一个模块


最有趣的是编写自己的模块。 因此,模块(不是必需的,但可取的)以其自己的mod[].c文件开头,该文件由Makefile添加(如果遵循约定, SRC_C变量)。 空模块如下:


 // nlr - non-local return:  C  ,      goto-  . //  nlr_raise             . #include "py/nlr.h" //   .  ,  mp_map_elem_t,  ,   . #include "py/obj.h" //   . mp_raise_ValueError(char* msg)  mp_raise_OSError(int errorcode)   . //  ,   mp_call_function_*     Callable (  callback-). #include "py/runtime.h" #include "py/binary.h" //  header   :       #include "portmodules.h" //    --  .     MP_QSTR_[ ]. MP_OBJ_NEW_QSTR   . //             RAM. //      -      __name__ STATIC const mp_map_elem_t mymodule_globals_table[] = { { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_mymodule) }, }; //      STATIC MP_DEFINE_CONST_DICT (mp_module_mymodule_globals, mymodule_globals_table); //   :             const mp_obj_module_t mp_module_mymodule = { .base = { &mp_type_module }, .globals = (mp_obj_dict_t*)&mp_module_mymodule_globals, }; 

当然,端口本身无法识别mp_module_mymodule常数:必须将其添加到mpconfigport.h端口mpconfigport.hMICROPY_PORT_BUILTIN_MODULES变量中。 顺便说一句 无聊的壁纸 芯片名称和端口名称也在此处更改。 完成所有这些更改后,您可以尝试编译模块并从REPL导入它。 只有一个具有模块名称的__name__属性可用于该模块(这是通过Tab使用REPL检查自动完成的一种好方法)。


 >>> import mymodule >>> mymodule.__name__ 'mymodule' 

常数


复杂性的下一个阶段是添加常量。 设置通常需要常量( INPUTOUTPUTHIGHLOW等)。这里的一切都很简单。 例如,这里的常数magic_number = 10


 STATIC const mp_map_elem_t mymodule_globals_table[] = { { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_mymodule) }, { MP_OBJ_NEW_QSTR(MP_QSTR_magic_number), MP_OBJ_NEW_SMALL_INT(10) }, }; 

测试:


 >>> import mymodule >>> mymodule.magic_number 10 

功能介绍


向模块添加功能遵循以下一般原则:声明,包装,添加(我给出的示例比文档中的示例稍微复杂一些)。


 //  STATIC mp_obj_t conditional_add_one(mp_obj_t value) { //   int.         -  :   . int value_int = mp_obj_get_int(value); value_int ++; if (value_int == 10) { //  None return mp_const_none; } //   int return mp_obj_new_int(value); } //    .     // runtime.h   . STATIC MP_DEFINE_CONST_FUN_OBJ_1(conditional_add_one_obj, conditional_add_one); //  STATIC const mp_map_elem_t mymodule_globals_table[] = { { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_mymodule) }, { MP_OBJ_NEW_QSTR(MP_QSTR_magic_number), MP_OBJ_NEW_SMALL_INT(10) }, { MP_OBJ_NEW_QSTR(MP_QSTR_conditional_add_one), (mp_obj_t)&conditional_add_one_obj }, }; 

测试:


 >>> import mymodule >>> mymodule.conditional_add_one(3) 4 >>> mymodule.conditional_add_one(9) >>> 

类(类型)


使用类(类型),一切也都相对简单。 这是文档中的一个示例(差不多):


 //     STATIC const mp_map_elem_t mymodule_hello_locals_dict_table[] = {}; //   STATIC MP_DEFINE_CONST_DICT(mymodule_hello_locals_dict, mymodule_hello_locals_dict_table); // ,  ,   const mp_obj_type_t mymodule_helloObj_type = { //    { &mp_type_type }, // : helloObj .name = MP_QSTR_helloObj, //  .locals_dict = (mp_obj_dict_t*)&mymodule_hello_locals_dict, }; //    STATIC const mp_map_elem_t mymodule_globals_table[] = { { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_mymodule) }, { MP_OBJ_NEW_QSTR(MP_QSTR_magic_number), MP_OBJ_NEW_SMALL_INT(10) }, { MP_OBJ_NEW_QSTR(MP_QSTR_conditional_add_one), (mp_obj_t)&conditional_add_one_obj }, { MP_OBJ_NEW_QSTR(MP_QSTR_conditional_add_one), (mp_obj_t)&mymodule_helloObj_type }, }; 

测试:


 >>> mymodule.helloObj <type 'helloObj'> 

可以继承,比较结果类型,但是它没有构造函数或任何关联数据。 数据被添加到构造函数的“旁边”:建议创建一个单独的结构,在该结构中Python类型将被单独存储,即一个任意数据集。


 //  -. ,    typedef struct _mymodule_hello_obj_t { //   mp_obj_base_t base; // -  uint8_t hello_number; } mymodule_hello_obj_t; 

如何与这些数据进行交互? 最困难的方法之一是通过构造函数。


 // -,   (,  ,   mymodule_helloObj_type //   ,     - ),   (args  kwargs)  //        : args, kwargs STATIC mp_obj_t mymodule_hello_make_new( const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args ) { //    mp_arg_check_num(n_args, n_kw, 1, 1, true); //   mymodule_hello_obj_t *self = m_new_obj(mymodule_hello_obj_t); //     self->base.type = &mymodule_hello_type; //   self->hello_number = mp_obj_get_int(args[0]) //   return MP_OBJ_FROM_PTR(self); //    __init__, ,  } //      make_new const mp_obj_type_t mymodule_helloObj_type = { { &mp_type_type }, .name = MP_QSTR_helloObj, .locals_dict = (mp_obj_dict_t*)&mymodule_hello_locals_dict, //  .make_new = mymodule_hello_make_new, }; 

在其他领域中,还有.print ,我猜想Python3的其他魔力。


但是获取一个对象的实例根本不需要make_new :初始化可以在任意函数中完成。 这是来自micropython/ports/esp32/modsocket.c一个好例子:


 //   :       STATIC mp_obj_t get_socket(size_t n_args, const mp_obj_t *args) { socket_obj_t *sock = m_new_obj_with_finaliser(socket_obj_t); sock->base.type = &socket_type; sock->domain = AF_INET; sock->type = SOCK_STREAM; sock->proto = 0; sock->peer_closed = false; if (n_args > 0) { sock->domain = mp_obj_get_int(args[0]); if (n_args > 1) { sock->type = mp_obj_get_int(args[1]); if (n_args > 2) { sock->proto = mp_obj_get_int(args[2]); } } } sock->fd = lwip_socket(sock->domain, sock->type, sock->proto); if (sock->fd < 0) { exception_from_errno(errno); } _socket_settimeout(sock, UINT64_MAX); return MP_OBJ_FROM_PTR(sock); } //     0-3  STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(get_socket_obj, 0, 3, get_socket); 

绑定方法


下一步是添加绑定方法。 但是,这与所有其他方法并没有太大区别。 我们从文档中返回示例:


 //    :     1 (self) STATIC mp_obj_t mymodule_hello_increment(mp_obj_t self_in) { mymodule_hello_obj_t *self = MP_OBJ_TO_PTR(self_in); self->hello_number += 1; return mp_const_none; } //     MP_DEFINE_CONST_FUN_OBJ_1(mymodule_hello_increment_obj, mymodule_hello_increment); //      'inc' STATIC const mp_map_elem_t mymodule_hello_locals_dict_table[] = { { MP_OBJ_NEW_QSTR(MP_QSTR_inc), (mp_obj_t)&mymodule_hello_increment_obj }, } 

仅此而已!


 >>> x = mymodule.helloObj(12) >>> x.inc() 

所有其他属性: getattrsetattr


如何使用@property和通常使用自己的__getattr__添加非功能? 请:通过绕过mymodule_hello_locals_dict_table手动完成。


 //     ... STATIC void mymodule_hello_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { mymodule_hello_obj_t *self = MP_OBJ_TO_PTR(self_in); if (dest[0] != MP_OBJ_NULL) { // __setattr__ if (attr == MP_QSTR_val) { self->val = dest[1]; dest[0] = MP_OBJ_NULL; } } else { // __getattr__ if (attr == MP_QSTR_val) { dest[0] = self->val; } } } // ...     attr const mp_obj_type_t mymodule_helloObj_type = { { &mp_type_type }, .name = MP_QSTR_helloObj, //     //.locals_dict = (mp_obj_dict_t*)&mymodule_hello_locals_dict, .make_new = mymodule_hello_make_new, //   - attr .attr = mymodule_hello_attr, }; 

您说,简明扼要的attr结果出乎意料。 这些mp_raise_AttributeError都在哪里( 注意 :这样的函数不存在)? 实际上,将自动调用AttributeError 。 秘密在于dest是两个元素的数组。 第一个元素的含义是“输出”,仅写:如果需要写入该值,则取值MP_OBJ_NULL如果需要读取,则MP_OBJ_NULL 。 因此,在函数退出时,在第一种情况下期望mp_obj_t ,在第二种情况下期望mp_obj_t 。 输入的第二个元素是只读的:如果需要写入该值,则取要写入的对象的值;如果需要读取该值,则取MP_OBJ_NULL 。 您不需要更改它。


仅此而已,您可以检查:


 >>> x = mymodule.helloObj(12) >>> x.val = 3 >>> x.val 3 

最有趣的是,REPL中的制表符.val仍然可以使用并提供.val ! 老实说,我不是C语言方面的专家,所以我只能猜测这是怎么发生的(通过重新定义运算符'==')。


港口


回到A9G模块,我描述了对所有基本功能支持,即SMS,GPRS(电子信号),GPS,电源管理。 现在,您可以将类似这样的内容上传到模块,并且可以正常工作:


 import cellular as c import usocket as sock import time import gps import machine #   print("Waiting network registration ...") while not c.is_network_registered(): time.sleep(1) time.sleep(2) #  GPRS print("Activating ...") c.gprs_activate("internet", "", "") print("Local IP:", sock.get_local_ip()) #  GPS gps.on() #    thingspeak host = "api.thingspeak.com" api_key = "some-api-key" fields = ('latitude', 'longitude', 'battery', 'sat_visible', 'sat_tracked') #  ,      ! fields = dict(zip(fields, map(lambda x: "field{}".format(x+1), range(len(fields))) )) x, y = gps.get_location() level = machine.get_input_voltage()[1] sats_vis, sats_tracked = gps.get_satellites() s = sock.socket() print("Connecting ...") s.connect((host, 80)) print("Sending ...") #      ,     HTTP.           HTTP, SSL   print("Sent:", s.send("GET /update?api_key={}&{latitude}={:f}&{longitude}={:f}&{battery}={:f}&{sat_visible}={:d}&{sat_tracked}={:d} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n".format( api_key, x, y, level, sats_vis, sats_tracked, host, **fields ))) print("Receiving ...") print("Received:", s.recv(128)) s.close() 

该项目欢迎任何可行的帮助。 如果您喜欢该项目和/或本文,请不要忘记在github上留下一个

Source: https://habr.com/ru/post/zh-CN446090/


All Articles