Micropython sur module GSM + GPS A9G

Cette fois, j'ai pensé à cacher un tracker GPS dans mon vélo par précaution. Il existe des tonnes d'appareils autonomes sur le marché pour suivre les voitures, les marchandises, les vélos, les bagages, les enfants et les animaux. La grande majorité d'entre eux interagissent avec l'utilisateur via SMS. Des options plus coûteuses offrent la fonctionnalité Localiser mon téléphone, mais sont liées à un service en ligne spécifique.
Idéalement, je voudrais avoir un contrôle total sur le tracker: utilisez-le dans un mode pratique sans SMS ni inscription. Google superficiel m'a apporté quelques modules en provenance de Chine, dont un que j'ai commandé (carte de pudding A9G) (~ 15 $).


Module


Cet article explique comment j'ai fait fonctionner python sur ce module.


Si l'A9G est un analogue de l'ESP (le fabricant est d'ailleurs le même), la carte de pudding elle-même est un analogue de la carte NodeMCU, sauf que la carte de pudding n'a pas de convertisseur USB-UART intégré. Mais il y a beaucoup d'autres choses intéressantes. Spécifications du fabricant :


  • Noyau 32 bits (RISC), jusqu'à 312 MHz
  • 29x GPIO (tous sont soudés, toutes les interfaces sont incluses dans ce numéro)
  • montres et chien de garde
  • 1x interface USB 1.1 (je ne l'ai pas trouvée là-bas, mais copie hors site) et microUSB pour l'alimentation
  • 2x UART (+1 service)
  • 2x SPI (non essayé)
  • 3x I2C (non essayé)
  • 1x SDMMC (avec emplacement physique)
  • 2x entrées analogiques (10 bits, dont l'une est utilisée par les contrôleurs de batterie au lithium)
  • Flash 4 Mo
  • 4 Mo de PSRAM
  • ADC (microphone, existe physiquement sur la carte) et DAC (haut-parleur, absent)
  • contrôleur de charge de la batterie (il n'y a pas de batterie elle-même)
  • en fait, GSM (800, 900, 1800, 1900 MHz) avec SMS, voix et GPRS
  • GPS connecté via UART2 (il y a un module «A9» sans)
  • Emplacement SIM (nanoSIM)
  • deux boutons (un reset, l'autre - inclusion et fonction programmable)
  • deux LED

La tension de fonctionnement est de 3,3 V, la tension d'entrée est de 5 à 3,8 V (selon la connexion). En général, le module dispose de tout le matériel nécessaire pour en assembler un simple appareil mobile à bouton poussoir. Mais d'après les exemples, il semble que les Chinois l'achètent pour la vente à partir de machines à sous ou de machines à sous ou quelque chose comme ça. Les alternatives au module sont des modules SIM800 très populaires, qui, malheureusement, n'ont pas de SDK dans le domaine public (c'est-à-dire que les modules sont vendus comme modems AT).


SDK


Le module est livré avec un SDK dans un anglais satisfaisant. S'installe sous Ubuntu, mais Windows et les conteneurs sont préférés. Tout fonctionne par piquer dans l'interface graphique: ESPtool pour ce module n'a pas encore été annulé. Le firmware lui-même est construit par le Makefile. Le débogueur est présent: avant le gel, le module jette la trace de pile dans le port de service. Mais personnellement, je n'ai pas pu traduire les adresses en lignes de code (gdb rapporte que les adresses ne correspondent à rien). Il est possible que cela soit dû à une mauvaise prise en charge de Linux en tant que tel. En conséquence, si vous voulez bricoler avec le module - essayez de le faire sous Windows (et désabonnez-vous sur github). Sinon, voici les instructions pour Linux. Après l'installation, vous devez vérifier l'exactitude des chemins d'accès dans .bashrc et supprimer (renommer) tous les CSDTK/lib/libQt* : sinon, le clignotant (aka débogueur) ne démarre tout simplement pas en raison d'un conflit avec, probablement, installé libQt.


Flasher


Pour le clignotant, il y a une instruction .


Connexion


Tout est plus compliqué que sur NodeMCU. Les modules se ressemblent, mais il n'y a pas de puce USB-TTY sur la carte de pudding et microUSB n'est utilisé que pour l'alimentation. En conséquence, vous aurez besoin d'un port USB-TTY à 3,3 V. Deux sont meilleurs: un pour le port de débogage et un pour UART1: le premier est utilisé pour télécharger le firmware et le second que vous pouvez utiliser comme terminal normal. Afin de ne pas faire glisser toutes ces morve sur l'ordinateur, j'ai également acheté un séparateur USB à 4 ports avec un câble de deux mètres et une alimentation externe (requise). Le coût total de ce kit avec le module lui-même sera de 25-30 $ (sans alimentation: utilisation depuis le téléphone).


Firmware


Le module est livré avec le firmware AT: vous pouvez vous connecter à un Arduino 3.3V et l'utiliser comme modem via UART1. Leur firmware est écrit en C. make crée deux fichiers de firmware: l'un est cousu pendant environ une minute, l'autre est assez rapide. Un seul de ces fichiers peut être cousu: la première fois est grande, les heures suivantes sont petites. Au total, pendant le processus de développement, j'ai le SDK chinois ( coolwatcher ) ouvert sur le bureau pour gérer le module, le miniterm comme stdio et l'éditeur de code.


API


Le contenu de l' API reflète la liste ci-dessus et ressemble à l'ESP8266 à ses débuts: il m'a fallu environ 3 heures pour lancer HelloWorld. Malheureusement, l'ensemble des fonctions disponibles pour l'utilisateur est très limité: par exemple, il n'y a pas d'accès au répertoire téléphonique sur la carte SIM, des informations de bas niveau sur la connexion au réseau cellulaire, etc. La documentation de l'API est encore moins complète, vous devez donc vous fier à des exemples (dont il en existe deux douzaines) et inclure des fichiers. Néanmoins, le module peut faire beaucoup de choses jusqu'aux connexions SSL: évidemment, le constructeur s'est concentré sur les fonctions les plus prioritaires.


Cependant, la programmation des microcontrôleurs chinois via l'API chinoise doit être appréciée. Pour tout le monde, le fabricant a commencé à porter le micropython sur ce module. J'ai décidé de m'essayer dans un projet open-source et de poursuivre ce bon travail (lien en fin d'article).


micropython


logo


Micropython est un projet open-source portant cPython sur des microcontrôleurs. Le développement s'effectue dans deux directions. Le premier est le support et le développement de bibliothèques de base communes à tous les microcontrôleurs qui décrivent le travail avec les principaux types de données en python: objets, fonctions, classes, chaînes, types atomiques, etc. Le second est, en fait, les ports: pour chaque microcontrôleur il faut «apprendre» à la bibliothèque à travailler avec UART pour les entrées-sorties, sélectionner une pile pour une machine virtuelle, spécifier un ensemble d'optimisations. En option, le travail avec le matériel est décrit: GPIO, alimentation, sans fil, système de fichiers.
Tout cela est écrit en C pur avec des macros: micropython a un ensemble de recettes recommandées de la déclaration de chaînes dans la ROM aux modules d'écriture. En plus de cela, les modules auto-écrits python sont entièrement pris en charge (l'essentiel est de ne pas oublier la taille de la mémoire). Les conservateurs du projet se sont donné comme objectif de lancer un dzhanga (photo avec une miche de pain). Comme publicité: le projet vend sa propre carte pour les étudiants pyboard , mais les ports pour les modules ESP8266 et ESP32 sont également populaires.


Lorsque le firmware est prêt et téléchargé - il vous suffit de vous connecter au microcontrôleur via UART et d'accéder au 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 

Après cela, vous pouvez commencer à écrire en python3 presque normal sans oublier les limitations de mémoire.


Le module A9G n'est pas officiellement pris en charge (une liste des modules officiellement pris en charge est disponible dans micropython/ports , il y en a environ une douzaine). Néanmoins, le fabricant de fer a bifurqué le micropython et a créé l'environnement pour le port micropython/ports/gprs_a9 : micropython/ports/gprs_a9 , pour lequel merci beaucoup à lui. Au moment où je me suis intéressé à ce problème, le port a été compilé avec succès et le microcontrôleur m'a accueilli avec REPL. Mais, malheureusement, à partir de modules tiers, il n'y avait que du travail avec le système de fichiers et GPIO: rien lié au réseau sans fil et au GPS n'était disponible. J'ai décidé de corriger ce défaut et me suis fixé comme objectif de porter toutes les fonctions nécessaires à un tracker GPS. La documentation officielle de cette affaire est inutilement concise: j'ai donc dû fouiller dans le code.


Par où commencer


Tout d'abord, accédez à micropython/ports et copiez micropython/ports/minimal dans le nouveau dossier où le port sera situé. Ensuite, modifiez main.c pour votre plateforme. Gardez à l'esprit que tout le délicieux est dans la fonction main , où vous devez appeler l'initialiseur mp_init() , après avoir préparé le microcontrôleur et les paramètres de pile pour cela. Ensuite, pour l'API événementielle, vous devez appeler pyexec_event_repl_init() et pyexec_event_repl_init() les caractères saisis via UART à la fonction pyexec_event_repl_process_char(char) . Cela fournira l'interopérabilité via REPL. Le deuxième fichier, micropython/ports/minimal/uart_core.c décrit le blocage des entrées et des sorties dans UART. J'apporte le code original pour STM32 pour ceux qui sont trop paresseux pour chercher.


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 } 

Après cela, vous devez réécrire le Makefile en utilisant les recommandations / compilateur du fabricant: tout est individuel ici. Tout, idéalement, cela devrait suffire: nous collectons, remplissons le firmware et voyons REPL dans UART.
Après avoir relancé le micropython vous devez prendre soin de son bien-être: configurer le garbage collector, la bonne réaction à Ctrl-D (soft reset) et quelques autres choses sur lesquelles je ne m'attarderai pas: voir le fichier mpconfigport.h .


Créer un module


La chose la plus intéressante est d'écrire vos propres modules. Ainsi, le module (non nécessaire, mais souhaitable) commence par son propre fichier mod[].c , qui est ajouté par le Makefile (variable SRC_C si vous suivez la convention). Un module vide est le suivant:


 // 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, }; 

Bien sûr, le port lui-même ne reconnaît pas la constante mp_module_mymodule : elle doit être ajoutée à la variable MICROPY_PORT_BUILTIN_MODULES dans les paramètres de port de mpconfigport.h . Au fait fonds d'écran ennuyeux le nom de la puce et le nom du port y changent également. Après toutes ces modifications, vous pouvez essayer de compiler le module et de l'importer depuis REPL. Un seul attribut __name__ avec le nom du module sera disponible pour le module (un excellent cas pour vérifier l'auto-complétion dans REPL via Tab).


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

Constantes


L'étape suivante de la complexité consiste à ajouter des constantes. Les constantes sont souvent nécessaires pour les réglages ( INPUT , OUTPUT , HIGH , LOW , etc.) Tout est assez simple ici. Ici, par exemple, la constante 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) }, }; 

Test:


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

Les fonctions


L'ajout d'une fonction à un module suit le principe général: déclarer, encapsuler, ajouter (je donne un exemple un peu plus complexe que dans la documentation).


 //  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 }, }; 

Test:


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

Classes (types)


Avec les classes (types), tout est aussi relativement simple. Voici un exemple tiré de la documentation (enfin, presque):


 //     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 }, }; 

Test:


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

Le type résultant peut être hérité, comparé, mais il n'a pas de constructeur ni de données associées. Les données sont ajoutées "à côté" du constructeur: il est proposé de créer une structure distincte dans laquelle le type Python sera stocké séparément et séparément - un ensemble de données arbitraires.


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

Comment interagir avec ces données? L'une des voies les plus difficiles passe par le constructeur.


 // -,   (,  ,   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, }; 

Parmi les autres domaines, il y a aussi .print , et je suppose que le reste de la magie de Python3 .


Mais make_new n'est pas du tout nécessaire pour obtenir une instance d'un objet: l'initialisation peut se faire dans une fonction arbitraire. Voici un bon exemple de 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); 

Méthodes liées


L'étape suivante consiste à ajouter les méthodes liées. Cependant, ce n'est pas très différent de toutes les autres méthodes. Nous revenons à l'exemple de la documentation:


 //    :     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 }, } 

C’est tout!


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

Tous les autres attributs: getattr , setattr


Que diriez-vous d'ajouter des non-fonctions, d'utiliser @property et généralement votre propre __getattr__ ? S'il vous plaît: cela se fait manuellement en contournant 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, }; 

Quelque chose de douloureusement concis s'est avéré, dites-vous. Où sont tous ces mp_raise_AttributeError ( note : une telle fonction n'existe pas)? En fait, une AttributeError sera appelée automatiquement. Le secret est que dest est un tableau de deux éléments. Le premier élément a la signification de "sortie", en écriture seule: il prend la valeur MP_OBJ_SENTINEL si la valeur doit être écrite et MP_OBJ_NULL si elle doit être lue. En conséquence, à la sortie de la fonction, MP_OBJ_NULL est attendu dans le premier cas et quelque chose mp_obj_t dans le second. Le deuxième élément est entrée, en lecture seule: prend la valeur de l'objet à écrire si la valeur doit être écrite et MP_OBJ_NULL si elle doit être lue. Vous n'avez pas besoin de le changer.


C'est tout, vous pouvez vérifier:


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

Le plus intéressant est que l'achèvement des onglets dans REPL fonctionne toujours et offre .val ! Pour être honnête, je ne suis pas un expert en C, donc je ne peux que deviner comment cela se produit (en redéfinissant l'opérateur '==').


Port


Revenant au module A9G, j'ai décrit le support de toutes les fonctions de base, à savoir, SMS, GPRS (usockets), GPS, gestion de l'alimentation. Maintenant, vous pouvez télécharger quelque chose comme ça dans le module et cela fonctionnera:


 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() 

Le projet accueille toute aide possible. Si vous avez aimé le projet et / ou cet article, n'oubliez pas de laisser un like sur le github .

Source: https://habr.com/ru/post/fr446090/


All Articles