Guidelines for AI agents (GitHub Copilot, Cursor, Claude, etc.) and developers working on this project.
OSS Transmitter is professional embedded firmware for ESP32, focused on:
Before making any code changes:
Create an action plan using manage_todo_list:
1. Read module X and understand its operation
2. Add new function to class Y
3. Update configuration file Z
4. Test change via serial monitor
5. Update documentation
Why is this important?
main.cpp (State Machine Orchestrator)
├── ConfigManager → LittleFS persistence
├── WiFiManager → Network connectivity
├── NTRIPClient → RTCM data source
├── RTCMParser → Message validation
├── RTCMBuffer → Type-based buffering
├── LoRaTransmitter → Radio transmission
├── DisplayManager → OLED UI
└── WebInterfaceManager → HTTP API + captive portal
Critical: Every change must respect state flow!
STATE_INIT
↓ WiFi + NTRIP + LoRa init
STATE_WAITING_FOR_FIRST_RTCM
↓ First RTCM message received
STATE_TRANSMISSION (3.5s)
↕ Cycle repeats
STATE_GATHERING (0.25s)
TRANSMISSION_PERIOD_MS = 3500 // TX burst window
GATHERING_PERIOD_MS = 250 // Silence period
GGA_SEND_INTERVAL_MS = 2000 // NTRIP position update
DISPLAY_UPDATE_INTERVAL = 500 // OLED refresh
⚠️ Don’t change these values without deep analysis - they affect rover synchronization!
// GOOD: CamelCase for classes
class NTRIPClient {
public:
// GOOD: camelCase for methods
bool connectToCaster();
// GOOD: snake_case for local variables
uint32_t bytes_received = 0;
private:
// GOOD: camelCase for private fields
String username;
WiFiClient client;
};
// GOOD: UPPER_SNAKE_CASE for constants
#define MAX_BUFFER_SIZE 1024
const uint32_t DEFAULT_TIMEOUT = 10000;
// GOOD: Comments explain "why", not "what"
// We must delay here to prevent LoRa module overheating
delay(100);
// BAD: Obvious comments
// Set variable to 5
int count = 5;
// ===== header_file.h =====
#ifndef HEADER_FILE_H
#define HEADER_FILE_H
// Forward declarations (if needed)
class ConfigManager;
// Class definition
class MyComponent {
public:
// Public API
MyComponent();
void initialize();
private:
// Private implementation
void internalMethod();
// Private fields
int counter;
};
#endif // HEADER_FILE_H
// ===== cpp_file.cpp =====
#include "cpp_file.h"
#include "other_dependencies.h"
// Constructor
MyComponent::MyComponent() : counter(0) {
}
// Public methods
void MyComponent::initialize() {
// Implementation
}
// Private methods
void MyComponent::internalMethod() {
// Implementation
}
ESP32 has only 520KB RAM - every byte counts!
// GOOD: Stack allocation for small buffers
uint8_t buffer[256];
// GOOD: Static for constant strings
static const char* TAG = "MyModule";
// CAUTION: Heap allocation - ensure delete/free!
uint8_t* bigBuffer = new uint8_t[4096];
// ... use buffer ...
delete[] bigBuffer;
// GOOD: String only when really needed
String message = createMessage(); // OK - return and use immediately
// BAD: Unnecessary String copying
String temp1 = getString();
String temp2 = temp1; // unnecessary copy
String temp3 = temp2 + "!"; // another copy
// GOOD: Check return values
if (!WiFi.begin(ssid, password)) {
Serial.println(F("WiFi begin failed"));
return false;
}
// GOOD: Serial.println for user-facing errors
if (connection_failed) {
Serial.println(F("[ERROR] Connection timeout"));
}
// GOOD: #ifdef DEBUG for debug logs
#ifdef DEBUG
Serial.printf("Buffer size: %d bytes\n", bufferSize);
#endif
// BAD: Ignoring errors
ntripClient.connect(); // What if it fails?
// GOOD: Always check LoRa is not busy
if (LoRa.beginPacket()) {
LoRa.write(data, length);
LoRa.endPacket();
}
// GOOD: Delay between packets (SX1276 needs time)
delay(100); // Prevent module overheating
// BAD: Transmission during STATE_GATHERING
if (currentState == STATE_GATHERING) {
LoRa.beginPacket(); // ERROR! Breaks timing sync!
}
// Access configuration
SystemConfig config = configManager.getConfig();
Serial.println(config.wifi.ssid);
// Save configuration
SystemConfig newConfig = configManager.getConfig();
newConfig.wifi.ssid = "NewNetwork";
configManager.saveConfig(newConfig);
// IMPORTANT: saveConfig() writes to LittleFS - don't call in loops!
// Parser automatically buffers messages by type
rtcmParser->processData(buffer, bytesRead);
// Transmit buffered (only in STATE_TRANSMISSION!)
rtcmParser->transmitBufferedMessagesDirect([](const uint8_t* data, size_t len, uint16_t type) {
return loraTransmitter->transmitMessage(data, len);
});
// Automatic fragmentation for large messages
bool success = loraTransmitter->transmitMessage(rtcmData, rtcmLength);
// Transmitter handles:
// - Message ID (wrapping 0-255)
// - Fragmentation (packets of 246 bytes)
// - Packet numbering
// REST API endpoints are automatically available:
// GET /api/config - Get configuration
// POST /api/config - Save configuration
// GET /api/stats - System statistics
// POST /api/restart - Restart device
// Adding a custom endpoint:
webInterface->addHandler("/api/custom", HTTP_GET, [](AsyncWebServerRequest* req) {
req->send(200, "application/json", "{\"status\":\"ok\"}");
});
// User-facing status
Serial.println(F(">>> Connecting to WiFi..."));
// Success
Serial.println(F("✓ WiFi connected"));
// Error
Serial.println(F("✗ NTRIP connection failed"));
// Debug (only if DEBUG defined)
#ifdef DEBUG
Serial.printf("[DEBUG] Buffer: %d/%d bytes\n", used, total);
#endif
// Statistics
Serial.println(F("=== STATS ==="));
Serial.printf("RX: %lu frames\n", totalFrames);
// config.h
#define DEBUG // General debug output
#define DEBUG_LORA_PACKETS true // LoRa packet details
#define DEBUG_LORA_VERBOSE true // Ultra-verbose (every byte)
After each change, verify:
lib_deps =
thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays@^4.4.0
sandeepmistry/LoRa@^0.8.0
me-no-dev/ESPAsyncWebServer
me-no-dev/AsyncTCP
bblanchon/ArduinoJson@^6.21.3
⚠️ Don’t update libraries without testing!
ESP32 embedded code often breaks with library updates. If you must:
All credentials (WiFi, NTRIP) are now managed via:
// GOOD: Configuration from ConfigManager
SystemConfig config = configManager.getConfig();
WiFi.begin(config.wifiSSID.c_str(), config.wifiPassword.c_str());
// BAD: Hardcoded credentials
#define WIFI_SSID "MyNetwork" // NEVER do this!
#define WIFI_PASSWORD "secret123" // NEVER do this!
// Access Point (fallback mode)
const char* AP_SSID = "RTCM_Transmitter_XXXXXX"; // XXXXXX = chip ID
const char* AP_PASSWORD = "rtcm1234"; // Change in production!
// IMPORTANT: In production add:
// - Rate limiting for API
// - HTTPS for sensitive endpoints
// - Authentication token for REST API
Always when:
../shared/docs/lora-protocol.md../shared/docs/maintenance-frame.md/**
* @brief Brief function description
*
* Longer description if needed - what the function does,
* its assumptions, edge cases, etc.
*
* @param data Pointer to RTCM data
* @param length Data length in bytes
* @return true if transmission successful, false otherwise
*/
bool transmitMessage(const uint8_t* data, size_t length);
// BAD!
void loop() {
loraTransmitter->transmitMessage(data, len); // Not checking state!
}
// GOOD
void loop() {
if (currentState == STATE_TRANSMISSION) {
loraTransmitter->transmitMessage(data, len);
}
}
ESP32 has watchdog - if you block too long, system will restart.
// BAD: Long operation without yield
for (int i = 0; i < 10000; i++) {
processData(i);
}
// GOOD: Yield periodically
for (int i = 0; i < 10000; i++) {
processData(i);
if (i % 100 == 0) {
yield(); // or delay(1)
}
}
// BAD: Allocation in loop() without free
void loop() {
uint8_t* buffer = new uint8_t[1024]; // Leak!
processBuffer(buffer);
// No delete[]!
}
// GOOD: Stack allocation
void loop() {
uint8_t buffer[1024];
processBuffer(buffer);
// Automatically deallocated
}
// BAD: String literal in RAM
Serial.println("This uses precious RAM!");
// GOOD: F() macro stores in flash
Serial.println(F("This is stored in flash!"));
read_file on relevant filesAsk the user instead of guessing:
Before a major change, read:
src/main.cpp - State machine logicinclude/config.h - Current parameters.h and .cpp../shared/docs/lora-protocol.md (shared spec) if changes involve radio../shared/docs/maintenance-frame.md (shared spec) if changes involve maintenance/command framesIf this document doesn’t answer your question:
../shared/docs/lora-protocol.md for protocol detailsLast Update: January 2026
Maintainer: OSS Transmitter Team
Remember: Good code is code that the next developer (or you in 6 months) can understand in 5 minutes! 🚀