Все уходят на Rust: в ML c Python, в бэкенде с Java, в микроконтроллерах c С/С++

Привет, Хабр! Сегодня заглянем на онлайн-митап от MWS. Посмотрим, как в трех разных кейсах переходили на Rust.
При работе с ML перешли с Python на Rust для синтеза и распознавания речи, ускорив работу сервисов. На другом проекте перешли с Java на Rust — в итоге стало меньше инцидентов, стала быстрее выкатка фич и разгрузили железо. И изюминкой будет рассказ про использование Rust вместо C/C++ в микроконтроллерах с игрой в динозаврика из Google Chrome в реальности.
Кейс № 1. Rust для ML вместо Python
Кейс № 2. Rust на проекте вместо Java
Кейс № 3. На микроконтроллере крутится Rust
MWS Meetup — серия технических встреч для разработчиков и инженеров, которые проходят онлайн и офлайн. На мероприятиях специалисты компании и приглашенные эксперты разбирают практические задачи из разработки, инфраструктуры и облачных технологий.
До этого митапа не знал, что у MWS есть свои открытые онлайн-митапы. Это был 10-й митап, и в нем разбирали кейсы с Rust. Как раз этот язык начал изучать в этом году, поэтому пришел послушать — мне было интересно, как он применяется в реальных проектах. После митапа решил, что новый проект буду делать на Rust, отказавшись от Python и Go. А еще захотелось купить микросхемы, радиодетали и попрограммировать свои устройства.
Кейс № 1. Rust для ML вместо Python
Артур Казарян выступил с докладом «Ускоряем инференс ML с Rust». Он рассказал про платформу синтеза и распознавания речи, в которой используется интерференс-сервер Triton, работающий на видеокартах NVidia

Triton поддерживает разные бэкенд-серверы, и только один Python-бэкенд, который поддерживает предобработку и постобработку. На других языках нет такого решения.
Рассмотрим ансамблевую модель ONNX для классификации текста.
Модель работает только с цифрами, поэтому нужна предобработка, превращающая текст или картинку в цифры, и постобработка, преобразующая цифры в текст. В этой схеме можно упереться в производительность модели или в производительность Python на предобработке и постобработке. Во втором случае и понадобилось кастомное решение на Rust.

Для бесшовной работы пришлось сделать так, чтобы никто из работающих с сервером не догадывался, что он написан на Rust, а не на Python. Чтобы этого добиться, подключились к header-файлам Triton, написанными на C++. На Rust есть библиотека bindgen, которая генерирует файл bindings.rs, с которыми уже можно работать на Rust.

После компиляции получается один .so-файл, который Triton ищет в трех местах:
/mnt/model/1/libtriton_model.so (зависимые модели, входящие в линковку)
/mnt/model/libtriton_model.so (исходный код конкретной модели)
/opt/tritonserver/backends/model/libtriton_model.so (bindings.rs, сгенерированный bindgen)
И этот файл отдаем на интеграцию — все очень просто.
Теперь переключимся на то, как устроен Rust-бэкенд:

В качестве примера — создание модели на Rust. В настройках прописываем путь к rust-бэкенду, а файл model.rs — главный файл модели.

При переходе от Python к Rust важно, чтобы все типы данных совпадали для обеспечения совместимости, этому уделили отдельное внимание:

Далее нам показали в сравнении три основных функции — initialize, execute, finalize.
На Python initialize() выглядел так:
def initialize(self, args):
self.tokenizer = (
AutoTokenizer.from_pretrained(
“/models/…/bert_tokenizer”
)
)На Rust теперь выглядит так:
fn initialize(config: HashMap<&str, &str>)
-> Result<Self, Error> {
let tokenizer = Tokenizer::form_file(
repo.join(“/models/…/bert_tokenizer/tokenizer.json”)
)?;
Ok(Self { tokenizer })
}Код основной логики на Python выглядел так:
decoded = self.tokenizer.batch_decode(input_)
response = np.array(decoded, dtype=np.object_)
output_tensor_transcr = pb_utils.Tensor(“OUTPUT”, response)
interence_response = pb_utils.InterenceResponse(
output_tensors=[output_tensor_transcr]
)
responses.append(inference_response)Тот же код на Rust стал выглядеть так:
match self.tokenizer.decode_batch(&batch_slices, true) {
Ok(decoded) => {
let mut output_data: Vec<u8> = vec![];
for row in decoded.iter() {
let encoded: Vec<u8> = rust_backend::utils::encode_string(&row);
output_data.extend(encoded);
}
response.set_output(
“OUTPUT”,
&output_data,
vec![batch_size, 1],
TritonserverDataTipe::Bytes,
TritonserverDataTipe::Cpu
)?;
}И напоследок выгрузка модели, в которой ничего не происходит
На Python:
def finalize(self):
passНа Rust:
fn finalize(&self) -> Result<(), Error> {
Ok(())
}Теперь переходим к сравнению производительности Python и Rust. Нужно сравнить производительность предобработки и постобработки, поэтому исключим саму модель и получился 10-кратный рост производительности. Но после добавления ONYX-модели разница уже не так заметна: порядка 20%. В этом случае нужно уже думать об оптимизации интерференс-модели.

Далее перешли к конкретным кейсам, в котором оптимизировали саму модель:

После показа кейсов перешли к тому, после чего вообще принимать решение о необходимости перехода с Python на Rust? Для этого смотрим состав ML-моделей на сервере, спамим запросы к ним, чтобы выяснить, на что сколько времени уходит. Если работа ML-модели занимает больше 90% времени, то нет смысла оптимизировать предобработку и постобработку, так как прирост будет несущественным. Затем нужно смотреть на уровень выше: в первую очередь оптимизировать медленные сервера.
Плюсы в сохранении архитектуры, экономии ресурсов, неизменности API.
Минусы в том, что ускорение не гарантировано, экосистема и компетенций меньше.

Кейс № 2. Rust на проекте вместо Java
Следующим выступал типлид Дмитрий Кузьмин с презентацией «Как мы снизили стоимость продукта после миграции части стека на Rust» на системе медиации и предбиллинга.

Пришли к необходимости переписывания legacy на другой язык, чтобы снизить постоянные затраты и негатив клиентов. И все это в критической системе с цепной реакцией при падении и невозможностью отката. Прямые затраты — это 3–8 часов в неделю на разбор инцидентов, пятничные падения с ночными дежурствами и работой в выходные. Чем дольше идет разбор — тем больше тратится денег клиентов и больше их недовольство.
Команда из пяти разработчиков, 20+ инфраструктурных сервисов с нагрузкой в 200 000 RPS по всей системе и 10–20 Тб трафика в сутки. Стек до изменений был в основном Java, а также Shell, Perl, PHP, Go. Рассматривали несколько вариантов: оставить как есть, языки с Garbage Collector, Rust. При анализе оказалось, что часть базовых вещей Rust решают часть эксплуатационных задач и задач по разработке.
Система критическая, поэтому делали постепенную миграцию в рамках параллельно запущенного пилота в течение четырех недель с затратами в 130–160 человеко-дней. Что по расчетам приводило к окупаемости меньше чем за месяц.
При этом старое решение продолжало работать, и при возникновении проблем на проде могли сразу переключиться на него. Также в новом решение добавили инструменты, которые отсутствовали в старом решении: инструменты межсервисного взаимодействия, трейсинг, расширенного логинования.
Использовали:
clippy + rustimt + sonarqube: ревью дешевле, стиль единый, контроль качества и сложности кода
cargo test: тесты как часть пайплайна, cargo-audit, cargo-deny
Наблюдаемость: OpenSerarch — логи, Sentry — трейсинг, Telegraf — метрики
Из сложностей: пришлось дописывания тесты для старых систем для возможности сравнения старой и новой системы.
На legacy системе о багах узнавали в runtime, а также были утечки и проблемы на GC.
В новой системе о багах узнавали при сборке, нет GC и память заранее определена.
В итоге инциденты и время на их разбор сократились в несколько раз. Неожиданно сократилась потребляемая память, а также уменьшилось время на онбординг новых сотрудников и реализацию фич.

Пришли к тому, что нужно сильнее покрывать тестами и повышать наблюдаемость через трейсинг и межсервисное взаимодействие по логам. Также лучше не трогать то, что не создает инцидентов сейчас. И конечно, нужно все измерять без домыслов.
Дмитрий рекомендует начинать с пилота, который решает большую часть проблем. Важно всех обучать и проводить внутренние встречи. Также надо продумывать и проверять взаимодействие со старыми системами заранее и переписывать по частям.
В конце доклада спикеру задали несколько вопросов. Решение о переходе было общекомандным. RTS не изменился, потому что количество клиентов не менялось. При переходе не использовали ИИ, переписывали вручную. Рынок Rust идет с задержкой, но появляются вакансии. Интересно, что Rust-разработчики с плюсовиками друг друга понимают, и плюсовики сами уже переходят на Rust. Важно выбирать тот язык, который решает вашу задачу. Сложности изучения Rust — стандартные: непонимание Borrow Checker и понимать что говорит Cargo. Делали профилирование — у Java было больше потребление input/output по дискам, а у Rust значительно ниже. В остальном не было видно значительных отличий.
Кейс № 3. На микроконтроллере крутится Rust
Финальный доклад — от Марка Вартаняна «Прошить железо c Rust: ESP32 за 20 минут». Марк обычно пишет на Go в облаке, но расскажет об опыте программирования на Rust. Основное преимущество Rust — это работа с памятью и владение ресурсами. А это как раз важно для микроконтроллеров — в показанном Марком микросхеме всего 100 МГц и 100 Кбайт оперативной памяти.
С микроконтроллерами Марк впервые пересекся на проекте телемедицины, в котором он писал бэкенд для электронной таблетницы: может светить огнями, кричать динамиками, крутить моторы, отсыпать таблетки и отправлять на сервер сообщение о пропуске — однажды это спасло жизнь человеку. Когда Марк занимался проектом, этот девайс стоял на столе и управлялся из консоли. В итоге программирование выходит в настоящий мир — и это вызывает восторг.
Проект, который сделал Марк: к микроконтроллеру подключен акселерометр и маленький экранчик. Прыгая с этим устройством, управляешь динозавром в игре, которая показывается на экране:

На микроконтроллере ESP32, он по всем показателям на порядок лучше Arduino. При этом она меньше — 5 × 5 миллиметров:

Также есть на рынке и другие решения.
STM32 — мощные и отличные решения, но на них нет Wi-Fi. Wi-Fi есть на Raspberry Pi Zero, но это уже другая лига со стоимостью чипа от 5к и гигагерцовым процессором. Забавно, что у Arduino есть модель с поддержкой WiFi, но эта поддержка сделана отдельным модулем ESP32. При выборе чипов от ESP важно понимать, что ESP8266 — древний чип, на котором Rust не запустишь. ESP32 — тоже старый, для запуска Rust на нем нужно патчить компилятор. Поэтому надо сразу брать ESP32-C. Другие варианты без Wi-Fi: ESP32-P — для высокого перформанса, ESP32-E — энергоэффективный. После выбора чипа можно выбрать вариант поставки этого чипа — он может быть и с двумя USB, и с OLED-экраном, и в небольшой плате. Сам микроконтроллер — это черная микросхема в виде ромба.
Купить можно на Озоне или другом маркетплейсе, они очень дешевые. Важно не взять случайно ESP32 без суффикса. И важно, чтобы архитектура была RISK-V (можно проверить на сайте производителя) — это то, на чем можно Rust запустить. Для начала работы потребуются монтажные провода DuPort, монтажная плата, набор светодиодов и резисторов.

Чтобы познакомиться с самим микроконтроллером, можно почитать документацию на 600 страниц, но проще найти в интернете схемы распиновки подобного вида:

Первое, что мы тут видим, — это USB, от которого приходит питание 5 вольт, которое разведено на первые две ноги. Также есть пониженное напряжение 3,3 вольта, которое используется микроконтроллером. На самой плате внизу красная C3 — это маленькая Wi-Fi-антенна и рядом с ним белый кварц для поддержания правильной частоты работы Wi-Fi. Над кварцем — микросхема, которая понижает напряжение с 5 до 3,3 вольта. На плате есть светодиод, подключенный к 8-й ноге. Есть две кнопки: левая кнопка программируемая, подключена к 9-й ноге. Правая — непрограммируемая: она для сброса и перезапуска микроконтроллера. Все полезные ноги помечены фиолетовым — это GPIO (General Purpose Input/Output). Они могут работать на вход и на выход, где 1 — это HIGH, 0 — это LOW. Ноги помеченные коричневым позволяют работать с аналоговым сигналом по ADC до 3,3 вольта. Прямое управление через ноги требует ресурсы процессора, поэтому есть отдельные интерфейсы, по которым можно отправлять сразу наборы байт по определенному протоколу, это разгружает процессор.
Рассмотрим шину I2C — к ней можно подключать несколько устройств параллельно. В данном проекте к ней подключен акселерометр и экран. Можно давать команды — например: «дай мне ускорение» или «выведи пиксель» — и получать ответы. У этой шины есть режимы 1 Гбит и 5 Гбит, что позволит подключать достаточно мощные компоненты.

Кроме шины I2C есть UART, которая позволяет подключить консоль, к которой подключаешься как к COM-порту. Можно слать текстовые команды и получать текстовые ответы. Оно дуплексное, то есть можно одновременно отправлять и получать. На картинке подключен GPS-модем, и именно эту модель не рекомендует Марк. Можно, например, запросить уровень сигнала (29 — это максимальный) или уведомить, что поступил звонок или пришло СМС. Обмен идет по двум проводам, но можно повесить только одно устройство, так как это не шина.

Но как эти все режимы работают? У каждой ноги есть внутри переключатель GPIO Mux, который переключается в зависимости от присланного в определенный регистр.

Также при работе с микропроцессором столкнешься с подобными схемами, в которых описаны возможности микропроцессора и вся поддерживаемая периферия. Есть даже отдельные блоки аппаратного шифрования.

Почему именно ESP32? Потому что у них на GitHub (EspressIf) и у них есть официальная поддержка Rust. esp-pucs — это структуры чипа, при записи в которые получаешь определенное поведение чипа. esp-generic — подготавливает проект. espFlash — для прошивки и ее записи на сам чип.
Все это нужно для работы поставить на компьютер, но лучше это поставить в докер-контейнер. Чтобы можно было работать с разными микросхемами и не приходилось каждый раз удалять ненужное и ставить нужное. Итого в докер нужно поставить rust, две архитектуры, cargo-binstall — чтобы устанавливать сразу бинарники, чтобы установка не шла по полчаса, а также поставить официальные программы для прошивки чипа.
FROM ubuntu: latest
SHELL ["/bin/bash", "-0"
"-o", "pipefail", "-c"]
RUN apt-get update && apt-get install --no-install-recommends -y \
build-essential g++ gcc clang make pkgconf pkg-config \
ca-certificates libssl-dev musl-tools llvm-dev libclang-dev libudev-dev libuv1-dev \
rustup esptool Ildb
# Rust, ESP32 RISC-V targets, tooling
RUN rustup toolchain install stable --component rust-src
RUN rustup add riscv32imc-unknown-none-elf # ESP32-C2, C3
RUN rustup add riscv32imac-unknown-none-elf # ESP32-C6, H2
# Cargo install. Binstall is a LOT faster.
RUN cargo install cargo-binstall
RUN cargo binstall espflash cargo-espflash \
esp-generate cargo-generate \
probe-rs-tools ldproxy --lockedДокер-файл можно через расширение devcontainer подключить к среде разработки. В нем подключаем образ, монтируем исходники в workspace, с которыми уже можем работать напрямую.
{
"name": "rust-esp32",
"image": "kolypto/rust-esp32-devcontainer",
"remoteUser": "ubuntu"
"runArgs": [ "--privileged" ],
"mounts": [
"source=/dev/,target=/dev/,type=bind",
].
"workspaceMount": "source=${localWorkspaceFolder}, target=workspace, type=bind,consistency=cached"
"workspaceFolder": "/workspace"
}При первом запуске нужно будет запустить генерацию проекта через esp-generate. Она спросит? какой у нас чип (например C3), там же можно включить некоторые фичи. Фичи, помеченные как unstable, не являются нестабильными или плохо работающими, лишь их интерфейс еще не зафиксирован до стабильного состояния.

Сгенерированный проект выглядит так. В главном конфиге указан чип и архитектура.

С микроконтроллером можно работать напрямую. Но это неинтересно, поэтому для тестов собрал небольшую схему со светодиодом и резистором, чтобы не сжечь чип. Более мощные нагрузки подключаются через транзистор:

Пишем простую программу на Rust. Сразу отключаем привычное std — это значит нет аллокатора, функций по работе со строками, привычных типов и прочего удобного, точка входа в программу другая и нам нужно работать на самом низком уровне с памятью вручную. Главная функция никогда не завершается — об этом позже. Настраиваем чип на максимальную частоту (минимальная потребуется при энергосбережения). Затем подключаем периферию для доступа к оборудованию. Настраиваем два пина: 8 — в output для включения светодиода, 9 — в input для кнопки.
#![no_std]
#![no_main]
use esp_hal::{...};
#[main]
in main() → ! {
let config = esp_hal:: Config:: default().with_cpu_clock(CpuClock:: max));
let peripherals = esp_hal:: init (config);
let mut led = Output:: new (peripherals. GPI08, Level:: High, OutputConfig:: default());
let mut button = Input:: new (peripherals.GPI09, InputConfig:: default()-with_pull(Pull::Up));
led. set_high();Main никогда не завершается, потому что в нем крутится бесконечный цикл. До входа в цикл выведем сообщение, чтобы видеть в консоли, что мы стартовали. В цикле проверяем нажатость кнопки и в зависимости от этого включаем/выключаем светодиод. Затем устройство засыпает на 500 миллисекунд, дожидаясь следующей проверки. Но так лучше не делать, так как при Delay процессор постоянно работает. Лучше для нажимания кнопок использовать прерывания, но это сильно усложнит код.
println!("Started!");
loop {
if button.is_high() {
led.set_high();
} else {
led.set_low();
}
// Busy wait
let delay = Delay::new();
delay.delay_millis(500);
}
}Когда код программы написан — собираем ее через cargo build.
Затем запускаем через cargo run. Под капотом запускается espFlash, который прошивает микроконтроллер: подключается к устройству, считывает его параметры, записывает программу в память — и чип готов к работе, даже есть некоторое управление прямо из консоли (CTRL+R — Reset chip, CTRL+C — Exit). Затем в консоли выводятся все параметры чипа — и тут же выводятся сообщения из нашей программы.
Есть симулятор Wokwi, на котором можно поиграться без микроконтроллера:

А теперь про то, как такие вещи делаются «по-взрослому». Обычно на таких проектах нужно параллельно работать с несколькими компонентами и учитывать физические процессы. На рисунке ниже — проект Wi-Fi-кнопки, при нажатии на которую отправляется сообщение в Telegram. Нужно сначала подключить фреймворк embassy с созданием async-задачи для него. У функции есть цикл, но благодаря async она умеет спать, разгружая процессор. В цикле ждем, когда кнопка будет нажата, точнее — ждем момента, когда кнопку отпустили. Затем ждем 20 миллисекунд, за которые проходят все физические процессы с замыканием и искрением контакта. Если кнопка нажата, отправляем сообщение в канал, сделанный ниже через CriticalSectionRawMutex
/// Task: listen to button clicks
#[embassy_executor::task()]
pub async fn task_button_clicks(mut button: gpio::Input< 'static>) {
loop {
// Wait for press (high→low transition)
button.wait_for_falling_edge().await;
// Debounce.
embassy_time:: Timer:: after_millis(20).await;
if button.is_low() {
// Send ONE event
defmt::debug!("Button clicked");
let _ = BUTTON_CLICKS.try_send(()): // Non-blocking
// Wait for it to be released. Don't send any more events.
button.wait_for_high() .await;
}
}
}
/// Channel: button clicks.
static BUTTON_CLICKS: Channel<CriticalSectionRawMutex, (), 1> = Channel::new();Читаем в одном канале, что кнопка нажата и отправляем сообщения в другой канал, через который будет отправляться сообщение в Telegram.
// Task: main logic
// — Read button clicks
// — Send them as Telegram messages
#[embassy_executor::task()]
pub async fn task_main() {
loop {
button::BUTTON_CLICKS.receive().await;
telegram::send_telegram_message (MESSAGE_CONTENT);
}
}
/// Send a message
pub fn send_telegram_message(msg: &str) {
// We only got a reference. To take ownership, we need a copy.
let owned: String<32> = String::try_from(msg).unwrap() ;
match MESSAGES_QUEUE.try_send(owned) {
Ok (()) => (),
Err (_) => defmt::error!("Queue full: cannot send message"),
}
}
/// Messages queue
static MESSAGES_QUEUE: Channel<CriticalSectionRawMutex, String::<32>, 8> = Channel:: new();Третий сервис уже работает с Telegram. Для начала нужно прочитать восемь случайных байт с датчика случайных чисел, который находится в метастабильном состоянии и от любого шума и тепла его колбасит от 0 до 1, поэтому с него можно считать случайные байты. Дальше мы инициализируем буфер для TLS, и это целые 16 Кб, и это очень много для микроконтроллера, у которого всего 400 Кб, часть которых уже занята. И в конце создаем буфер по 4 Кб для чтения и записи. Итого 40 Кб, только чтобы отправить один запрос, поэтому для связи с внешним миром обычно используют отдельные чипы.
// Send a message
async fn telegram_send_message (stack: embassy_net::Stack<'_>,
send_to: i64, message: &str) -> Result<(), TelegramSendMessageError> {
// TLS needs a random value
let rnd = Rng::new();
let tls_seed = {
let mut bytes = [0; 8];
rnd.read(&mut bytes);
u64::from_le_bytes(bytes)
};
// Init TLS.
let (mut rx_buffer, mut tx_buffer) = ([0; 16640], [0; 16640]);
let tls = TlsConfig::new(
tls_seed,
&mut rx_buffer,
&mut tx buffer,
reqwless::client::TlsVerify::None,
);
let tcp_state = TcClientState::<1, 4096, 4096>::new();
let tcp = TcpClient::new(stack, &tcp_state);
let dns = DnsSocket::new(stack);
let mut client = HttpClient::new_with_tls(&tcp, &dns, tls);Конструируем сообщение. Затем сериализуем JSON, для этого опять нужна 256 байт памяти. Отправляем запрос и читаем ответ и тоже нужно 4 Кб памяти.
// Data
let mut url: String<128> = String::new();
use core::fmt::Write;
write!(url, "https://api.telegram.org/bot{}/sendMessage", BOT_TOKEN).unwrap();
let msg = TelegramMessageInput {
chat_id: send_to,
text: message,
};
let mut body_buf = [0u8; 256];
let _body_len = serde_json_core::to_slice(&msg, &mut body_buf)?;
// Request
let mut buf = [0; 4096];
let mut req = client.request(reqwless::request::Method::POST, url.as_str())
.await?
.content_type(ContentType::ApplicationJson)
.body(body_buf.as_slice());
let resp = req.send (&mut buf).await?;Наконец читаем ответ до конца, и благодаря await можно уснуть, чтобы другое устройство могло перехватить управление, пока мы спим. Дешифруем UTF, если требуется. И вместо парсинга JSON мы просто ищем в нем подстроку “ok”:true — чтобы сэкономить память. При обращении к Telegram, наверное, не лучшая идея, так как они могут добавить пробел или изменить формат, а при обращении к своим серверам это отличный вариант.
// Read response
let response = resp.body().read_to_end().await?;
let resp_text = core::str::from_utf8(&response)
.map_err(|_| TelegramSendMessageError::ResponseError)?;
// Check for success
if !resp_text.contains(r#" "ok":true"#) {
defmt::error!("Telegram failed: {3", resp_text);
return Err(TelegramSendMessageError::ResponseError)
}
// Ok
return Ok(())
}На реальных больших проектах это пишут сложнее и на С++. Например, используя операционную систему FreeRTOS в мозгах машины Tesla. Создают очередь, чтобы писать туда события нажатых кнопок. Затем создается задача с бесконечным циклом внутри. И если кнопка нажата или отпущена, то в очередь отправляется сообщение. Далее уведомляют планировщик, что функция может вздремнуть и пока можно отдать управление другим. Эта операционная система FreeRTOS оперирует не процессами, а функциями, у которых есть стек, приоритет и она может спать.
void gpio_input_task_init(void) {
button_events_queue = xQueueCreate(8, sizeof(button_event_t));
xTaskCreate(gpio_input_task, "gpio_input", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);
}
static void gpio_input_task(void *argument) {
(void) argument;
for (;;) {
for (int ii = 0; i < BUTTON_COUNT; i++) {
button_t *button = &buttons[i];
bool pressed = read_button(button);
if (pressed != button->is_pressed) {
button->is_pressed = pressed;
button_event_t event = {.button = (button_id_t)i, .pressed = pressed};
xQueueSend(button_events_queue, &event, 0);
}
}
vTaskDelay(pdMS_TO_TICKS(BUTTON_POLL_PERIOD_MS));
}
}
static QueueHandle_t button_events_queue;
static bool read_button(const button_t *button) {
return HAL_GPIO_ReadPin(button->port, button->pin) == GPIO_PIN_RESET;
}Код под микроконтроллеры можно писать и на MicroPython — он сам управляет памятью. Тут также читаем два пина, и если кнопка нажата — включаем лампочку и дальше спим. Это очень просто, но мы тут памятью не управляем, поэтому это для домашних проектов и прототипов.
from machine import Pin
from time import sleep_ms
led = Pin(8, Pin. OUT)
btn = Pin(9, Pin.IN, Pin.PULL_UP) # button to GND, pressed = 0
while True:
pressed = btn.value() == 0
led.value(1 if pressed else 0)
sleep_ms(10)Есть аналогичное и для Go, называется TinyGo. В этом примере идет подключение к Wi-Fi, запускает на микроконтроллере веб-сервер, на котором даже есть микространица с сообщением hello.
func main() {
link := &link.Esplink{}
netdev.UseNetdev(link)
println("Connecting to WiFi...")
failIfErr ("connect", link.NetConnect (&nl. ConnectParams{
Ssid: ssid,
Passphrase: password,
}))
println("Connected to WiFi.")
var http httphi.MuxSlice
http.Handle("/", func(exch *httphi.Exchange) {
exch.RespondString(200, "application/json", '{"message": "hello"}`)
})
var router httphi.Router
cfg := httphi.DefaultRouterConfig(4, 2048, http.MaxPathValues())
failIfErr("configuring Router", router.Configure(&http, cfg))
err := link.ListenAndServe(&router, port)
failIfErr("Esplink.ListenAndServe", err)
}Помимо микроконтроллера, есть много компонентов для умного дома ESPHome. Например, в примере ниже микроволновый радар, который определяет наличие человека дома. И обычным yaml-файлом без программирования можно настроить работу этого устройства.

Марк ремомендует посмотреть 20 уроков и примеры Rust ESP32:
Все переходим на Rust
Послушав три доклада, решил, что новый проект буду писать на Rust. А после последнего доклада очень захотелось купить микропроцессор и поиграться с ним.. Книга «Паттерны проектирования Rust», лежащая у меня на столе, дождалась наконец — пришло ее время.
Резюмируя все доклады…
Плюсы от использования Rust:
Сокращение используемой памяти.
Совместимость с другими системами на Python, Java и C/C++.
Отсутствие Garbage Collector и связанных с ним проблем.
Выявление проблем на этапе сборки, а не в runtime.
Возможность работать с ограниченными ресурсами и даже на микропроцессорах.
Минусы:
Нужно время на изучение после другого языка.
Непривычный поначалу Borrow Checker и то, что пишет cargo.
Пока мало вакансий разработчиков Rust в РФ.
Явно плюсы перевешивают. А каков был ваш опыт перехода на Rust и делали ли проекты на микроконтроллерах? Продолжим в комментариях.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.