initial commit

This commit is contained in:
David F 2026-02-28 17:03:14 -05:00
commit 5ae3ffcc8a
12 changed files with 1014 additions and 0 deletions

12
.clangd Normal file
View file

@ -0,0 +1,12 @@
CompileFlags:
Remove:
# GCC-specific flags that clangd doesn't understand
- -fstrict-volatile-bitfields
- -fno-tree-switch-conversion
- -fno-jump-tables
- -freorder-blocks
# Xtensa-specific (esp32dev)
- -mlongcalls
- -mtext-section-literals
- -fipa-pta
- -free

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.pio
.cache

362
compile_commands.json Normal file

File diff suppressed because one or more lines are too long

37
include/README Normal file
View file

@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

24
include/screen.h Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // Not used
#define SCREEN_ADDRESS 0x3C // < from Datasheet, maybe 0x3c, maybe 0x3d
#define I2C_SDA_PIN 6 // D4 (GPIO 06) on XIAO_ESP32_C3
#define I2C_SCL_PIN 7 // D5 (GPIO 07) on XIAO_ESP32_C3
namespace Screen {
extern Adafruit_SSD1306 display;
// attempt to start up screen; will lock program if fails
void setup_screen();
// example code to draw to screen (test working)
void test_fillrect();
void wire_search(uint8_t loops);
} // namespace Screen

46
lib/README Normal file
View file

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

269
lib/SerialCmd/SerialCmd.cpp Normal file
View file

@ -0,0 +1,269 @@
#include "SerialCmd.h"
#include <cstring>
#include <cstdlib>
// --- CmdArgs ---
void CmdArgs::parse(char* line) {
_command = nullptr;
_rawCount = 0;
_posCount = 0;
// In-place tokenization with quoted string support
bool inToken = false;
bool inQuote = false;
char* tokenStart = nullptr;
for (char* p = line; ; ++p) {
char c = *p;
if (c == '\0') {
if (inToken && _rawCount < SERIAL_CMD_MAX_ARGS) {
_tokens[_rawCount++] = tokenStart;
}
break;
}
if (inQuote) {
if (c == '"') {
*p = '\0';
inQuote = false;
inToken = false;
if (_rawCount < SERIAL_CMD_MAX_ARGS) {
_tokens[_rawCount++] = tokenStart;
}
}
continue;
}
if (c == '"') {
if (inToken && _rawCount < SERIAL_CMD_MAX_ARGS) {
*p = '\0';
_tokens[_rawCount++] = tokenStart;
}
inQuote = true;
inToken = true;
tokenStart = p + 1;
continue;
}
if (c == ' ' || c == '\t') {
if (inToken) {
*p = '\0';
if (_rawCount < SERIAL_CMD_MAX_ARGS) {
_tokens[_rawCount++] = tokenStart;
}
inToken = false;
}
} else if (!inToken) {
inToken = true;
tokenStart = p;
}
}
// First token is the command name
if (_rawCount > 0) {
_command = _tokens[0];
}
// Classify remaining tokens: flags vs positional
for (int i = 1; i < _rawCount; ++i) {
if (_tokens[i][0] == '-') {
// It's a flag — skip for positional list
} else {
if (_posCount < SERIAL_CMD_MAX_ARGS) {
_positional[_posCount++] = _tokens[i];
}
}
}
}
const char* CmdArgs::arg(int index) const {
if (index < 0 || index >= _posCount) return nullptr;
return _positional[index];
}
int CmdArgs::intArg(int index, int fallback) const {
const char* v = arg(index);
return v ? atoi(v) : fallback;
}
float CmdArgs::floatArg(int index, float fallback) const {
const char* v = arg(index);
return v ? atof(v) : fallback;
}
bool CmdArgs::has(const char* flag) const {
for (int i = 1; i < _rawCount; ++i) {
if (strcmp(_tokens[i], flag) == 0) return true;
}
return false;
}
const char* CmdArgs::raw(int index) const {
if (index < 0 || index >= _rawCount) return nullptr;
return _tokens[index];
}
// --- SerialCmd ---
SerialCmd::SerialCmd(Stream& stream) : _stream(stream) {
_line[0] = '\0';
}
bool SerialCmd::on(const char* name, Handler handler) {
return on(name, nullptr, handler);
}
bool SerialCmd::on(const char* name, const char* usage, Handler handler) {
if (_commandCount >= SERIAL_CMD_MAX_COMMANDS) return false;
_commands[_commandCount++] = { name, usage, handler };
return true;
}
void SerialCmd::onDefault(Handler handler) {
_defaultHandler = handler;
}
bool SerialCmd::poll() {
while (_stream.available()) {
feedChar(_stream.read());
if (_lineReady) {
dispatch();
return true;
}
}
return false;
}
void SerialCmd::wait() {
_lineReady = false;
while (!_lineReady) {
if (_stream.available()) {
feedChar(_stream.read());
} else {
yield();
}
}
dispatch();
}
void SerialCmd::feedChar(char c) {
// Handle \r\n sequences — ignore \n right after \r
if (c == '\n' && _prevCR) {
_prevCR = false;
return;
}
_prevCR = (c == '\r');
// End of line
if (c == '\r' || c == '\n') {
if (_echo) _stream.println();
_line[_linePos] = '\0';
if (_linePos > 0) {
_lineReady = true;
}
return;
}
// Backspace (BS or DEL)
if (c == '\b' || c == 127) {
if (_linePos > 0) {
_linePos--;
if (_echo) _stream.print("\b \b");
}
return;
}
// Accumulate printable characters
if (_linePos < SERIAL_CMD_MAX_LINE - 1) {
_line[_linePos++] = c;
if (_echo) _stream.print(c);
}
}
void SerialCmd::dispatch() {
_lineReady = false;
CmdArgs args;
args.parse(_line);
if (!args.command()) {
_linePos = 0;
return;
}
// Built-in /sync — checked before user commands
if (strcmp(args.command(), "/sync") == 0) {
sendSyncList();
_linePos = 0;
return;
}
for (int i = 0; i < _commandCount; ++i) {
if (strcmp(_commands[i].name, args.command()) == 0) {
_commands[i].handler(args);
_linePos = 0;
return;
}
}
if (_defaultHandler) {
_defaultHandler(args);
}
_linePos = 0;
}
void SerialCmd::sendSyncList() {
_stream.println("#sync-begin");
for (int i = 0; i < _commandCount; ++i) {
_stream.print(_commands[i].name);
if (_commands[i].usage) {
_stream.print(' ');
_stream.print(_commands[i].usage);
}
_stream.println();
}
_stream.println("#sync-end");
}
bool SerialCmd::sync(unsigned long timeout_ms) {
sendSyncList();
unsigned long deadline = millis() + timeout_ms;
char buf[SERIAL_CMD_MAX_LINE];
int pos = 0;
bool prevCR = false;
while (millis() < deadline) {
if (!_stream.available()) {
yield();
continue;
}
char c = _stream.read();
// Handle \r\n sequences
if (c == '\n' && prevCR) {
prevCR = false;
continue;
}
prevCR = (c == '\r');
if (c == '\r' || c == '\n') {
buf[pos] = '\0';
if (pos > 0 && strcmp(buf, "#acknowledge-sync") == 0) {
return true;
}
pos = 0;
continue;
}
if (pos < SERIAL_CMD_MAX_LINE - 1) {
buf[pos++] = c;
}
}
return false;
}

87
lib/SerialCmd/SerialCmd.h Normal file
View file

@ -0,0 +1,87 @@
#ifndef SERIAL_CMD_H
#define SERIAL_CMD_H
#include <Arduino.h>
#include <functional>
#ifndef SERIAL_CMD_MAX_LINE
#define SERIAL_CMD_MAX_LINE 128
#endif
#ifndef SERIAL_CMD_MAX_ARGS
#define SERIAL_CMD_MAX_ARGS 16
#endif
#ifndef SERIAL_CMD_MAX_COMMANDS
#define SERIAL_CMD_MAX_COMMANDS 16
#endif
class CmdArgs {
public:
void parse(char* line);
const char* command() const { return _command; }
// Positional args (skips flags)
const char* arg(int index) const;
int intArg(int index, int fallback = 0) const;
float floatArg(int index, float fallback = 0.0f) const;
int count() const { return _posCount; }
// Flag check
bool has(const char* flag) const;
// Raw token access (includes flags)
const char* raw(int index) const;
int rawCount() const { return _rawCount; }
private:
const char* _command = nullptr;
const char* _tokens[SERIAL_CMD_MAX_ARGS];
int _rawCount = 0;
const char* _positional[SERIAL_CMD_MAX_ARGS];
int _posCount = 0;
};
class SerialCmd {
public:
using Handler = std::function<void(CmdArgs&)>;
explicit SerialCmd(Stream& stream);
bool on(const char* name, Handler handler);
bool on(const char* name, const char* usage, Handler handler);
void onDefault(Handler handler);
bool poll();
void wait();
bool sync(unsigned long timeout_ms = 5000);
void setEcho(bool enabled) { _echo = enabled; }
private:
void feedChar(char c);
void dispatch();
void sendSyncList();
Stream& _stream;
bool _echo = false;
char _line[SERIAL_CMD_MAX_LINE];
int _linePos = 0;
bool _lineReady = false;
bool _prevCR = false;
struct Command {
const char* name;
const char* usage;
Handler handler;
};
Command _commands[SERIAL_CMD_MAX_COMMANDS];
int _commandCount = 0;
Handler _defaultHandler;
};
#endif

19
platformio.ini Normal file
View file

@ -0,0 +1,19 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32c3]
platform = espressif32
board = seeed_xiao_esp32c3
framework = arduino
monitor_speed = 115200
monitor_port = /dev/ttyACM2
lib_deps =
adafruit/Adafruit SSD1306
adafruit/Adafruit GFX Library

65
src/main.cpp Normal file
View file

@ -0,0 +1,65 @@
#include <Adafruit_GFX.h>
#include <Arduino.h>
#include <SerialCmd.h>
#include <cstdint>
#include <screen.h>
#define BAUD 115200
SerialCmd cmd(Serial);
unsigned long lastStatus = 0;
void register_commands() {
cmd.on("/ping", "— responds with pong",
[](CmdArgs &args) { Serial.println("pong"); });
cmd.on("/screen", "'-draw' to redraw rectangles", [](CmdArgs &args) {
if (args.has("-draw")) {
Screen::test_fillrect();
Serial.println("Completed drawing");
} else {
Serial.println("usage: /screen -draw");
}
});
cmd.on("/status", "— print device status", [](CmdArgs &args) {
Serial.printf("uptime: %lus\n", millis() / 1000);
});
cmd.on("/scanI2C", "-scans for any recognizable I2C devices",
[](CmdArgs &args) { Screen::wire_search(3); });
cmd.onDefault(
[](CmdArgs &args) { Serial.printf("unknown: %s\n", args.command()); });
Serial.println("ready");
}
void setup() {
Serial.begin(BAUD);
while (!Serial)
delay(10);
Serial.printf("Serial communication initialized at %d\n", BAUD);
Serial.println("10s delay before I2C initialization");
delay(10000);
Serial.println("Delay Complete");
Screen::setup_screen();
Screen::test_fillrect();
register_commands();
}
void loop() {
cmd.poll();
// Periodic status to prove non-blocking
if (millis() - lastStatus >= 10000) {
lastStatus = millis();
Serial.printf("[uptime %lus]\n", millis() / 1000);
}
}

80
src/screen.cpp Normal file
View file

@ -0,0 +1,80 @@
#include "screen.h"
#include "Arduino.h"
#include "esp32-hal.h"
#include <cstdint>
namespace Screen {
bool initialized_wire = false;
bool initialized_screen = false;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// attempt to start up screen; will lock program if fails
void setup_screen() {
if (!initialized_wire) {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
initialized_wire = true;
}
for (;;) {
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println("SSD1306 failed initiation");
delay(500);
} else {
initialized_screen = true;
return;
}
}
}
// example code to draw to screen (test working)
void test_fillrect() {
display.clearDisplay();
for (int16_t i = 0; i < display.height() / 2; i += 3) {
display.fillRect(i, i, display.width() - i * 2, display.height() - i * 2,
SSD1306_INVERSE);
display.display();
delay(1);
}
}
void wire_search(uint8_t loops = 5) {
if (!initialized_wire) {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
initialized_wire = true;
}
for (uint8_t i = 0; i < loops; i++) {
byte error, address;
int nDevices;
Serial.println("Scanning...");
nDevices = 0;
for (address = 1; address < 127; address++) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
Serial.print("I2C device found at address 0x");
if (address < 16)
Serial.print("0");
Serial.print(address, HEX);
Serial.println(" !");
nDevices++;
} else if (error == 4) {
Serial.print("Unknown error at address 0x");
if (address < 16)
Serial.print("0");
Serial.println(address, HEX);
}
}
if (nDevices == 0)
Serial.println("No I2C devices found.");
else
Serial.println("Done.");
delay(5000);
}
}
} // namespace Screen

11
test/README Normal file
View file

@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html