Хабрахабр

Micropython на GSM+GPS модуле A9G

На рынке есть масса автономных устройств для слежения за автомобилями, грузом, велосипедами, багажом, детьми и животными. В этот раз я задумался о том, чтобы спрятать в велосипед GPS-трэкер в качестве меры предосторожности. Более дорогие варианты предоставляют функциональность Find my phone, но привязаны к конкретному онлайн-сервису.
В идеале хотелось бы иметь полный контроль над трекером: использовать его в удобном режиме без СМС и регистрации. Подавляющее большинство из них взаимодействуют с пользователем с помощью СМС. Поверхностное гугление вывело меня на пару модулей из поднебесной, один из которых, A9G pudding board, я и заказал (~15$).

Модуль

Эта статья о том, как я заставил работать python на этом модуле.

Зато есть много другого интересного. Если A9G — аналог ESP (производитель, кстати, один и тот же), то сам pudding board является аналогом платы NodeMCU за исключением того, что на pudding board нет встроенного конвертера USB-UART. Спецификации от производителя:

  • ядро 32 bit (RISC), до 312MHz
  • 29x GPIO (все распаяны, в это число включены все интерфейсы)
  • часы и watchdog
  • 1x интерфейс USB 1.1 (я его там не нашел, но копирую с офсайта) и microUSB для питания
  • 2x UART (+1 сервисный)
  • 2x SPI (не пробовал)
  • 3x I2C (не пробовал)
  • 1x SDMMC (с физическим слотом)
  • 2x аналоговых входа (10 бит, возможно, один из них используется контроллеров литиевых аккумуляторов)
  • 4Mb flash
  • 4Mb PSRAM
  • ADC (микрофон, физически существует на плате) и DAC (динамик, отсутствует)
  • контроллер заряда аккумулятора (самого аккумулятора нет)
  • собственно, GSM (800, 900, 1800, 1900 MHz) с SMS, голосом и GPRS
  • GPS, подключенный через UART2 (есть модуль "A9" без него)
  • слот для SIM (nanoSIM)
  • две кнопки (одна reset, другая — включение и программируемая функция)
  • два светодиода

3В, входное напряжение — 5-3. Рабочее напряжение 3. Вообще, модуль имеет всё необходимое железо для того, чтобы собрать из него простенький кнопочный мобильный аппарат. 8В (в зависимости от подключения). Альтернативами модулю являются довольно популярные модули SIM800, у которых, к сожалению, нет SDK в свободном доступе (т.е. Но из примеров создаётся впечатление, что китайцы его покупают для продажи из автоматов или автоматов с азартными играми или что-то вроде этого. модули продаются как AT модемы).

Устанавливается под Ubuntu, но предпочтительными являются Windows и контейнеры. К модулю прилагается SDK на удовлетворительном английском. Сама прошивка собирается Makefile-ом. Всё работает через тыкание в GUI: ESPtool для этого модуля только предстоит зареверсить. Но лично я так и не смог перевести адреса в строчки кода (gdb сообщает, что адреса ничему не соответствуют). Дебаггер наличествует: прежде чем зависнуть, модуль вываливает stack trace в сервисный порт. Соответственно, если хотите повозиться с модулем — попробуйте это сделать под Windows (и отписаться на github). Вполне возможно, что это связано с плохой поддержкой Linux как такового. После установки нужно проверить правильность путей в .bashrc и удалить (переименовать) все файлы CSDTK/lib/libQt*: иначе, прошивальщик (он же дебаггер) просто не запустится из-за конфликта с, вероятно, установленным libQt. В противном случае вот инструкция для Linux.

Прошивальщик

К прошивальщику идёт инструкция.

Модули выглядят похоже, но на pudding board нет USB-TTY чипа и microUSB используется только для питания. Тут всё сложнее, чем, на NodeMCU. 3V. Соответственно, вам понадобится USB-TTY на 3. Чтобы не тащить все эти сопли к компьютеру я дополнительно приобрел USB разветвитель на 4 порта с двухметровым проводом и внешним блоком питания (обязателен). А лучше — два: один для дебаг порта и ещё один для UART1: первый используется для заливки прошивки а второй вы сможете использовать как обычный терминал. Суммарная стоимость этого набора с самим модулем составит 25-30$ (без блока питания: используйте от телефона).

3В ардуине и использовать в качестве модема через UART1. Модуль приходит с AT прошивкой: можно подключить к 3. make создает два файла прошивки: один шьётся около минуты, другой — достаточно быстро. Свои прошивки пишутся на C. Суммарно, у меня в процессе разработки на рабочем столе открыта китайская SDK (coolwatcher) для управления модулем, miniterm в качестве stdio и редактор кода. Шить можно только один из этих файлов: первый раз — большой, последующие разы — маленький.

К сожалению, набор функций, доступных пользователю, весьма ограничен: к примеру, нет доступа к телефонной книге на SIM-карте, низкоуровневой информации о подключении к сотовой сети и тому прочее. Содержание API отражает список наверху и напоминает ESP8266 в свои ранние дни: у меня ушло часа 3 на то, чтобы запустить HelloWorld. Тем не менее, модуль может очень многое вплоть до SSL-подключений: очевидно, производитель сфокусировался на наиболее приоритетных функциях. Документация по API ещё менее полная, поэтому опираться приходится на примеры (которых два десятка) и include-файлы.

Для всех остальных производитель начал портировать micropython на этот модуль. Впрочем, программирование китайских микроконтроллеров посредством китайского API надо любить. Я решил попробовать себя в open-source проекте и продолжить это доброе дело (ссылка в конце статьи).

logo

Разработка ведётся в двух направлениях. Micropython — это open-source проект портирующий cPython на микроконтроллеры. Второе — это, собственно, порты: для каждого микроконтроллера необходимо "научить" библиотеку работать с UART для ввода-вывода, выделить стэк под виртуальную машину, указать набор оптимизаций. Первое — это поддержка и развитие общих для всех микроконтроллеров core-библиотек, описывающих работу с основными типами данных в python: объекты, функции, классы, строки, атомарные типы и тому прочее. В дополнение к этому, полностью поддерживаются самописные модули на питоне (главное — не забывать об объёме памяти). Опционально, описывается работа с железом: GPIO, питание, беспроводная связь, файловая система.
Всё это пишется на чистых С с макросами: у micropython есть набор рекомендованных рецептов начиная с объявления строк в ROM до написания модулей. В качестве рекламы: проект продаёт собственную плату для студентов pyboard, но также популярны порты для модулей ESP8266 и ESP32. Кураторы проекта ставят целью возможность запустить джангу (картинка с буханкой хлеба).

Когда прошивка готова и залита — вы просто подключаетесь к микроконтроллеру через UART и попадаете в питонский 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, не забывая об ограничениях памяти.

Тем не менее, производитель железа форкнул micropython и создал окружение для порта A9G: micropython/ports/gprs_a9, за что ему большое спасибо. Модуль A9G не поддерживается официально (список официально поддерживаемых модулей доступен в micropython/ports, их около десятка). Но, к сожалению, из сторонних модулей присутствовала только работа с файловой системой и GPIO: ничего, связанного с беспроводной сетью и GPS доступно не было. На момент, когда я заинтересовался этим вопросом, порт успешно компилировался и микроконтроллер приветствовал меня REPL. Официальная документация на этот случай излишне лаконична: поэтому, пришлось ковыряться в коде. Я решил исправить эту недоработку и поставил себе цель портировать все функции, необходимые для GPS-трекера.

С чего начать

Затем, редактируем main.c под вашу платформу. Первым делом идём в micropython/ports и копируем micropython/ports/minimal в новую папку, в которой будет находится порт. Потом, для event-driven API, необходимо вызвать pyexec_event_repl_init() и скармливать вводимые через UART символы в функцию pyexec_event_repl_process_char(char). Имейте ввиду, что вся вкуснятина находится в функции main, где нужно вызвать инициализатор mp_init(), предварительно подготовив для него настройки микроконтроллера и стэк. Второй файл — micropython/ports/minimal/uart_core.c описывает блокирующий ввод и вывод в UART. Это и обеспечит взаимодействие через REPL. Привожу оригинальный код для STM32 для тех, кому лень искать.

main.c

int main(int argc, char **argv) } #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
}

Всё, этого в идеале должно хватить: собираем, заливаем прошивку и видим REPL в UART.
После оживления micropython необходимо позаботиться о его хорошем самочувствии: настроить сборщик мусора, правильную реакцию на Ctrl-D (soft reset) и некоторые другие вещи, на которых я не буду останавливаться: см. После этого нужно переписать Makefile используя рекомендации / компилятор от производителя: тут всё индивидуально. файл 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: её необходимо добавить в переменную MICROPY_PORT_BUILTIN_MODULES в настройках порта mpconfigport.h. У модуля будет доступен только один атрибут __name__ с именем модуля (отличный случай для проверки автодополнения в REPL через Tab). После всех этих изменений можно попытаться скомпилировать модуль и импортировать его из REPL.

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

Константы

Константы часто необходимы для настроек (INPUT, OUTPUT, HIGH, LOW и т.п.) Тут всё достаточно просто. Следующий этап по сложности — добавление констант. Вот, к примеру, константа 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'>

Данные добавляются "рядом" с конструктором: предлагается создать отдельную структуру, в которой будет хранится отдельно питонский тип и отдельно — произвольный набор данных. Получившийся тип можно наследовать, сравнивать, но у него нет ни конструктора ни каких-либо ассоциированных данных.

// Произвольная над-структура. Да, с именами путаница
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.

Вот неплохой пример из micropython/ports/esp32/modsocket.c: Но make_new вовсе не обязателен для получения экземпляра объекта: инициализацию можно производить в произвольной функции.

// Другая сигнатура функции: количество аргументов и указатель на аргументы
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);

Привязанные методы (bound methods)

Впрочем, это мало чем отличается от всех остальных методов. Следующий этап — добавление привязанных методов. Возвращаемся к примеру из документации:

// Ещё один пример сигнатуры: количество аргументов строго равно 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()

Все остальные атрибуты: getattr, setattr

Пожалуйста: это делается вручную в обход mymodule_hello_locals_dict_table. Как насчёт добавления не-функций, использования @property и вообще собственного __getattr__?

// Функция со специфической сигнатурой ...
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,
};

Где же все эти mp_raise_AttributeError (прим: такая функция не существует)? Что-то больно лаконичный attr получился, скажете вы. Секрет в том, что dest — это массив из двух элементов. На самом деле, AttributeError будет вызван автоматически. Соответственно, на выходе из функции ожидается MP_OBJ_NULL в первом случае и что-то mp_obj_t во втором. Первый элемент имеет смысл "вывода", write-only: он принимает значение MP_OBJ_SENTINEL если значение необходимо записать и MP_OBJ_NULL если его нужно прочитать. Менять его не надо. Второй элемент — "ввод", read-only: принимает значение объекта для записи, если значение необходимо записать и MP_OBJ_NULL, если его необходимо прочитать.

Вот и всё, можно проверять:

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

Я, если честно, никак не эксперт в C, поэтому могу только предполагать как это происходит (переопределением оператора '=='). Самое интересное — что автодополнение по Таb в REPL по-прежнему работает и предлагает .val!

Порт

Теперь можно залить что-то вроде этого на модуль и оно будет работать: Возвращаясь к модулю A9G, я описал поддержку всех основных функций, а именно, SMS, GPRS (usockets), 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()

Если вам понравился проект и/или эта статья — не забудьте оставить лайк на гитхабе. Проект приветствует любую посильную помощь.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть