# Janos Uart App

> Build LVGL touchscreen applications that communicate with JanOS ESP32C5 firmware over UART. Use when creating screens for Tab5, CoreS3, Cardputer, or any ESP-IDF device that sends UART commands like scan_networks, start_deauth, list_hosts, show_pass. Covers screen building, UART parsing, attack flows, and navigation patterns. Use when this capability is needed.

- Skill: `tomevault-io/janos-uart-app-2` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/janos-uart-app-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/janos-uart-app-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/janos-uart-app-2

---


# 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](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, &current, &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;
}
```

Scale `TILE_WIDTH` and `TILE_HEIGHT` based on display resolution. Tab5 uses 230x140 for a 1280x720 display. For CoreS3 (320x240), use ~100x80 or similar.

### Navigation via Event Callbacks

```c
static void main_tile_event_cb(lv_event_t *e) {
    const char *name = (const char *)lv_event_get_user_data(e);
    if (strcmp(name, "WiFi Scan & Attack") == 0)  show_scan_page();
    else if (strcmp(name, "Global WiFi Attacks") == 0) show_global_attacks_page();
    else if (strcmp(name, "Bluetooth") == 0) show_bluetooth_menu_page();
    // ...
}
```

### Popup Pattern

```c
// Semi-transparent overlay covering the whole screen
overlay = lv_obj_create(lv_scr_act());
lv_obj_remove_style_all(overlay);
lv_obj_set_size(overlay, lv_pct(100), lv_pct(100));
lv_obj_set_style_bg_color(overlay, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(overlay, LV_OPA_50, 0);
lv_obj_clear_flag(overlay, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(overlay, LV_OBJ_FLAG_CLICKABLE);  // block clicks to background

// Centered popup
popup = lv_obj_create(overlay);
lv_obj_set_size(popup, POPUP_W, POPUP_H);
lv_obj_center(popup);
// ... add content to popup ...
```

Close popup: `lv_obj_del(overlay)` deletes both overlay and popup (child).

### Thread-Safe LVGL Updates

LVGL is NOT thread-safe. Any LVGL call from a background UART task must be wrapped:

```c
if (bsp_display_lock(0)) {
    lv_label_set_text(status_label, "Connected!");
    bsp_display_unlock();
}
```

If `bsp_display_lock` is not available, use a mutex or LVGL's built-in `lv_lock()`/`lv_unlock()` (LVGL 9.x).

### Memory Management

- Use **PSRAM** (`heap_caps_malloc(size, MALLOC_CAP_SPIRAM)`) for large data structures (network lists, probe arrays, host tables).
- Use **internal RAM** for small UI state structs and LVGL objects (LVGL manages its own allocator).
- Always free PSRAM allocations in cleanup functions.
- Keep network scan results in a global structure so they survive screen transitions.

## Common Workflows

### 1. Scan-Select-Attack

```
uart_send("scan_networks")
  → wait for "Scan results printed"
  → parse CSV lines into network array
  → show network list with checkboxes
  → user selects networks and taps attack button

uart_send("select_networks 1 3 5")    // 1-based indices
uart_send("start_deauth")             // or start_evil_twin, start_handshake, etc.
  → start monitor task to parse UART output
  → update screen with attack status

// On back/stop:
uart_send("stop")
```

### 2. Evil Twin Flow

```
scan_networks → select_networks → list_sd → user picks HTML
→ select_html <index> → start_evil_twin
→ monitor for: "Client connected", "Password received", "Password verified!"
→ stop
```

### 3. Connect-ListHosts-ARPBan

```
// Check if password known via show_pass evil, or ask user
// For open networks, omit the password:
wifi_connect <SSID>              // open network
wifi_connect <SSID> <password>   // WPA/WPA2 network
  → wait for "SUCCESS" / "FAILED" / "TIMEOUT"

list_hosts
  → wait for "Discovered Hosts", parse IP->MAC lines

// User taps a host:
arp_ban <MAC> [IP]
  → show "ARP Poisoning Active"

// On back:
stop
```

### 3b. Connect-NMAP (Port Scan)

```
wifi_connect <SSID> [password]
  → wait for "SUCCESS" / "FAILED" / "TIMEOUT"

start_nmap [quick|medium|heavy] [IP]
  → Phase 1 (host discovery): parse "Total: N hosts discovered"
  → Phase 2 (port scanning):
      for each "Host: <IP>  (<MAC>)" line → add host to list
      for each "Scanning <IP> ports X-Y [current/total]" → update progress bar
      for each "<port>/tcp  open  <service>" → add open port to current host
      "(no open ports)" → mark current host as no-ports
  → Completion: "Scanned N hosts, found M open ports"

// Can be stopped anytime with:
stop → "(scan stopped by user)"
```

### 4. Portal/Karma Setup

```
list_sd → user picks HTML → select_html <index>
start_portal <SSID>         // or start_karma <probe_index>
  → monitor for client connections and form submissions
  → stop
```

### 5. Wardrive

```
start_wardrive   // or start_wardrive_promisc
  → wait for "GPS fix obtained" (show "Acquiring GPS Fix..." until then)
  → parse CSV network lines as they arrive
  → show "Logged N networks to ..."
  → handle "GPS fix lost!" / "GPS fix recovered:"
  → stop
```

### 6. Beacon Spam from SSID File

```
list_ssids → show indexed SSID list
  → user can add_ssid <name> to append
  → user can remove_ssid <index> to delete
start_beacon_spam_ssids
  → wait for "Beacon spam started. Use 'stop' to end."
  → stop
```

Or with inline SSIDs:
```
start_beacon_spam "SSID1" "SSID2" "SSID3"
  → wait for "Beacon spam started. Use 'stop' to end."
  → stop
```

### 7. PCAP Capture

```
// Radio mode (no prerequisites):
start_pcap radio
  → wait for "PCAP radio capture started -> ..."
  → stop → "PCAP saved: ... (N frames, M drops)"

// Net mode (requires WiFi connection):
wifi_connect <SSID> [password]
  → wait for "SUCCESS"
start_pcap net
  → wait for "PCAP net capture started -> ..."
  → stop → "PCAP saved: ... (N frames, M drops)"
```

### 8. Bluetooth Locate

```
scan_bt → parse device list → show scrollable list
→ user taps device → scan_bt <MAC> → continuous RSSI updates
→ stop
```

## Adapting for Smaller Screens (CoreS3)

When porting from Tab5 (1280x720) to CoreS3 (320x240):

- Reduce tile grid: 2x3 instead of 3x4. Tile size ~100x80.
- Use smaller fonts: `lv_font_montserrat_14` for body, `lv_font_montserrat_18` for titles (instead of 20/28).
- Popups should be nearly full-screen (280x200) since the display is small.
- Scrollable lists are essential -- use `lv_table` or `lv_list` with small row heights.
- Consider a single UART (no Kraken dual-UART mode) to keep complexity low.
- The home button at top should be compact (icon only, no text).
- Tab bar (UART1/UART2/INTERNAL) may not fit; use a dropdown or eliminate tabs for single-UART configs.

## Checklist for New Screens

1. Create `show_<feature>_page()` function
2. Build UI: container, title, content area, back button
3. Send UART command(s)
4. Start background monitor task if response is continuous
5. Parse UART response using line-by-line accumulation pattern
6. Update UI inside `bsp_display_lock()`/`unlock()` from background tasks
7. Implement cleanup: set `monitoring = false`, send `"stop"`, delete LVGL objects, free memory
8. Wire up the back/home button to call cleanup and return to parent screen

---
> Source: [C5Lab/M5MonsterC5-CardputerADV](https://github.com/C5Lab/M5MonsterC5-CardputerADV) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-06-19 -->

