JanOS UART Application Builder
Guide for building ESP-IDF + LVGL applications that control JanOS firmware on ESP32C5 via UART. Reference implementation: /Users/janulrich/kod/dev/tabgit/M5MonsterC5-Tab5/main/main.c
For the complete command list, see commands-reference.md.
Architecture
┌────────────────────────────────┐ UART (115200 8N1) ┌──────────────────────┐
│ UI Device (Tab5/CoreS3/etc) │ ──────────────────────────────> │ JanOS on ESP32C5 │
│ - LVGL touchscreen │ <────────────────────────────── │ - WiFi radio │
│ - Sends text commands │ Text responses │ - BLE radio │
│ - Parses text responses │ │ - SD card │
│ - FreeRTOS tasks for I/O │ │ - GPS (optional) │
└────────────────────────────────┘ └──────────────────────┘
JanOS exposes a text CLI over UART. The UI app sends commands like scan_networks\r\n and parses the multi-line text responses to update the screen.
UART Setup
c
#define UART_NUM UART_NUM_1
#define UART_BAUD_RATE 115200
#define UART_BUF_SIZE 4096
static void uart_init(void) {
const uart_config_t uart_config = {
.baud_rate = UART_BAUD_RATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
uart_driver_install(UART_NUM, UART_BUF_SIZE * 2, 0, 0, NULL, 0);
uart_param_config(UART_NUM, &uart_config);
uart_set_pin(UART_NUM, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}
Pin assignments (configurable, stored in NVS):
| Connector | TX | RX | Usage | |
|---|
| Grove | 53 | 54 | UART1 (primary) | |
| M5Bus | 37 | 38 | UART2 (Kraken secondary) | |
For CoreS3: use the appropriate GPIO for your board. The Tab5 reference stores pin config in NVS and reads with get_uart1_pins().
Sending Commands
Always append \r\n. Log every command sent.
c
static void uart_send_command(const char *cmd) {
uart_write_bytes(UART_NUM, cmd, strlen(cmd));
uart_write_bytes(UART_NUM, "\r\n", 2);
ESP_LOGI(TAG, "Sent: %s", cmd);
}
Reading & Parsing Responses
Core Pattern: Line-by-Line Accumulation
All UART parsing follows the same pattern -- read bytes, accumulate into a line buffer character by character, parse on newline:
c
char rx_buffer[UART_BUF_SIZE];
char line_buffer[512];
int line_pos = 0;
while (!done && !timed_out) {
int len = uart_read_bytes(UART_NUM, rx_buffer, UART_BUF_SIZE - 1, pdMS_TO_TICKS(100));
if (len > 0) {
rx_buffer[len] = '\0';
for (int i = 0; i < len; i++) {
char c = rx_buffer[i];
if (c == '\n' || c == '\r') {
if (line_pos > 0) {
line_buffer[line_pos] = '\0';
// === PARSE THIS LINE ===
if (strstr(line_buffer, "COMPLETION_MARKER")) {
done = true;
break;
}
parse_data_line(line_buffer);
line_pos = 0;
}
} else if (line_pos < sizeof(line_buffer) - 1) {
line_buffer[line_pos++] = c;
}
}
}
}
Completion Markers
Every command response has a known end marker. Wait for it before proceeding:
| Command | Completion Marker | |
|---|
scan_networks | "Scan results printed" | |
wifi_connect | "SUCCESS" or "FAILED" or "TIMEOUT" | |
start_nmap | "Scanned" + "open ports" (final summary line) | |
list_hosts | "Discovered Hosts" (header line, data follows) | |
list_sd | "HTML files found" (header, data follows), or timeout | |
show_pass | Timeout (no explicit end marker) | |
list_probes | Timeout (no explicit end marker) | |
wpasec_upload | "Done:" | |
start_pcap | "PCAP radio capture started" or "PCAP net capture started" (initial); on stop: "PCAP saved:" | |
start_beacon_spam | "Beacon spam started. Use 'stop' to end." | |
start_beacon_spam_ssids | "Beacon spam started. Use 'stop' to end." (same as start_beacon_spam) | |
list_ssids | Timeout (no explicit end marker, list ends after last indexed line) | |
add_ssid | "Added SSID:" | |
remove_ssid | "SSID removed." | |
version | "JanOS version: X.Y.Z" (single line, immediate) | |
For commands without explicit end markers, use a timeout with empty-read detection (e.g., 3 consecutive empty reads of 500ms each).
Key Parsing Recipes
Network scan CSV -- fields: index, SSID, (empty), BSSID, channel, security, RSSI, band:
c
// Line: "1","AX3","","C4:2B:44:12:29:20","112","WPA2","-59","5GHz"
static bool parse_network_line(const char *line, wifi_network_t *net) {
if (line[0] != '"') return false;
char temp[256];
strncpy(temp, line, sizeof(temp) - 1);
char *fields[8] = {NULL};
int field_idx = 0;
char *p = temp;
while (*p && field_idx < 8) {
if (*p == '"') {
p++;
fields[field_idx] = p;
while (*p && *p != '"') p++;
if (*p == '"') { *p = '\0'; p++; }
field_idx++;
if (*p == ',') p++;
} else { p++; }
}
if (field_idx < 8) return false;
net->index = atoi(fields[0]);
strncpy(net->ssid, fields[1], sizeof(net->ssid) - 1);
strncpy(net->bssid, fields[3], sizeof(net->bssid) - 1);
net->channel = atoi(fields[4]);
strncpy(net->security, fields[5], sizeof(net->security) - 1);
net->rssi = atoi(fields[6]);
strncpy(net->band, fields[7], sizeof(net->band) - 1);
return true;
}
Host list -- parse " IP -> MAC" lines:
c
if (strstr(line, "Our IP:") != NULL) {
// Extract IP after "Our IP: " up to ","
} else if (strstr(line, "->") != NULL) {
char *p = line;
while (*p == ' ') p++;
// read IP until space, skip " -> ", read MAC
}
Evil twin passwords -- "SSID", "password":
c
if (line[0] == '"') {
char *ssid_start = line + 1;
char *ssid_end = strchr(ssid_start, '"');
if (ssid_end && *(ssid_end+1) == ',' && *(ssid_end+3) == '"') {
*ssid_end = '\0';
char *pass_start = ssid_end + 4;
char *pass_end = strchr(pass_start, '"');
if (pass_end) { *pass_end = '\0'; /* ssid_start and pass_start are your values */ }
}
}
HTML file list -- "N filename.html":
c
if (strstr(line_buffer, "HTML files found") != NULL) {
header_found = true;
} else if (header_found && line_pos > 2) {
int file_num;
char filename[64];
if (sscanf(line_buffer, "%d %63s", &file_num, filename) == 2) {
// store filename at index file_num
}
}
Probe list -- "N SSID":
c
char *p = line;
while (*p == ' ') p++;
if (isdigit((unsigned char)*p)) {
int idx = 0;
while (isdigit((unsigned char)*p)) { idx = idx * 10 + (*p - '0'); p++; }
while (*p == ' ') p++;
if (*p != '\0') {
char ssid[33];
strncpy(ssid, p, sizeof(ssid) - 1);
// trim trailing whitespace
}
}
Deauth detector -- "[DEAUTH] CH: N | AP: name (BSSID) | RSSI: N":
c
static bool parse_deauth_line(const char *line, deauth_entry_t *entry) {
if (!strstr(line, "[DEAUTH]")) return false;
const char *ch = strstr(line, "CH:");
entry->channel = atoi(ch + 3);
const char *ap = strstr(line, "AP:"); ap += 3; while (*ap == ' ') ap++;
const char *paren = strchr(ap, '(');
size_t ap_len = paren - ap;
while (ap_len > 0 && ap[ap_len-1] == ' ') ap_len--;
memcpy(entry->ap_name, ap, ap_len);
const char *paren_end = strchr(paren + 1, ')');
memcpy(entry->bssid, paren + 1, paren_end - paren - 1);
entry->rssi = atoi(strstr(line, "RSSI:") + 5);
return true;
}
Handshake success:
c
if (strstr(line, "HANDSHAKE IS COMPLETE AND VALID")) { /* handshake validated */ }
if (strstr(line, "PCAP saved:")) { /* extract filename from path */ }
if (strstr(line, "handshake saved for SSID:")) { /* extract SSID after "SSID: " */ }
Evil twin password capture:
c
if (strstr(line, "connected to SSID=")) { /* extract SSID between ' quotes */ }
if (strstr(line, "password=")) { /* extract password after = */ }
if (strstr(line, "Password verified!")) { /* attack succeeded */ }
wifi_connect result (password is optional -- omit for open networks):
c
if (strstr(rx_buffer, "SUCCESS")) { connected = true; }
if (strstr(rx_buffer, "FAILED") || strstr(rx_buffer, "TIMEOUT")) { failed = true; }
// Extract DHCP IP from: "DHCP IP: 192.168.0.5, Netmask: 255.255.255.0, GW: 192.168.0.1"
const char *dhcp = strstr(rx_buffer, "DHCP IP:");
if (dhcp) { /* parse IP, Netmask, GW */ }
start_nmap progress and results:
c
// Progress line: " Scanning 192.168.0.4 ports 21-143 [1/100] ..."
if (strstr(line, "Scanning") && strstr(line, "ports") && strchr(line, '[')) {
char ip[16]; int port_from, port_to, current, total;
sscanf(line, " Scanning %15s ports %d-%d [%d/%d]",
ip, &port_from, &port_to, ¤t, &total);
int pct = (current * 100) / total;
// update progress bar
}
// New host: "Host: 192.168.0.4 (00:C0:CA:B4:E6:3F)"
if (strncmp(trimmed, "Host:", 5) == 0) {
char ip[16], mac[18];
if (sscanf(trimmed, "Host: %15s (%17[^)])", ip, mac) == 2) {
// new host with MAC
} else if (sscanf(trimmed, "Host: %15s (MAC unknown)", ip) == 1) {
// new host without MAC
}
}
// Open port: " 135/tcp open MSRPC"
int port; char service[32];
if (sscanf(trimmed, "%d/tcp open %31s", &port, service) == 2 ||
sscanf(trimmed, "%d/tcp open %31s", &port, service) == 2) {
// found open port
}
// Completion: "Scanned 4 hosts, found 4 open ports"
if (strstr(line, "Scanned") && strstr(line, "open ports")) {
int hosts, ports;
sscanf(line, "Scanned %d hosts, found %d open ports", &hosts, &ports);
// scan complete
}
Screen Building
Screen Lifecycle
show_*_page()
├── Create LVGL objects (container, labels, buttons, table)
├── Register event callbacks
├── Start background UART monitor task (if needed)
└── Set as current visible page
[User interacts / UART data arrives]
cleanup (on back/close):
├── Set monitoring_active = false
├── Send "stop" via UART
├── Wait for task to finish (furi_thread_join or vTaskDelete)
├── Delete LVGL objects (lv_obj_del)
├── NULL all pointers
└── Free allocated memory
Tile (Button) Creation
c
static lv_obj_t *create_tile(lv_obj_t *parent, const char *icon_text,
const char *label_text, lv_color_t bg_color,
lv_event_cb_t cb, const char *user_data) {
lv_obj_t *tile = lv_btn_create(parent);
lv_obj_set_size(tile, TILE_WIDTH, TILE_HEIGHT);
lv_obj_set_style_bg_color(tile, bg_color, LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(tile, lv_color_lighten(bg_color, 50), LV_STATE_PRESSED);
lv_obj_set_style_radius(tile, 12, 0);
lv_obj_set_style_shadow_width(tile, 0, 0);
lv_obj_set_flex_flow(tile, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(tile, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *icon = lv_label_create(tile);
lv_label_set_text(icon, icon_text);
lv_obj_set_style_text_font(icon, &lv_font_montserrat_28, 0);
lv_obj_set_style_text_color(icon, lv_color_white(), 0);
lv_obj_t *label = lv_label_create(tile);
lv_label_set_text(label, label_text);
lv_obj_set_style_text_color(label, lv_color_white(), 0);
if (cb) lv_obj_add_event_cb(tile, cb, LV_EVENT_CLICKED, (void *)user_data);
return tile;
}