-
Notifications
You must be signed in to change notification settings - Fork 260
99 Contribute
Contributing to this firmware is simple. Below we describe a straightforward method to add a new command to the project.
As a contributor, you usually only need to work on the Controller and Service related to the protocol where you want to add your command.
If you are not familiar with PlatformIO, you can follow the build instructions here:
https://github.com/geo-tp/ESP32-Bus-Pirate/wiki/99-Build
Each terminal command is handled in a Controller (e.g., UartController, I2cController, LedController, etc.).
Example: src/Controllers/UartController
void UartController::handleRead() {
terminalView.println("UART Read: Streaming until [ENTER] is pressed...");
uartService.flush();
while (true) {
// Stop if ENTER is pressed
char key = terminalInput.readChar(); // read a char, non blocking
if (key == '\r' || key == '\n') {
terminalView.println("\nUART Read: Stopped by user.");
break;
}
// Print UART data as it comes
while (uartService.available() > 0) {
char c = uartService.read();
terminalView.print(std::string(1, c)); // print to the terminal
}
}
}-
terminalViewhandles output (print, println) in the web or serial terminal.
class ITerminalView {
public:
virtual ~ITerminalView() = default;
// Initialize the terminal
virtual void initialize() = 0;
// Show welcome message with logo and infos
virtual void welcome(TerminalTypeEnum& terminalType, std::string& terminalInfos) = 0;
// Print to the terminal
virtual void print(const std::string& text) = 0;
virtual void println(const std::string& text) = 0;
virtual void printPrompt(const std::string& mode = "HIZ") = 0;
// Wait press
virtual void waitPress() = 0;
// Clear the terminal
virtual void clear() = 0;
};
-
terminalInputhandles input (readChar for keypresses, etc.) in the web or serial terminal.
class IInput {
public:
virtual ~IInput() = default;
// Blocking read
virtual char handler() = 0;
// Non blocking read
virtual char readChar() = 0;
// Wait an input
virtual void waitPress() = 0;
};Your handleXXX method can optionally take a const TerminalCommand& cmd parameter, which contains the user's input split into root, subcommand, and args.
For example, the UART command:
void UartController::handleSpam(const TerminalCommand& cmd); // Usage: spam <text> <ms>Will be interpreted as:
cmd.getRoot() -> "spam"cmd.getSubCommand() -> "Hello"cmd.getArgs() -> "10"
You can then use ArgTransformer to parse and validate these arguments as needed.
-
ArgTransformerto validate and convert strings intouint8_t,uint32_t, hex, etc. -
UserInputManagerfor interactive prompts and validation (yes/no, pin number, validated strings, hex list, etc.).
Example:
// Split args into a list of string
std::vector<std::string> args = argTransformer.splitArgs(cmd.getArgs());
// Validate string arg
bool valid = argTransformer.isValidNumber(args[0])
// Transform string arg into an unsigned int
uint8_t pin = argTransformer.toUint8(args[0]);
// Ask and validate a char choice
char parityChar = userInputManager.readCharChoice("Parity (N/E/O)", defaultParity, {'N', 'E', 'O'});
// Ask yes or no
bool inverted = userInputManager.readYesNo("Inverted?", state.isUartInverted());
The GlobalState object provides a centralized way to share data across controllers.
You can use it to:
- Store and access pin mappings for each protocol (e.g., UART RX/TX pins, I2C SCL/SDA).
- Share global configuration or flags (e.g., current terminal mode, webui IP, etc).
Example:
int rxPin = state.getUartRxPin();
state.setUartRxPin(rxPin);The Controller is for high-level logic and user interaction. The Service handles protocol-level actions.
Example:
char UartService::read() {
return Serial1.read();
}
void UartService::write(char c) {
Serial1.write(c);
}
These components are already integrated into each controller, you can simply use them directly in your new handleXXX method.
| Component | Responsibility |
|---|---|
| service (uartService, i2cService, etc) | Low-level protocol logic |
| terminalView | Output to terminal (web and serial) |
| terminalInput | Input from user (web and serial) |
| argTransformer | String arguments to typed value |
| userInputManager | Ask for a certain input type and validate it |
| state | Application state shared between controllers |
Example:
void FictiveController::handleXXX(const TerminalCommand& cmd) {
terminalView.println("[FICTIVE] Command execution started.");
// Read arguments from the TerminalCommand
std::string rawValue = cmd.getSubCommand();
if (rawValue.empty()) {
terminalView.println("[FICTIVE] No value provided. Please enter a number");
return;
}
// Validate string using ArgTransformer
if (!argTransformer.isValidNumber(rawValue)) {
terminalView.println("[FICTIVE] Invalid number format.");
return;
}
// Convert string arg into int using ArgTransformer
uint8_t value = argTransformer.toUint8(rawValue);
// Ask user confirmation using UserInputManager
bool confirmation = userInputManager.readYesNo("Are you sure?", false);
if (!confirmation) {
terminalView.println("[FICTIVE] User is not sure. Stopped.");
return;
}
// Use service logic (e.g., simulate sending the value somewhere)
bool result = fictiveService.sendValue(value);
if (result) {
terminalView.println("[FICTIVE] Value sent successfully.");
} else {
terminalView.println("[FICTIVE] Failed to send value.");
}
// Access shared state (e.g., configured pin for this protocol)
int pin = state.getFictiveDataPin();
terminalView.println("[FICTIVE] Used data pin: " + std::to_string(pin));
}
⚠️ Voltage Warning: Devices should only operate at 3.3V or 5V.
Do not connect peripherals using other voltage levels — doing so may damage your ESP32.