Toy drones are everywhere. You can pick up an Eachine E88 or E58 for under $30, fly it around your living room, and have a decent time. But what happens when the official app stops working, the manufacturer disappears, or you simply want to understand what’s actually happening between your phone and that spinning plastic in the air?
That question is what started this project.
The Goal
Build a fully custom WiFi flight controller for Eachine E88/E58 toy drones, running on an M5Stack AtomS3 attached to an M5Stack Atomic Battery Base — a compact ESP32-S3 assembly with a built-in LCD, RGB LED, LiPo battery, and a single face button. No phone required. No official app. Full control over every byte sent to the drone.
The complete build process for the black drone — which we called Maritaca Force 1 — is documented in a YouTube playlist where you can follow the project from the very beginning, including hardware assembly, initial tests, and first flights. Watch the Maritaca Force 1 build playlist on YouTube.
The Hardware
M5Stack AtomS3 + Atomic Battery Base
The AtomS3 is a compact development board built around the ESP32-S3 (240 MHz dual-core, 8 MB Flash, 2 MB PSRAM), attached to the M5Stack Atomic Battery Base which adds a LiPo battery and an IP5306 power management IC. Together they form the controller unit used in this project:
Built-in 0.85″ 128×128 LCD (GC9107 driver, SPI) — small but enough for a real flight HUD
Built-in RGB LED via M5Unified
One face button (BtnA, GPIO 41) — the entire UI runs through this single input
WiFi 2.4 GHz built in — connects directly to the drone’s access point
LiPo battery via Atomic Battery Base — USB-C charging, fits in one hand
Battery level read directly via I2C (0x75) — no extra library needed, just two register reads
The Drones — Eachine E88 Clones
Both drones are E88 clones that expose a WiFi access point, stream MJPEG video over UDP, and accept flight commands via UDP packets using a protocol derived from the Eachine E58 family.
We have two drones in this project:
Maritaca Force 1 (black drone, WIFI_8K_ variant) — 8-byte UDP packets on port 8090
Dr.One (grey drone, FLOW-WIFI variant) — 88-byte wrapped packets on port 8800, with optical flow altitude hold
Both were fully reverse-engineered from packet captures. No SDK, no documentation.
The Build System
The firmware is written in C++ using PlatformIO and the Arduino framework, targeting the m5stack-atoms3 board. The only external library dependency is M5Unified — everything else (WiFi, BLE, UDP) comes from the ESP32 Arduino core.
Why Not Just Use the App?
The official apps are garbage. Laggy, ad-infested, and they break with OS updates.
Learning. Reverse engineering a proprietary UDP protocol from packet captures is a genuinely interesting exercise.
Control. Once you own the protocol, you can do things the app never supported — like controlling the drone with a physical BLE gamepad, or using the board’s accelerometer as a tilt controller.
The AtomS3 fits in your pocket. A dedicated hardware controller with a real display is more satisfying than a phone app.
What’s Coming in This Series
Part 1 (this post) — Hardware and motivation
Part 2 — Reverse engineering the UDP protocol with packet captures
Part 3 — Firmware architecture: HAL pattern, non-blocking loop, flight state machine
Part 4 — Accelerometer tilt control
Part 5 — BLE HID gamepad host on ESP32 (iPega PG-9021S)
Part 6 — Second drone: FLOW-WIFI reverse engineering
Após algum tempo sem posts aqui no blog, deixo esse post sobre um novo projeto que deixei público no GitHub, onde demonstro como integrar projetos escritos em C com bibliotecas escritas em Rust (o inverso também é possível), utilizando CMake e todo ferramental disponível no ecossistema do Rust.
O projeto em questão é o weather-bot que já está disponível no meu perfil no GitHub, prontinho para uso nas plataformas Linux e MacOSX, até a data desse artigo.
Conforme explico no vídeo sobre o weather-botem meu canal no Youtube, o projeto ainda não foi testado no Windows, portanto possíveis ajustes talvez sejam necessários para compatibilizar o mesmo nessa plataforma e caso seja necessário alguma compatibilização, a mesma deva afetar apenas na camada do CMake, uma vez que o projeto foi desenvolvido com o máximo de código multi-plataforma em mente, tanto na parte C quanto na parte Rust.
Abaixo segue o vídeo sobre a “saga” do desenvolvimento desse projeto.
Finalmente após algum período longe do desenvolvimento de software para MSX, decidi voltar ao game unindo o útil mundo do IoT ao agradável universo de retrocomputing através do nosso querido e amado MSX.
Como se trata de um tema atual, rico de informações e possibilidades, decidi escrever dois artigos sendo esse aqui o primeiro que é mais introdutório e o segundo será mais prático, descrevendo o passo-a-passo de como usar toda a tecnologia envolvida nesse pequeno projeto.
Não é a primeira vez que me aventuro na área de comunicação no MSX, pois há 10 anos atrás (passou o tempo e nem percebi) escrevi sobre Networking no MSX através de 2 artigos intitulados MSX Networking e MSX Networking – Developers, developers, developers, developers, … respectivamente e que na época tiveram boa receptividade na comunidade MSX.
Nesses posts apresentei tecnologias como a interface de rede para MSX denominada ObsoNET e a interface que estava sendo desenvolvida na comunidade MSX na época e que não tinha nome e que eu intitulei como OptoNET e desde então é reconhecida como OptoNET, inclusive teve uma versão Wi-Fi da mesma e que usa ESP8266.
No ultimo artigo inclusive liberei a implementação de uma abstração de Network Socket para uso em aplicações escritas em Turbo Pascal 3 no MSX, bem como a implementação de um driver para OptoNET sendo assim possível a sua programação em Pascal usando essa abstração.
Nesse interim adquiri quase tudo o que pude referente a comunicação, desde as diversas placas de rede existentes para MSX como a excelente Gr8Net de Eugeny Brychkov (from Russia with love), DenYoNet da Sunrise, até interfaces RS-232 como a Fast Harukaze e a CT-80NET da Gradiente.
Comecei a desenvolver muita coisa para RS-232 utilizando a BIOS padronizada pelo consorcio MSX mas infelizmente paralisei seu desenvolvimento em um estágio bem avançado, o mesmo tendo acontecido com o desenvolvimento das rotinas para uso da ObsoNET que também está quase no mesmo estágio de conclusão das rotinas de RS-232 e que esse ano, ambas as implementações certamente serão concluídas e assim teremos mais uma importante feature na PopolonY2k Framework Library.
Pois bem, há quase 10 anos tenho essas duas interfaces RS-232, Fast Harukaze e a Gradiente CT-80NET e arrisco a dizer que a possibilidade de programar softwares de comunicação para MSX foi o que me fez a voltar a ser bastante atuante na comunidade (isso lá no início dos anos 2000) e que principalmente foi um tópico que me fez a querer ter um MSX real (agora também os sintetizados via FPGA) para colocar meus planos em prática.
RS-232 – O inicio de tudo
O RS-232, também conhecido como RS-232C, é um protocolo padrão de mercado criado no final dos anos 60, mais especificamente em 1969 e que teve seu uso amplamente difundido para o público técnico e de uso geral principalmente nos anos 80 com o advento da computação pessoal difundida pelos mais diversos PC`’s de 8, 16 e 32 Bits da época, atravessando o tempo e chegando aos dispositivos IoT dos dias atuais, como Arduino e Raspberry Pi.
LoRa – Um protocolo serial via wireless
A comunicação avançou muito nos últimos anos e com todo esse avanço tivemos diversas tecnologias criadas que vão desde comunicação via Ethernet cabeada, até WiFi, o que chamamos de comunicação Wireless ou sem fio.
Apesar do bom e velho RS-232C e suas variantes como a RS-485 terem sido desenvolvidos principalmente para operar utilizando cabos, não é recente a sua utilização via wireless através de dispositivos especialmente projetados para lidar com as caraterísticas especiais de uma comunicação sem fio.
Nesse universo e principalmente devido ao grande avanço das tecnologias de Internet Of Things (IoT), muita coisa nova tem sido desenvolvida, principalmente através da união de grandes players de tecnologia, que por sua vez tem ajudado a criar padrões para utilização no universo IoT.
É nesse cenário nasce a Lora Alliance, que é uma organização sem fins lucrativos responsável pela criação, especificação de tecnologias baseadas em LoRa.
Mas que raios é esse LoRa afinal ?
LoRa é um protocolo de comunicação via rádio que significa Long Range (Longo alcance), ou seja, é um protocolo para envio de informações a longas distâncias, onde se tem relatos de comunicação entre 2 pontos a uma distância de até 15Km.
É lógico que depende de vários fatores, como visada (Line of Sight) dentre outros que podem interferir no alcance, porém só o fato de existir essa possibilidade de comunicação a longas distâncias já torna LoRa muito interessante para uso em diversas aplicações como já vem sendo utilizado pelo agronegócio para monitoria de equipamentos em fazendas que na maioria dos casos é um ambiente propício para esse tipo de aplicação
Apesar da grande distância que um dispositivo LoRa pode proporcionar, nem tudo são flores sendo a principal desvantagem a baixa velocidade pois a comunicação entre dispositivos LoRa é extremamente lenta, impossibilitando o seu uso em transmissões com grande volume de dados como áudio e vídeo por exemplo.
Esse é um dos principais motivos pelos quais as informações trafegadas entre dispositivos LoRa, estar restrita a envio de dados de telemetria ou aplicações onde baixa latência não é o principal requisito.
LoRa possui duas topologias de operação, sendo elas Point-to-Point e Network, essa última de fato é uma espécie de Broadcasting, além de variantes como a LoRaWan capaz de colocar um dispositivo LoRa na internet, o que foge um pouco do escopo desse post portanto vamos explorá-la em um post futuro.
Dispositivos LoRa na prática
Na prática temos uma infinidade de dispositivos LoRa desenvolvidos por diversos fabricantes e embora exista uma certa padronização no nível de sinal, frequência de rádio e afins, não há uma padronização na interface de software com LoRa, com isso cada fabricante de dispositivos LoRa está criando sua própria abstração de comunicação com seus dispositivos.
A fabricante REYAX, é a que melhor se adaptou e está usando comandos AT, padrão da Hayes criado para comunicação via modem que se tornou padrão de facto sendo amplamente utilizado pela industria desde os anos 80 bem como pela própria REYAX em seu RYLR998, o que torna esse dispositivo um dos mais “padronizados” em nível de software no mercado.
Dentre os diversos fabricantes, um dos mais “famosos” é a chinesa EBYTE, que possui diversos modelos de módulos LoRa, todos bem parecidos visualmente e operacionalmente entre si, apenas mudando especificações técnicas talvez relacionadas a potência e consumo por exemplo. O módulo que utilizo nos experimentos com MSX e IoT é exatamente esse da imagem, o E220-900T22D.
Tanto o módulo LoRa da REYAX, quanto o da EBYTE, tem sua interface física de comunicação baseada em UARTTTL serial e no caso dos módulos da EBYTE eles tem um protocolo binário bem simples para setup do módulo LoRa e que está disponível quando o módulo está operando em modo de setup/configuração.
Infelizmente a documentação do módulo da EBYTE não é muito clara e gera algumas duvidas que nos induz a pensar que a documentação está errada ou o módulo não funciona, entretanto após pesquisar outros sites e vídeos sobre esses módulos da EBYTE percebi mesmo que essa documentação é só mau escrita mesmo e que o módulo funciona corretamente e de fato ele é mais simples do que eu imaginava, não necessitando conhecer algum protocolo específico para sua operação (exceto quando você está em modo de setup/configuração do módulo).
Bom, acredito que após essa longa introdução sobre comunicação, dispositivos seriais e LoRa, podemos começar a colocar a mão na massa e ver como usar os dispositivos LoRa e principalmente como utilizá-los através de um computador MSX.
Como eu já citei inicialmente, esse é o primeiro artigo introdutório e no próximo descreveremos tecnicamente os detalhes e possibilidades de integração de dispositivos LoRa no MSX ou em outras tecnologias como Arduino, Raspberry Pi, etc.
Após um período de hiato voltamos a lançar mais uma edição da Revista MSX Brasil Oficial que de fato estava no forno desde inicio de 2020 entretanto com os acontecimentos mundiais ocorridos nesses últimos 2 anos, a prioridade dessa edição diminuiu bastante a ponto de praticamente cancelarmos todo o planejamento original e repensarmos a mesma quase que completamente.
Passado esse momento de instabilidade voltamos com muito conteúdo repaginado e relacionado aos últimos acontecimentos ocorridos na comunidade MSX brasileira e também internacional.
Nessa edição temos um review sobre a excelente versão de Ghosts n’ Goblins para MSX equipados com o poderoso V9990, além de entrevistas com “antigas” e “novas” personalidades conhecidas na comunidade MSX onde poderemos saber um pouco sobre sua história bem como as motivações que os fizeram abraçar essa plataforma que tem mantido unidas diversas gerações de entusiastas.
Após alguns meses do lançamento da última versão do PopolonY2k Framework, venho trabalhando em alguns programas de exemplo para reforçar os conceitos implementados no framework bem como na difusão de conhecimento de tudo o que tem nele desenvolvido.
A comunidade nacional tem utilizado muitas partes do framework e principalmente a comunidade internacional tem reportado alguns problemas encontrados, principalmente no Pop!Art, o que me levou a verificar que de fato os problemas estão relacionados a algumas peças do framework e que brevemente serão ajustadas ainda esse ano.
No grupo de WhatsApp“MSX Pascal, C, ASM e etc” temos discutido muito sobre assuntos relacionados a desenvolvimento para retro-machines, principalmente para o MSX e em algumas dessas discussões surgiram alguns questionamentos sobre o uso das rotinas de uso da mapper do PopolonY2k Framework.
Por esse motivo, preparei um sample com um código bem elucidativo sobre o uso das principais rotinas de alocação e paginação da mapper que fazem uso das rotinas do MSXDOS2 disponíveis no framework, conforme análise e explicação pode ser vista no vídeo abaixo.
MSXDOS2 Mapper PopolonY2k Framework routines
O framework também contempla uso da mapper através do acesso direto a portas de I/O, o que torna o seu uso mais rápido, entretanto essas rotinas são as chamadas “mal comportadas” e devem ser utilizadas com muita cautela principalmente quando se tem completa certeza de que não há conflitos com áreas pré-alocadas pelo próprio MSXDOS2 via suas rotinas padrão.
O Pop!Art faz isso com extrema segurança, onde o mesmo aloca e preenche os dados da música VGM usando as rotinas de mapper padrão do MSXDOS2 e usa as de acesso direto, apenas no momento em que está tocando quando está apenas fazendo paginação dos segmentos da mapper pré-alocados pelas rotinas do MSXDOS2.
Os exemplos estão sendo adicionados diretamente no repositório do projeto na OldSkoolTech no SourceForge.net e podem ser consultados nesse link aqui.
O sample de uso da mapper (maprtest.pas) está completamente comentado sendo bem auto-explicativo, além de ter uma análise bem completa no vídeo acima, podendo seu código ser conferido abaixo:
(*<maprtest.pas>
* Memory mapper routines test.
*
* - Routines tested:
* - InitMapper;
* - GetMapperPageByAddress;
* - PutMapperPageByAddress;
* - AllocMapperSegment;
* - FreeMapperSegment;
*
* CopyLeft (c) since 1995 by PopolonY2k.
*)
(**
*
* $Id$
* $Author$
* $Date$
* $Revision$
* $HeadURL$
*)
{-----------------------------------------------------------------------------}
{ PopolonY2k Framework dependencies }
{-----------------------------------------------------------------------------}
{$i types.pas}
{$i helpchar.pas}
{$i msxbios.pas}
{$i extbio.pas}
{$i maprbase.pas}
{$i maprallc.pas}
{$i maprpage.pas}
{-----------------------------------------------------------------------------}
{ Module definitions }
{-----------------------------------------------------------------------------}
Type TMappedBuffer = Array[0..ctMaxMapperPageSize] Of Char; { Mapped 16K bufr }
{-----------------------------------------------------------------------------}
{ Helper functions }
{-----------------------------------------------------------------------------}
(**
* Print part of a buffer content passed as reference.
* @param aBuffer Reference to buffer to be printed;
*)
Procedure PrintBuffer( Var aBuffer : TMappedBuffer );
Const
__ctMaxCol : Byte = 10;
Var
x, y : Byte;
Begin
For y := 0 To __ctMaxCol Do
For x := 0 To __ctMaxCol Do
Begin
GotoXY( ( x + 1 ), ( y + 1 ) );
Write( aBuffer[( x + y ) * __ctMaxCol] );
End;
WriteLn;
WriteLn( 'Press <enter> to continue' );
ReadLn;
ClrScr;
End;
{-----------------------------------------------------------------------------}
{ Main program variables and constants }
{-----------------------------------------------------------------------------}
Const
ctDefaultPage = $8000; { Default page for data - Page 2 }
Var
maprHandle : TMapperHandle;
chKey : Char;
nActiveSegmentId : Byte;
aSegments : Array[0..1] Of Byte;
aDataBuffer : TMappedBuffer Absolute ctDefaultPage;
{-----------------------------------------------------------------------------}
{ Main program entry point }
{-----------------------------------------------------------------------------}
Begin { Main program entry }
ClrScr;
{ Initialize mapper system }
If( Not InitMapper( maprHandle ) ) Then
Begin
WriteLn( 'Error to initialize Mapper' );
Exit;
End;
WriteLn( 'Mapper succesfully initialized' );
{ Get the current segment used by data buffer on stack }
aSegments[0] := GetMapperPageByAddress( maprHandle, Addr( aDataBuffer ) );
WriteLn( 'Main Segment -> ', aSegments[0], ' on page 2' );
WriteLn( 'Press <enter> to see it''s content' );
ReadLn;
ClrScr;
{ Fill page 2 on Main Segment, fully with X character }
FillChar( aDataBuffer, SizeOf( aDataBuffer ), 'X' );
PrintBuffer( aDataBuffer );
{ Alloc new segment to put data (MSXDOS2 BIOS) }
If( Not AllocMapperSegment( maprHandle,
maprHandle.nPriMapperSlot,
UserSegment, aSegments[1] ) ) Then
Begin
WriteLn( 'Mapper segment allocation failed' );
Exit;
End;
WriteLn( 'New Segment -> ', aSegments[1], ' successfully allocated' );
WriteLn( 'Type <enter> to see it''s content' );
ReadLn;
ClrScr;
{ Activate page 2 content to the New Segment allocated }
PutMapperPageByAddress( maprHandle, aSegments[1], Addr( aDataBuffer ) );
{ Fill page 2 on New Segment fully with Y character }
FillChar( aDataBuffer, SizeOf( aDataBuffer ), 'Y' );
PrintBuffer( aDataBuffer );
nActiveSegmentId := aSegments[1];
{ Handle user switch segment contents }
Repeat
GotoXY( 1, 19 );
WriteLn( 'Active Segment -> ', nActiveSegmentId );
WriteLn;
WriteLn( '0 - Switch to Main Segment ', aSegments[0] );
WriteLn( '1 - Switch to New Segment ', aSegments[1] );
WriteLn( 'ESC - exit' );
chKey := ReadKey;
If( chKey In ['0', '1'] ) Then
Begin
nActiveSegmentId := aSegments[Byte( chKey ) - Byte( '0' )];
{ Activate page 2 content to segment chosen by user }
PutMapperPageByAddress( maprHandle,
nActiveSegmentId,
Addr( aDataBuffer ) );
{ Show segment content }
PrintBuffer( aDataBuffer );
End;
Until( chKey = #27 );
WriteLn;
{ Release all segments allocated by the application }
If( Not FreeMapperSegment( maprHandle,
maprHandle.nPriMapperSlot,
aSegments[1] ) ) Then
Begin
WriteLn( 'SegmentId -> ', aSegments[1], ' deallocation failed' );
Exit;
End;
WriteLn( 'All segments successfully deallocated' );
End.
Em tempo, Ricardo Jurczyk Pinheiro desenvolveu um excelente e mais completo exemplo que faz uso das rotinas de mapper do framework de maneira mais completa, explorando inclusive outras funções de suporte a mapper do framework.