SUS Serial Monitor & Plotter
A serial monitor that keeps up with your board.
Multi-port terminal, live plotter and ESP32 backtrace decoding — inside VS Code, wired into PlatformIO, ESP-IDF and Arduino CLI.
English · Русский

English
Contents
Why
Most serial monitors hand you a text stream and stop there. That is fine until the board reboots in the middle of a log, panics with a raw Backtrace: 0x400d1a2b:0x3ffb1f30, or you need two boards side by side on one timeline.
This extension is built for that part of the job:
- it owns the port itself — so board reset,
setup() output and multiple ports on a shared clock all work, instead of being lost to a wrapped CLI monitor;
- it reads your project —
platformio.ini, sdkconfig or sketch.yaml: port and baud rate come from there, and the port is handed back automatically while you flash;
- it decodes ESP32 backtraces into
function → file:line from the ELF your build already produced.
Features
- 📟 Terminal — RX/TX over UART with millisecond timestamps, autoscroll and colour-coded RX / TX / system lines
- 🔌 Multiple ports at once — two or more boards in one panel, every line and every plot series tagged with a coloured port label (
P1, P2, …), on one shared timeline
- 🧠 Port profiles — baud rate and line ending are remembered per board, per workspace: the board that needs 921600 and a bare
\n comes back that way next time, without the settings screen
- 🔎 Live filter — hides non-matching lines as they arrive, and the ones already on screen
- ⌨️ Command history and snippets — ↑ and ↓ in the send field walk the commands of this session, the same list sits next to Send, and saved snippets from the settings join it
- 📈 Live plotter (Chart.js) — plots
name:value pairs and bare numbers found in incoming lines, series named after your own keys, several series per port and several ports on one chart, or one port at a time when two boards disagree about the scale
- 🖼️ Chart export — PNG snapshot and CSV of the raw points for Excel or pandas, following whatever the chart currently shows
- 🔢 HEX dump — a third tab with the bytes as they arrived: offset, bytes and an ASCII column, 8, 16 or 32 per row, one port at a time — the stream before decoding, where a
0x00 or a binary frame is a number instead of nothing
- 💾 Real-time file logging — every line is appended to a file as it arrives, not only when you press save
- 📊 Debug statistics — idle time, round-trip time and error / warning / crash / reboot counters, all per port, with the totals across boards on their own row
- 🧩 Backtrace decoding —
Backtrace: lines are resolved to function → file:line through the toolchain's own addr2line, with a bundled DWARF parser as the fallback
- 🪦 Core dump parsing — the base64 block a panic leaves behind is collected instead of printed, unpacked into the tasks it holds, and the crashed one comes out as a decoded backtrace like any other
- 🔄 Board reset per port — DTR/RTS pulse from the port chip, and the control lines are left deasserted so a physical RESET runs your firmware instead of dropping into the bootloader
- ⚙️ PlatformIO integration — port and speed from
platformio.ini, automatic release and reconnect around upload tasks
- ⚙️ ESP-IDF integration — project detected from
sdkconfig and CMakeLists.txt, baud rate from sdkconfig, port from the idf.* settings of the Espressif extension, the freshest ELF picked up from build/, and a separate reset sequence for the built-in USB Serial/JTAG
- ⚙️ Arduino CLI integration — sketch folder detected from
sketch.yaml or the .ino itself, port and board from the build profile, and the baud rate read out of your own Serial.begin() when the project file says nothing about it
- ⚙️ Editor settings — theme, baud rate, line ending, plot depth, timestamps and autoscroll live in the VS Code settings screen, searchable and synced
- 🎨 Six themes — VS Code Dark/Light, Monokai, Dracula, Solarized Dark, Hacker (Matrix), remembered between sessions
- 🌐 English and Russian — the panel, the palette entries and the log follow the editor display language
- 📄 Export and copy — terminal to
.txt, or the whole log to the clipboard
- ⚡ Native port access — the port is opened by the
serialport module in the extension host, not by a wrapped CLI
Requirements
| Requirement |
Notes |
| VS Code |
1.84 or newer. |
| A PlatformIO, ESP-IDF or Arduino project in the folder |
The extension activates on any of them. Without one the monitor still works, but the project integration is unavailable. |
| PlatformIO IDE / ESP-IDF extension / arduino-cli |
Not required. PlatformIO only supplies the build tasks the monitor synchronises with; ESP-IDF only supplies the idf.* settings that are read when present; for Arduino only the files in the sketch folder are read, and nothing is ever run. |
| Device driver |
The usual one for your board (CP210x, CH340, FTDI and friends), installed in the OS. |
| Native module |
serialport ships with the extension, built through N-API so it works across Electron versions. See Troubleshooting if a port refuses to open. |
Quick start
- Open a folder that contains a
platformio.ini, an ESP-IDF project (CMakeLists.txt and sdkconfig) or an Arduino sketch (sketch.yaml or just the .ino).
- Put the cursor in any editor and press Ctrl+K M (Cmd+K M on macOS) — the monitor opens and connects to the board in one step. It is a chord: hold Ctrl, press K, release, press M.
- Type a command in the field at the bottom and press Enter. The line ending is selectable,
\r\n by default.
Serial.printf("temp:%.1f hum:%d\n", t, h) shows up on the Plotter tab as two named series; bare numbers work too.
- To add a second board, press + Add port in the toolbar and pick it from the list.
Nothing arriving? Run SUS Monitor: Show Log. Port errors and symbol-loading problems are written there and to the terminal, so that is where the reason lives — port did not open, firmware.elf not found, upload task not recognised.
Commands
Available from the Command Palette (Ctrl+Shift+P); typing SUS brings up all three.
| In the palette |
What it does |
SUS Monitor: Open Monitor |
Opens the panel without touching any port |
SUS Monitor: Open and Connect to Board |
Opens and connects using the project settings (Ctrl+K M, editor focus required) |
SUS Monitor: Show Log |
Opens the SUS Monitor diagnostic channel in the Output panel |
The panel also has an icon in the editor title bar while platformio.ini, sdkconfig, sketch.yaml or a .ino file is the active file.
Settings
Declared under SUS Serial Monitor in the editor's own settings screen (Ctrl+,, then search for SUS Monitor), so they are searchable and travel with Settings Sync.
They decide what the panel starts with, not what it is stuck with. The toolbar keeps the same controls for things you change every five minutes, and what you pick there lasts until the panel is closed without touching your settings. Two things are written back, because they are chosen for good rather than for a minute: the theme, and the baud rate and line ending of a port you have selected — those land in port profiles. Editing a setting while the monitor is open reaches the panel straight away.
| Setting |
Default |
What it does |
susSerialMonitor.theme |
dark |
Colour theme of the monitor. Also changeable in the panel — the toolbar selector writes here. |
susSerialMonitor.baudRate |
115200 |
The speed the panel starts with, and the speed used for a port you add by hand. Open and Connect ignores it and takes the speed from the project instead: monitor_speed in PlatformIO, CONFIG_ESPTOOLPY_MONITOR_BAUD in ESP-IDF (or idf.monitorBaudRate, which wins over both), port_config.baudrate or the Serial.begin() of the sketch in an Arduino project. A port profile for that very board wins over all of them. |
susSerialMonitor.lineEnding |
\r\n |
What to append to a command sent to the port. A port profile overrides it for the board that has one. |
susSerialMonitor.portProfiles |
{} |
Baud rate and line ending remembered per board, filled in by the panel — see Port profiles. Stored in the workspace (.vscode/settings.json), so every project keeps its own. |
susSerialMonitor.commandSnippets |
[] |
Saved commands listed next to the input field — see Command snippets. Pick one to put it in the field; nothing is sent until you press Enter. Edit the list in the settings, per workspace. |
susSerialMonitor.plotHistory |
50 |
How many recent points the chart keeps. More points, longer history, heavier redraw. |
susSerialMonitor.plotParseMode |
auto |
What the plotter takes from a line: auto — name:value pairs when the line has any, bare numbers otherwise; pairs — pairs only; numbers — bare numbers by position. Also changeable in the plotter toolbar. |
susSerialMonitor.showTimestamp |
true |
Show a timestamp on every monitor line. |
susSerialMonitor.autoscroll |
true |
Scroll the monitor to the newest line as data arrives. |
susSerialMonitor.limitTerminalLines |
true |
Keep only a bounded window of lines in the panel. Turn off to keep the whole session log; at high data rates the DOM grows without limit and can fall behind the stream. |
susSerialMonitor.maxTerminalLines |
10000 |
How many recent lines the panel keeps when limitTerminalLines is on. Older lines are dropped in batches, with a one-time notice. |
susSerialMonitor.openInNewWindow |
true |
Open the monitor as a separate editor window instead of a tab next to the code. Needs VS Code 1.85+; on older versions it stays a tab and the reason goes to the log. |
A value that is not in the list of choices is ignored, and the panel keeps the built-in default — a typo in settings.json cannot leave a selector empty.
Interface
| Element |
Purpose |
| + Add port |
Picks a port from a QuickPick list — boards first — and connects it as a new session |
| Baud rate |
With a port selected, the speed of that port: a change reopens it and goes into its profile. With nothing connected, the speed for the next port you add |
| Theme selector |
Colour theme of the monitor; the choice is written back to settings |
| Port chips |
One per connected port: colour tag, speed, reset (↺) and disconnect (✕). A click on the chip itself points the toolbar at that port, and the selected chip is outlined |
| 🧩 .elf/.map |
Loads a symbol file for backtrace decoding by hand; .elf gives file and line, .map only names |
| 🔴 Log to file |
Starts and stops real-time logging to a file |
| Monitor / Plotter / HEX |
Tab switch |
| Autoscroll / Time |
Terminal behaviour toggles |
| Filter field |
Hides log lines that do not contain the substring — the lines of the selected port, or the whole log while no port is connected |
| Chart port |
Which port the chart shows — All ports or one of them; a single board gets the whole Y axis to itself. Shown on the plotter tab |
| Parsing mode |
What the chart takes from a line: Auto, name:value or Numbers; shown on the plotter tab |
| Points: |
How many recent points the plotter keeps, 50 by default |
| 🖼️ PNG / 📄 CSV |
Chart export, shown on the plotter tab; both follow what the chart shows, and the file name names the port when one is picked |
| Dump port |
Which port the HEX dump shows; shown on the HEX tab |
| Bytes per row: |
Row width of the dump — 8, 16 (default) or 32; shown on the HEX tab |
| RX counter |
Live traffic across all ports: bytes, lines, bytes per second |
| 📊 Statistics |
Opens the debug statistics panel |
| Clear |
Clears terminal, chart and counters; ports stay connected |
| Copy |
Copies the whole terminal log to the clipboard |
| Save log |
Exports the log to a .txt file once |
Target port for the command, line ending (none, \n, \r, \r\n — default), the command field itself, Send and the History list.
The target-port selector doubles as the port selection: whichever port it points at is the port the toolbar talks about, and the line ending next to it belongs to that port and is remembered in its profile.
Sent commands are remembered for the session: ↑ and ↓ in the command field walk them the way a shell does, and the History list next to Send holds the same commands, newest first, for picking one with the mouse. A recalled command lands in the field ready to edit; anything you had half-typed before pressing ↑ comes back when you walk past the newest entry with ↓.
Command snippets
The Snippets list next to History holds commands saved in the
susSerialMonitor.commandSnippets setting — edited per workspace in the
settings screen, so every project keeps its own. Picking a snippet puts it in
the command field, ready to edit and send; nothing is sent until you press
Enter. Unlike the session history, snippets survive closing the
panel: they are the commands you type every time a board boots, without typing
them again.
Multiple ports at once
Each + Add port creates an independent connection, as many as the OS will grant.
- Every port keeps its own baud rate and line ending — from its profile when it has one, otherwise from the toolbar at the moment it was connected.
- Terminal and plotter are shared, but each line and series carries the colour and tag of its port — two devices line up on one timestamp scale.
- One port is selected at a time, and the toolbar controls that belong to a port — baud rate, line ending, filter — talk about that one. Click a chip or use the port selector next to the command field to move the selection; the titles of those controls name the port, so the selection is never guesswork.
- A newly connected port does not steal the selection: adding a second board while you are working with the first leaves the toolbar and the command field where they were.
- The filter belongs to the selected port, so a search across one board's log does not blank out the other's. The first time it matters — a filter typed with two or more ports connected — the monitor says so in the log, once.
- Reset (↺) and disconnect (✕) act on one port, from its own chip.
- Idle time, RTT and the error / warning / crash / reboot counters are all measured per port, so a single hung or misbehaving device is visible instead of a number that says only "twelve errors somewhere". The 📊 Statistics panel has a row per board and an All ports row with the totals.
- Counters of a board you disconnect stay in the panel, dimmed, because its lines stay in the terminal. Clear is what resets them, together with the terminal and the chart.
- The chart is one for all ports, but it can be pointed at a single board — see Plotter.
Port profiles
Two boards on the desk rarely want the same settings. The ESP32 talking at 921600 with a bare \n and the STM32 at 115200 with \r\n used to mean two trips to the toolbar every time the panel opened. A profile is the memory of those two choices, kept per board and per project.
- What is remembered: the baud rate and the line ending. Both are written the moment you change them in the toolbar with a port selected — never on connect, so opening the monitor to look at a log leaves your
settings.json alone.
- When it is applied: on connect, and the profile wins over everything else — over the toolbar and over the project file (
monitor_speed, CONFIG_ESPTOOLPY_MONITOR_BAUD, port_config.baudrate). The same rule idf.port already follows: the project file says what the firmware was built with, the setting says what you decided afterwards. When the profile's speed differs from the one that was asked for, the monitor says so in the log rather than changing the speed silently.
- Changing the speed reopens the port. A baud rate is fixed when the driver opens the port, so there is no other way; the monitor closes the port, opens it again at the new speed, prints
Reopening P1 at 921600 baud and hands the selection back to the same board. The new line ending takes effect on the next command, with nothing to reopen.
- Which board is which: the key is the board's serial number as the OS reports it (
sn:…); a device without one — a CH340 clone, typically — falls back to its USB VID and PID (usb:1a86:7523), which identifies the model rather than the board, so two such clones share a profile. The key survives a replug and an editor restart, and it does not depend on the port name: the same board is recognised on COM7 today and COM12 tomorrow.
- Where it is stored:
susSerialMonitor.portProfiles in the workspace, that is .vscode/settings.json, so a profile belongs to the project it was made in. With no folder open there is nowhere to put it and it goes to the user settings instead. The value is a plain object you can read and edit:
"susSerialMonitor.portProfiles": {
"sn:0001A2B3": { "baudRate": 921600, "lineEnding": "\n" },
"usb:1a86:7523": { "baudRate": 115200 }
}
- Forgetting a board: delete its entry, or the whole setting, in the settings screen. Nothing else refers to those keys.
- A value that makes no sense — a speed that is not a number, a line ending that is not one of the four — is ignored, and the port opens with the toolbar's own choice. Neither a typo in
settings.json nor a board without a serial number can keep a port from opening; a board that cannot be identified simply has no profile to remember.
Plotter
- What a line contributes to the chart is up to the parsing mode, picked in the toolbar next to Points: or set once in
susSerialMonitor.plotParseMode:
- Auto (default) —
name:value pairs when the line has any, bare numbers otherwise;
- name:value — pairs only; a line without a pair leaves the chart untouched, which is what separates telemetry from the rest of the log;
- Numbers — bare numbers only, by position, the way it worked before the modes existed.
- A pair is
name:value or name=value, optionally led by > as Teleplot writes it. Values are separated by whitespace, commas or semicolons.
- A named series is identified by its name rather than by its place in the line, so a board printing
temp:25 on one line and hum:40 temp:26 on the next still draws two series (P1 · temp, P1 · hum) instead of four. Unnamed values keep positional series per port (P1 · S1, P1 · S2, P2 · S1, …).
- Only decimal numbers are plotted — with a sign, a decimal point and an exponent. Hex is left alone on purpose:
Number('0x400d1234') is a valid 1074795572, and addresses out of a boot log used to land on the chart next to real data.
- Series are coloured from the current theme's palette, and the colour follows the series across a theme change.
- A missing value leaves a gap rather than a drop to zero.
- History depth is the Points: field, 50 by default.
- One chart, one port at a time when you need it. The leftmost selector on the plotter tab is All ports by default; pick a board and only its series are drawn. The Y axis is shared by everything visible, and that is the whole point: a series of 0…3.3 next to one of 0…4095 flattens into a line at the bottom, and a hidden series does not count towards the axis, so the chosen board gets the full height back. A board that starts talking later does not steal the view; if the chosen board is disconnected, the chart falls back to all ports, because an empty chart looks like a fault rather than like a closed port.
- PNG saves the current canvas; CSV exports every point of every visible series with its timestamp. When one port is shown, both file names carry it —
plot_P1_….png, plot_data_P1_….csv — so two exports of two boards are not the same file twice.
HEX dump
The HEX tab shows the bytes as the port handed them over — before any decoding into text, so a byte the terminal cannot show is a number here instead of nothing: a 0x00, a broken UTF-8 sequence, a Modbus or SLIP frame, a stray 0x0d that makes lines overwrite each other.
00000000 48 65 6c 6c 6f 20 77 6f 72 6c 64 0d 0a 00 ff 41 |Hello world....A|
- The offset on the left is absolute — counted from the moment the port opened, not from the top of the window — so it says how much has already scrolled past.
- The ASCII column shows printable bytes as characters and everything else as a dot, which is what makes a text protocol readable and a binary one recognisable.
- Bytes per row is 8, 16 (default) or 32. 8 is for hand-counting a short frame, 32 for a wide window.
- One port at a time, picked in the toolbar next to the row width. Each port keeps its own buffer: bytes from two boards interleaved in one dump would make the offsets meaningless, and a foreign byte inside your frame is a protocol bug you would then go looking for and never find.
- The buffer keeps the last 8 KiB per port. The dump is rebuilt whole on every redraw, so this is a deliberate window on the recent past rather than a full recording — the whole session belongs in a file, through 🔴 Log to file.
- Clear empties the dump along with the terminal and the chart, and restarts the offsets from zero.
- Nothing is written to the settings: the port and the row width are session choices, and the tab is drawn only while it is visible, so leaving the monitor on the text terminal costs nothing.
ESP32 debugging
Errors, warnings, reboots and crashes are classified and counted as they arrive, separately for each port — the counters live in the 📊 Statistics panel, a row per board plus an All ports row.
| Counter |
What matches |
| Errors in the log |
the E level tag (E (123) tag:, [E], E/tag:), or the words error, ошибка, exception, fail, failed, panic, паника |
| Warnings in the log |
the W level tag (W (123) tag:, [W], W/tag:), or the words warning, warned, предупреждение |
| Crashes (backtrace) |
a Backtrace: line, Guru Meditation, fatal exception, abort() was called, assert failed |
| Reboots detected |
ESP32/Arduino boot markers: rst:0x, ets Jun, ets Jul, rebooting, reset reason, boot:0x13, power on reset |
A level tag outweighs the words: W (123) wifi: connect failed counts as a warning
rather than an error, even though the text says failed. A crash or a reboot marker
outweighs both. Except for [E] and [W], which the Arduino core prints after a
timestamp, a level tag is only recognised at the start of a line.
When a Backtrace: line appears, its addresses are decoded in place under the line:
Backtrace:0x400d1fb9:0x3ffb21a0 0x400d2c85:0x3ffb21c0
[DECODE] 0x400d1fb9 → loop() at main.cpp:142
Symbols are picked up on their own — the newest ELF from the build folder is loaded when the panel opens (.pio/build for PlatformIO, build/ for ESP-IDF and for an Arduino sketch built with Export compiled binary), so there is nothing to point at by hand. The 🧩 .elf/.map button is there for the cases where you want a different build.
Decoding goes through the toolchain's own addr2line first, which is what makes exact file:line work on current ESP-IDF builds: they emit DWARF 5, and the bundled parser reads only DWARF 2–4. When addr2line is unavailable or fails, the bundled parser takes over, and after it the ELF symbol table — there a frame degrades to symbol +0xoffset, a correct name without a line number. Every fallback writes its reason to the log.
Core dump
A panic can end in something bigger than a Backtrace: line. With CONFIG_ESP_COREDUMP_ENABLE_TO_UART the panic handler prints a whole core dump — a few hundred lines of base64 between CORE DUMP START and CORE DUMP END — which is a snapshot of every task, not just the one that died. The monitor collects that block instead of printing it, and reports what is inside:
[COREDUMP] Core dump started — collecting the block instead of printing it
[COREDUMP] Core dump: 8 tasks, Xtensa, 21504 bytes in 336 lines
[COREDUMP] Crashed task, TCB 0x3ffb2f10:
[DECODE] 0x400d1fb9 → publish() at mqtt.cpp:88
[DECODE] 0x400d2c85 → loop() at main.cpp:142
[COREDUMP] Other tasks, the top frame of each:
[DECODE] TCB 0x3ffb8a40: 0x4008a1c2 → vTaskDelay() at tasks.c:1580
Core dump checksum='2b4e9f01'
- The addresses go through the same decoder as an ordinary backtrace, in one
addr2line call for the whole dump, so the frames come out as function → file:line when symbols are loaded.
- The crashed task is unwound in full — up to 16 frames — and named by the extra note the panic handler writes. Every other task contributes its top frame, which is enough to see who was waiting on what without burying the crash.
- The block itself never reaches the terminal, and neither does it count as another crash: the panic line above it was already counted. The
checksum= line after the end marker is an ordinary line and stays visible.
- A lost end marker cannot eat the session. A second
CORE DUMP START, a disconnect, or 6 MB of body all close the block and report it as cut short, with whatever was collected still parsed.
- On RISC-V the dump gives two frames at most (
pc and ra): unwinding deeper needs the CFI tables of the firmware, which the dump does not carry. Xtensa needs no such tables — its frames are found in the stack itself.
- The old binary core-dump format (
CONFIG_ESP_COREDUMP_DATA_FORMAT_BIN) holds no registers, so only its task count can be reported; the monitor says so and names the setting to change.
monitor_port and monitor_speed are read from platformio.ini, honouring [env] inheritance and default_envs.
- With no
monitor_port set, the first port that looks like a board wins — matched on the USB VID of the usual suspects (CP210x 10c4, CH340 1a86, FTDI 0403, Arduino 2341, Espressif 303a, Adafruit 239a) and on the device name. If nothing looks like a board, the first available port is used.
- When a PlatformIO upload task starts, the port is released so the bootloader can take it.
- When the task ends, the monitor reconnects with retries, because a board does not come back to the OS instantly. If the upload failed, the port stays closed.
- Opening a port leaves DTR and RTS deasserted. Both lines at the same level leave the auto-reset circuit alone, so pressing RESET on the board runs your firmware. With DTR asserted and RTS clear — what the Windows driver defaults to — the same press lands in
rst:0x1 (POWERON),boot:0x2 (DOWNLOAD(USB/UART0)) instead.
ESP-IDF integration
A folder is treated as an ESP-IDF project when its CMakeLists.txt pulls in $ENV{IDF_PATH}/tools/cmake/project.cmake, or when a sdkconfig sits next to a CMakeLists.txt. Plain CMake projects are left alone. platformio.ini is checked first and wins: a PlatformIO project built with framework = espidf carries both markers, but it is PlatformIO that builds it, and its ELF lives in .pio/build.
- Baud rate comes from
CONFIG_ESPTOOLPY_MONITOR_BAUD in sdkconfig, falling back to the flat CONFIG_MONITOR_BAUD, then to the old per-value boolean key CONFIG_MONITOR_BAUD_*B used before IDF 4.3, then to project_description.json, then to the console baud rate, and finally to 115200. The sdkconfig keys are preferred over the generated description because menuconfig writes them the moment you change them, while the description trails by one unbuilt edit. Before the first build, sdkconfig.defaults is read instead.
- Port comes from the Espressif extension's settings —
idf.port, or idf.portWin on Windows. There is no port in sdkconfig at all, and the setting is what you last picked in ESP-IDF: Select Port, so it is the more precise source. idf.monitorBaudRate and idf.buildPath are honoured the same way. These keys are read through the ordinary settings API, so having them in settings.json is enough — the Espressif extension does not have to be installed. They apply to ESP-IDF projects only, never to a PlatformIO one sitting next to them.
- Symbols are the newest ELF in
build/ — app_elf from project_description.json when it is there, otherwise the freshest .elf that is not the bootloader. idf.buildPath moves the search along with the build folder.
- Flashing releases the port. For ESP-IDF that covers
flash, upload and monitor tasks: the Espressif extension opens the port with its own monitor, and two monitors on one port do not get along. Russian task names are recognised too. The monitor reconnects with retries once the task is over.
- Built-in USB Serial/JTAG (VID
303a, PID 1001 — ESP32-S3, C3, C6 and friends without an external USB-UART) gets its own reset sequence. The chip provides its own USB, so resetting it makes the port disappear from the OS and come back a moment later; the monitor releases the handle and reopens the port at the same baud rate instead of reporting a dead connection. If the console is routed to the built-in USB — primary or secondary, CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG or CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG — auto-connect picks that port rather than the external bridge: a devkit shows both, and only one of them carries the log. An explicit idf.port still wins, since ports are only scanned when no port is set.
Arduino CLI integration
A folder is treated as an Arduino sketch when it holds a sketch.yaml (the sketch project file of arduino-cli 0.21 and newer), or, failing that, a .ino in its root — sketches without a project file are still the common case. Subfolders one level down are checked too, so a folder full of sketches works. platformio.ini and the ESP-IDF markers are both checked first and win: a PlatformIO project on the Arduino framework keeps its sketches in src/, and an IDF project may carry an .ino next to the arduino component, but in both cases it is the other build system that produces the ELF.
- Baud rate comes from
port_config.baudrate of the chosen profile, then from the top-level default_port_config, then from the Serial.begin() of the sketch, and finally from 115200. The sketch is a real source here, not a guess of last resort: Arduino has no monitor_speed equivalent, so in most projects the speed is written nowhere else. Only Serial and USBSerial count — Serial1.begin(9600) is usually a GPS on the second UART. A #define argument is resolved, commented-out calls are ignored, and the main .ino is read first because setup() lives there.
- Port and board come from the build profile:
default_profile if it names one, otherwise the only profile there is. port and fqbn of the profile win over the top-level default_port and default_fqbn. A port with a non-serial protocol is ignored — network flashing puts an IP address there, and it is not something to open as a port. The profile name (or the FQBN when there is no profile) is shown in the panel the way a PlatformIO environment is.
- Symbols are the newest ELF in
build/<vendor>.<arch>.<board>/. arduino-cli only puts a build there with --export-binaries, or Export compiled binary in the Arduino IDE; the default build path is a temp folder with a hash in its name, which cannot be guessed. Without the export, point at the ELF with 🧩 .elf/.map.
addr2line is looked up in the arduino-cli data folder — packages/<vendor>/tools/<package>/<version>/bin, under Arduino15 in AppData on Windows, in ~/Library on macOS, ~/.arduino15 elsewhere, and wherever ARDUINO_DIRECTORIES_DATA points. Both naming generations of the esp32 core are searched (xtensa-esp32s3-elf-gcc and riscv32-esp-elf-gcc in 2.x, esp-x32 and esp-rv32 in 3.x), and a binary whose name carries the chip is preferred over a same-family one, because the wrong architecture decodes silently into nonsense.
- The chip is read out of the board id in the FQBN, not assumed to be the board:
esp32:esp32:esp32s3box is an S3, esp32:esp32:esp32da is a plain ESP32. When the id says nothing about the chip (um_tinys3 and friends), all suitable toolchains are tried.
- The built-in USB Serial/JTAG is recognised from the board menu options in the FQBN rather than from a config file:
CDCOnBoot=cdc (cdc_on_boot=1 in older cores) routes the console there, so auto-connect prefers that port. On S2, S3 and P4 an explicit USBMode other than hwcdc cancels that — the console then goes to TinyUSB CDC, which is a different device with a different PID.
- Flashing releases the port for tasks that mention Arduino: arduino-cli tasks are written by hand in
tasks.json, so the word in the task name is what identifies them. A monitor task keeps the port, the same way it does for PlatformIO.
Themes
| Theme |
Style |
| 🌙 VS Code Dark |
Dark, default |
| ☀️ VS Code Light |
Light |
| 🍬 Monokai |
The classic editor dark theme |
| 🧛 Dracula |
Purple and pink accents |
| 🌊 Solarized Dark |
Muted, low contrast |
| 💻 Hacker (Matrix) |
Monochrome green terminal |
Switching a theme repaints the interface, the plot palette and the Chart.js axes. Port tag colours stay fixed on purpose, so a board keeps its identity across themes.
Known limitations
- Exact
file:line on a DWARF 5 build needs the toolchain's addr2line; without it the bundled parser covers DWARF 2–4 only, and a DWARF 5 frame falls back to symbol-table resolution — a name without a line number. The standalone HTML build always uses the bundled parser.
- One symbol file is shared by all ports — two boards running different builds cannot both be decoded precisely at the same time.
- Command history lives for the session only — closing the panel forgets what was sent. Saved snippets are separate: they live in the settings and survive the session.
- A port profile remembers the baud rate and the line ending, but not the filter: filters are per port and last for the session only.
- A board the OS reports no serial number for is identified by USB VID and PID, so two identical clones share one profile. In the standalone HTML build that is the only key available at all — the Web Serial API withholds serial numbers — so there any two boards of the same model always share a profile, and it is kept in the browser's
localStorage rather than in a workspace.
- There is one chart, not one per port: it can be pointed at a single board, but two boards cannot be watched side by side on separate axes.
- The HEX dump is a window on the last 8 KiB of each port, not a recording: older bytes scroll out of it (the offset says how many). Save the whole stream with 🔴 Log to file, and note that the file gets the decoded text, not the bytes.
- Project integration covers PlatformIO, ESP-IDF and Arduino CLI; a bare folder is not recognised as a project. The panel itself still opens anywhere — the status bar button and the palette do not depend on a project.
- An Arduino sketch has to be built with Export compiled binary (
--export-binaries) for backtraces to be decoded on their own: the default build folder is a temp directory with a hash in its name, and there is nothing there to find.
- A parsed core dump names its tasks by TCB address, not by name: the task name sits at an offset inside the FreeRTOS TCB that moves with the build configuration, and a guessed offset would print convincing nonsense. The dump's own checksum line is not verified either — it is shown as it arrived.
Troubleshooting
| Symptom |
Cause and fix |
Port is busy in another program |
Something else holds the port — usually the PlatformIO monitor terminal or a running upload. Close it and reconnect. |
No native build was found for platform=… runtime=electron abi=… |
The bundled serialport binary does not match your editor's Electron. Rebuild it against the version from Help → About: npx @electron/rebuild -v <Electron version> |
| Permission denied on Linux |
Add your user to the dialout group and re-login. |
Backtrace shows symbol +0xoffset |
The address came from the symbol table: addr2line was not found in the toolchain or failed, and the build uses DWARF 5, which the bundled parser skips. The function name is correct, the line number is missing. Open SUS Monitor: Show Log — the reason for the fallback is written there. |
| Ctrl+K M does nothing |
Open SUS Monitor: Show Log first: if there is no Command susSerialMonitor.openAndConnect line, the keypress never reached the extension. Likely causes, in order: focus is not in an editor (the binding declares editorTextFocus); the chord was typed too slowly and VS Code dropped it; the built-in Change Language Mode command, which also ships on Ctrl+K M, won the key. Check Ctrl+K Ctrl+S → filter ctrl+k m → right-click → "Show Same Keybindings", and rebind if something else owns it. |
| Nothing happens at all |
Open SUS Monitor: Show Log. Every silent fallback writes its reason there. |
| The interface is in the wrong language |
The monitor follows the editor language. Run Configure Display Language from the palette and reload the window — the panel, the palette entries and the log all switch together. Any language other than Russian gets the English interface. |
Also available without VS Code
The same monitor builds into a single self-contained HTML file that runs on the Web Serial API — double-click it in Chrome or Edge, no install, no Node. Both targets are generated from one shared core, so features land in both. See the developer README for build instructions.
Roadmap
The plan is complete: DWARF 5 backtraces via the toolchain, ESP-IDF core dumps,
a HEX view, command history and snippets, warning classification, per-board
port profiles, named plot series, per-port counters and charts, and Arduino CLI
support are all in. Ideas and bug reports are welcome at
github.com/iliasnovickov-cmd/SUS-monitor-vs-code/issues.
License
MIT
Русский
Содержание
Зачем
Большинство мониторов порта отдают поток текста и на этом заканчиваются. Это устраивает до первой перезагрузки платы посреди лога, до первой паники с сырым Backtrace: 0x400d1a2b:0x3ffb1f30 и до момента, когда две платы нужно увидеть рядом на одной шкале времени.
Расширение сделано именно под эту часть работы:
- порт открывает само — поэтому сброс платы, вывод
setup() и несколько портов с общими часами работают, а не теряются внутри обёрнутого CLI-монитора;
- читает ваш проект —
platformio.ini, sdkconfig или sketch.yaml: порт и скорость берутся оттуда, а на время прошивки порт отдаётся автоматически;
- декодирует backtrace ESP32 в
функция → файл:строка по тому ELF, который уже собрал ваш проект.
Возможности
- 📟 Терминал — приём и отправка по UART с метками времени до миллисекунды, автоскроллом и цветовой подсветкой RX / TX / системных сообщений
- 🔌 Несколько портов одновременно — две и более платы в одной панели, каждая строка и каждая линия графика помечена цветным тегом порта (
P1, P2…), общая шкала времени
- 🧠 Профили портов — скорость и окончание строки запоминаются для каждой платы в рамках проекта: плата, которой нужны 921600 и одиночный
\n, в следующий раз откроется так же, без похода в настройки
- 🔎 Живой фильтр — скрывает несовпадающие строки на лету, и уже показанные, и все приходящие дальше
- ⌨️ История команд и сниппеты — ↑ и ↓ в поле ввода листают команды этой сессии, тот же список лежит рядом с кнопкой отправки, а рядом с ним — сохранённые из настроек сниппеты
- 📈 Плоттер в реальном времени (Chart.js) — строит график по парам
имя:значение и просто по числам в строке, серии называются вашими же ключами, несколько линий на порт и несколько портов на одном графике — или один порт за раз, когда у двух плат не сходится масштаб
- 🖼️ Экспорт графика — PNG (снимок) и CSV (сырые точки для Excel или pandas) ровно того, что показано на графике
- 🔢 HEX-дамп — третья вкладка с байтами как они пришли: смещение, байты и колонка ASCII, по 8, 16 или 32 в строке, по одному порту за раз — поток до декодирования, где
0x00 или бинарный кадр видны числом, а не пустым местом
- 💾 Запись лога в файл в реальном времени — каждая строка дописывается в файл по мере поступления, а не только по кнопке «Сохранить»
- 📊 Отладочная статистика — простой, время отклика (RTT) и счётчики ошибок, предупреждений, крашей и перезагрузок — всё отдельно по каждому порту, с итогом по всем платам отдельной строкой
- 🧩 Декодирование Backtrace — строки
Backtrace: раскрываются в функция → файл:строка через addr2line из тулчейна, со встроенным парсером DWARF как откатом
- 🪦 Разбор core dump — блок base64, который остаётся после паники, собирается вместо вывода в терминал, распаковывается по задачам, и упавшая выходит обычным декодированным backtrace
- 🔄 Сброс платы для каждого порта — импульс DTR/RTS с чипа порта; управляющие линии остаются снятыми, поэтому физическая кнопка RESET запускает прошивку, а не загрузчик
- ⚙️ Интеграция с PlatformIO — порт и скорость из
platformio.ini, автоматическое освобождение и переподключение вокруг задач прошивки
- ⚙️ Интеграция с ESP-IDF — проект опознаётся по
sdkconfig и CMakeLists.txt, скорость из sdkconfig, порт из настроек idf.* расширения Espressif, свежий ELF из build/ и отдельная последовательность сброса для встроенного USB Serial/JTAG
- ⚙️ Интеграция с Arduino CLI — папка скетча опознаётся по
sketch.yaml или по самому .ino, порт и плата берутся из профиля сборки, а скорость — из вашего же Serial.begin(), если в файле проекта её нет
- ⚙️ Настройки редактора — тема, скорость, окончание строки, глубина графика, метки времени и автоскролл живут в экране настроек VS Code: их находит поиск и подхватывает Settings Sync
- 🎨 6 тем оформления — VS Code Dark/Light, Monokai, Dracula, Solarized Dark, Hacker (Matrix), выбор сохраняется между сессиями
- 🌐 Русский и английский — панель, названия команд и журнал идут за языком интерфейса редактора
- 📄 Экспорт и копирование — терминал в
.txt или весь лог в буфер обмена
- ⚡ Свой доступ к порту — порт открывает модуль
serialport в extension host, а не обёртка над CLI
Требования
| Требование |
Пояснение |
| VS Code |
1.84 или новее. |
| Проект PlatformIO, ESP-IDF или Arduino в папке |
Расширение активируется на любом из них. Без проекта монитор работает, но интеграция с проектом недоступна. |
| PlatformIO IDE / расширение ESP-IDF / arduino-cli |
Не обязательны. PlatformIO нужен только ради задач сборки, с которыми синхронизируется монитор; от расширения Espressif читаются лишь ключи idf.*, если они есть; у Arduino читаются только файлы папки скетча, и ничего не запускается. |
| Драйвер устройства |
Как обычно для платы: CP210x, CH340, FTDI и т. д. — должен стоять в системе. |
| Нативный модуль |
serialport поставляется вместе с расширением и собран через N-API, поэтому работает на разных версиях Electron. Если порт не открывается — см. ниже. |
Быстрый старт
- Откройте папку, в которой есть
platformio.ini, проект ESP-IDF (CMakeLists.txt и sdkconfig) или скетч Arduino (sketch.yaml либо просто .ino).
- Поставьте курсор в любой редактор и нажмите Ctrl+K M (на macOS Cmd+K M) — монитор откроется и сразу подключится к плате. Это аккорд: удерживая Ctrl, нажмите K, отпустите, нажмите M.
- Наберите команду в поле внизу и нажмите Enter. Окончание строки выбирается рядом, по умолчанию
\r\n.
Serial.printf("temp:%.1f hum:%d\n", t, h) появится на вкладке Плоттер двумя именованными сериями; просто числа тоже работают.
- Вторая плата добавляется кнопкой + Добавить порт в панели инструментов.
Ничего не приходит? Откройте SUS Monitor: показать журнал. Ошибки порта и проблемы с загрузкой символов пишутся туда и в терминал — там и живёт причина: не открылся порт, не нашёлся firmware.elf, задача прошивки не распознана.
Команды
Доступны из палитры команд (Ctrl+Shift+P) — достаточно набрать SUS.
| Команда |
Что делает |
SUS Monitor: открыть монитор |
Открывает панель, не трогая порты |
SUS Monitor: открыть и подключиться к плате |
Открывает и подключается по настройкам проекта (Ctrl+K M, нужен фокус в редакторе) |
SUS Monitor: показать журнал |
Открывает канал диагностики SUS Monitor в панели «Вывод» |
Пока активен файл platformio.ini, sdkconfig, sketch.yaml или любой .ino, кнопка открытия панели есть и в заголовке редактора.
Настройки
Объявлены в штатном экране настроек VS Code под именем SUS Serial Monitor (Ctrl+,, поиск по SUS Monitor): их находит поиск, и они едут за вами через Settings Sync.
Настройки задают, с чем панель откроется, а не запрещают менять. Тулбар остаётся для того, что крутят каждые пять минут: выбранное там живёт до закрытия панели и в настройки не пишется. Обратно пишутся только две вещи — те, что выбирают насовсем, а не на минуту: тема, а также скорость и окончание строки выбранного порта, которые уходят в профили портов. Правка настройки при открытом мониторе доходит до панели сразу.
| Настройка |
По умолчанию |
Что делает |
susSerialMonitor.theme |
dark |
Тема оформления монитора. Меняется и прямо в панели — выбор в тулбаре пишется сюда же. |
susSerialMonitor.baudRate |
115200 |
Скорость, с которой открывается панель, и с которой подключается порт, добавленный руками. Команда «открыть и подключиться» её не смотрит — берёт скорость из проекта: monitor_speed в PlatformIO, CONFIG_ESPTOOLPY_MONITOR_BAUD в ESP-IDF (либо idf.monitorBaudRate, который главнее обоих), port_config.baudrate или Serial.begin() скетча в проекте Arduino. Профиль порта этой платы главнее их всех. |
susSerialMonitor.lineEnding |
\r\n |
Чем заканчивать команду, отправленную в порт. Для платы, у которой есть профиль, его значение сильнее. |
susSerialMonitor.portProfiles |
{} |
Запомненные скорость и окончание строки по платам — заполняет сама панель, см. Профили портов. Хранится в рабочей папке (.vscode/settings.json), поэтому у каждого проекта свои. |
susSerialMonitor.commandSnippets |
[] |
Сохранённые команды в списке рядом с полем ввода — см. Сниппеты команд. Выбор ставит команду в поле; отправляется она только по Enter. Список редактируется в настройках, для каждой папки проекта. |
susSerialMonitor.plotHistory |
50 |
Сколько последних точек держать на графике. Больше точек — длиннее история, но тяжелее перерисовка. |
susSerialMonitor.plotParseMode |
auto |
Что плоттер берёт из строки: auto — пары имя:значение, если они есть, иначе просто числа; pairs — только пары; numbers — только числа по их месту в строке. Меняется и в тулбаре плоттера. |
susSerialMonitor.showTimestamp |
true |
Показывать метку времени у каждой строки монитора. |
susSerialMonitor.autoscroll |
true |
Прокручивать монитор к последней строке по мере поступления данных. |
susSerialMonitor.limitTerminalLines |
true |
Хранить в панели только ограниченное окно строк. Выключите, чтобы держать весь лог сессии; при большом потоке данных DOM растёт без предела и может отстать от потока. |
susSerialMonitor.maxTerminalLines |
10000 |
Сколько последних строк хранит панель, когда limitTerminalLines включена. Старые строки удаляются пачками, с однократным предупреждением. |
susSerialMonitor.openInNewWindow |
true |
Открывать монитор отдельным окном редактора, а не вкладкой рядом с кодом. Требуется VS Code 1.85 или новее: на старых версиях монитор останется вкладкой, а причина уйдёт в журнал. |
Значение, которого нет в списке выбора, игнорируется, и остаётся умолчание — опечатка в settings.json не оставит селектор пустым.
Интерфейс
Тулбар
| Элемент |
Назначение |
| + Добавить порт |
Выбор порта из списка (платы наверху) и подключение его как отдельного соединения |
| Baud rate |
При выбранном порте — скорость этого порта: смена переоткрывает его и уходит в его профиль. Когда портов нет — скорость для следующего подключаемого |
| Тема |
Выбор темы оформления |
| Чипы портов |
По одному на подключённый порт: цветной индикатор, скорость, сброс (↺) и отключение (✕). Щелчок по самому чипу наводит тулбар на этот порт, выбранный чип обведён рамкой |
| 🧩 .elf/.map |
Загрузка файла символов вручную: .elf даёт файл и строку, .map — только имена |
| 🔴 Запись в файл |
Начинает и останавливает запись лога в файл в реальном времени |
| Монитор / Плоттер / HEX |
Переключение вкладок |
| Автоскролл / Время |
Чекбоксы поведения терминала |
| Поле фильтра |
Скрывает строки лога без указанной подстроки — строки выбранного порта, а пока портов нет, весь лог |
| Порт графика |
Какой порт показывает график: все порты или один из них — одной плате достаётся вся ось Y. Виден на вкладке «Плоттер» |
| Режим разбора |
Что график берёт из строки: Авто, имя:значение или Числа; виден на вкладке «Плоттер» |
| Точек |
Сколько последних точек хранит плоттер, по умолчанию 50 |
| 🖼️ PNG / 📄 CSV |
Экспорт графика, виден на вкладке «Плоттер»; оба идут за тем, что показано, и в имени файла стоит порт, если выбран один |
| Порт дампа |
Какой порт показывает HEX-дамп; виден на вкладке HEX |
| Байт в строке |
Ширина строки дампа: 8, 16 (по умолчанию) или 32; видна на вкладке HEX |
| Счётчик RX |
Живой трафик по всем портам: байты, строки, байты в секунду |
| 📊 Статистика |
Открывает панель отладочной статистики |
| Очистить |
Очищает терминал, график и счётчики; порты остаются подключёнными |
| Копировать |
Копирует весь лог терминала в буфер обмена |
| Сохранить лог |
Разовый экспорт лога в .txt |
Нижняя панель ввода
Целевой порт для команды, окончание строки (нет, \n, \r, \r\n — по умолчанию), само поле ввода, кнопка Отправить и список История.
Список «отправить в» заодно и есть выбор порта: на какой порт он показывает, о том и говорит тулбар, а окончание строки рядом принадлежит именно этому порту и запоминается в его профиле.
Отправленные команды помнятся в течение сессии: ↑ и ↓ в поле ввода листают их так же, как в оболочке, а список История рядом с кнопкой держит те же команды — новые сверху, чтобы выбрать мышью. Подставленная команда сразу готова к правке, а недописанное до нажатия ↑ возвращается, если пройти ↓ ниже самой новой записи.
Сниппеты команд
Список Сниппеты рядом с Историей держит команды из настройки
susSerialMonitor.commandSnippets — она правится в экране настроек для каждой
папки проекта, поэтому у каждого проекта свой набор. Выбор сниппета ставит
команду в поле ввода, готовую к правке и отправке; само ничего не отправится,
пока не нажмёте Enter. В отличие от истории сессии, сниппеты
переживают закрытие панели: это команды, которые вы набираете при каждом
включении платы, без повторного ввода.
Работа с несколькими портами
Каждое нажатие + Добавить порт создаёт независимое соединение — столько, сколько разрешит система.
- У каждого порта своя скорость и своё окончание строки: из профиля, если он есть, иначе из тулбара на момент подключения.
- Терминал и плоттер общие, но каждая строка и линия помечена цветом и тегом своего порта — два устройства выстраиваются на одной шкале времени.
- Выбран всегда один порт, и элементы тулбара, которые относятся к порту, — скорость, окончание строки, фильтр — говорят именно о нём. Выбор переносится щелчком по чипу или списком «отправить в» рядом с полем ввода; в подсказках этих элементов стоит номер порта, поэтому угадывать не приходится.
- Новый порт выбор не отбирает: вторая плата, подключённая посреди работы с первой, оставляет тулбар и поле ввода там, где они были.
- Фильтр принадлежит выбранному порту, поэтому поиск по логу одной платы не гасит лог другой. В первый раз, когда это важно — фильтр набран при двух и более портах, — монитор один раз говорит об этом в логе.
- Сброс (↺) и отключение (✕) действуют на один порт, со своего чипа.
- Простой, RTT и счётчики ошибок, предупреждений, крашей и перезагрузок считаются отдельно по каждому порту: «двенадцать ошибок» не говорит, у какой платы, а нужно именно это, когда рядом работают отлаживаемая прошивка и та, которая просто должна жить. В панели 📊 Статистика на каждую плату своя строка, а под ними строка все порты с итогом.
- Счётчики отключённой платы остаются в панели, приглушённые: её строки остались в терминале, и цифра, исчезающая вместе с платой, спорила бы с тем, что видно на экране. Обнуляет их «Очистить» — вместе с терминалом и графиком.
- График один на все порты, но его можно навести на одну плату — см. «Плоттер».
Профили портов
Две платы на столе редко хотят одного и того же. ESP32 на 921600 с одиночным \n и STM32 на 115200 с \r\n раньше означали два похода в тулбар при каждом открытии панели. Профиль — это память об этих двух выборах, своя у каждой платы и у каждого проекта.
- Что запоминается: скорость и окончание строки. Оба пишутся в тот момент, когда вы меняете их в тулбаре при выбранном порте, — и никогда при подключении: монитор, открытый просто посмотреть лог, ваш
settings.json не трогает.
- Когда применяется: при подключении, и профиль сильнее всего остального — сильнее тулбара и сильнее файла проекта (
monitor_speed, CONFIG_ESPTOOLPY_MONITOR_BAUD, port_config.baudrate). По этому же правилу живёт idf.port: файл проекта говорит, чем собирали, а настройка — что человек решил после. Если скорость из профиля отличается от запрошенной, монитор пишет об этом в лог, а не меняет скорость молча.
- Смена скорости переоткрывает порт. Скорость задаётся драйвером при открытии, другого способа нет: монитор закрывает порт, открывает его заново на новой скорости, печатает
Переоткрываю P1 на 921600 baud и возвращает выбор той же плате. Новое окончание строки действует со следующей команды, переоткрывать нечего.
- Как узнаётся плата: ключом служит серийный номер платы, каким его отдаёт ОС (
sn:…). У устройства без серийника — обычно это клон на CH340 — ключ собирается из USB VID и PID (usb:1a86:7523), а это уже модель, а не конкретная плата: два таких клона делят один профиль. Ключ переживает переподключение и перезапуск редактора и не зависит от имени порта — ту же плату узнают и на COM7 сегодня, и на COM12 завтра.
- Где лежит: настройка
susSerialMonitor.portProfiles в рабочей папке, то есть в .vscode/settings.json, — профиль принадлежит проекту, в котором его сделали. Если папка не открыта, положить его некуда, и он уходит в пользовательские настройки. Значение — обычный объект, его можно читать и править руками:
"susSerialMonitor.portProfiles": {
"sn:0001A2B3": { "baudRate": 921600, "lineEnding": "\n" },
"usb:1a86:7523": { "baudRate": 115200 }
}
- Забыть плату: удалите её запись или всю настройку в экране настроек. Больше на эти ключи никто не ссылается.
- Бессмысленное значение — скорость не числом, окончание строки не из четырёх возможных — игнорируется, и порт открывается с тем, что выбрано в тулбаре. Ни опечатка в
settings.json, ни плата без серийного номера открыться порту не помешают: неопознанной плате просто нечего запоминать.
Плоттер
- Что именно строка даёт графику, решает режим разбора — он выбирается в тулбаре рядом с полем Точек или задаётся один раз настройкой
susSerialMonitor.plotParseMode:
- Авто (по умолчанию) — пары
имя:значение, если они в строке есть, иначе просто числа;
- имя:значение — только пары; строка без пары график не трогает вовсе, и это отделяет телеметрию от остального лога;
- Числа — только числа по позиции, как работало до появления режимов.
- Пара — это
имя:значение или имя=значение, можно с ведущим >, как пишет Teleplot. Разделители значений — пробелы, запятые и точки с запятой.
- Именованная серия узнаётся по имени, а не по месту в строке: плата печатает
temp:25, следующей строкой hum:40 temp:26 — и это по-прежнему две линии (P1 · temp, P1 · hum), а не четыре. Числа без имени по-прежнему дают позиционные линии на порт (P1 · S1, P1 · S2, P2 · S1…).
- На график идут только десятичные числа — со знаком, точкой и экспонентой. Шестнадцатеричные отброшены намеренно:
Number('0x400d1234') — это вполне себе 1074795572, и адреса из загрузочного лога раньше ложились на график рядом с данными.
- Цвет линии берётся из палитры текущей темы и держится за серией при смене темы.
- Пропущенное значение оставляет разрыв, а не падение в ноль.
- Глубина истории — поле Точек, по умолчанию 50.
- Один график, но при надобности — один порт за раз. Самый левый список на вкладке «Плоттер» по умолчанию стоит на всех портах; выберите плату — и рисуются только её линии. Ось Y одна на всё, что видно, и в этом весь смысл: серия 0…3.3 рядом с серией 0…4095 ложится в линию у нуля, а скрытая серия в расчёте оси не участвует — выбранной плате возвращается вся высота графика. Заговорившая позже плата показ себе не забирает; а если выбранную отключили, показываются все — пустой график без единой линии выглядит как поломка, а не как «порт закрыт».
- PNG сохраняет текущий снимок канваса, CSV — все точки показанных линий с их метками времени. Когда выбран один порт, он стоит и в имени файла —
plot_P1_….png, plot_data_P1_….csv, — иначе два экспорта двух плат не отличить друг от друга.
HEX-дамп
Вкладка HEX показывает байты так, как их отдал порт — до всякого превращения в текст. Поэтому байт, который терминалу показать нечем, здесь виден числом: 0x00, битая последовательность UTF-8, кадр Modbus или SLIP, лишний 0x0d, из-за которого строки затирают друг друга.
00000000 48 65 6c 6c 6f 20 77 6f 72 6c 64 0d 0a 00 ff 41 |Hello world....A|
- Смещение слева абсолютное — считается от момента открытия порта, а не от начала окна, поэтому по нему видно, сколько уже уехало вверх.
- В колонке ASCII печатаемые байты стоят символами, остальные — точкой: именно это делает текстовый протокол читаемым, а бинарный — узнаваемым.
- Байт в строке — 8, 16 (по умолчанию) или 32. Восемь удобно, когда короткий кадр считают глазами, тридцать два — когда окно широкое.
- Один порт за раз, выбирается в тулбаре рядом с шириной строки. Буфер у каждого порта свой: перемешанные в одном дампе байты двух плат обессмыслили бы смещения, а чужой байт посреди вашего кадра — это ошибка протокола, которую потом будут искать и не найдут.
- Буфер держит последние 8 КиБ на порт. Дамп собирается целиком на каждую перерисовку, поэтому это осознанное окно в недавнее прошлое, а не полная запись: всю сессию нужно писать в файл кнопкой 🔴 Запись в файл.
- Очистить обнуляет дамп вместе с терминалом и графиком, и смещения начинают отсчёт заново.
- В настройки ничего не пишется: порт и ширина строки — выбор на сессию. Дамп рисуется только пока вкладка открыта, так что работа с текстовым терминалом ничего не стоит.
Отладка ESP32
Ошибки, предупреждения, перезагрузки и краши распознаются и считаются на ходу, отдельно по каждому порту: счётчики живут в панели 📊 Статистика — строка на плату плюс строка все порты с итогом.
| Счётчик |
Что попадает |
| Ошибок в логе |
метка уровня E (E (123) tag:, [E], E/tag:) или слова error, ошибка, exception, fail, failed, panic, паника |
| Предупреждений в логе |
метка уровня W (W (123) tag:, [W], W/tag:) или слова warning, warned, предупреждение |
| Крашей (Backtrace) |
строка Backtrace:, Guru Meditation, fatal exception, abort() was called, assert failed |
| Обнаружено перезагрузок |
Маркеры старта ESP32/Arduino: rst:0x, ets Jun, ets Jul, rebooting, reset reason, boot:0x13, power on reset |
Метка уровня весомее слов: W (123) wifi: connect failed попадёт в предупреждения, а
не в ошибки, хотя в строке есть failed. Краш и маркер перезагрузки весомее и метки, и
слов. Метка ищется только с начала строки — кроме [E] и [W], которые ядро Arduino
печатает после метки времени.
Когда в логе появляется строка Backtrace:, её адреса расшифровываются прямо под ней:
Backtrace:0x400d1fb9:0x3ffb21a0 0x400d2c85:0x3ffb21c0
[DECODE] 0x400d1fb9 → loop() at main.cpp:142
Символы подхватываются сами: при открытии панели берётся самый свежий ELF из папки сборки — .pio/build у PlatformIO, build/ у ESP-IDF и у скетча Arduino, собранного с экспортом бинарника, — указывать руками ничего не нужно. Кнопка 🧩 .elf/.map нужна для случаев, когда требуется другая сборка.
Декодирование идёт в первую очередь через addr2line из тулчейна — именно это даёт точные файл:строка на актуальных сборках ESP-IDF: они идут с DWARF 5, а встроенный парсер понимает только DWARF 2–4. Если addr2line недоступен или не отработал, за дело берётся встроенный парсер, а за ним — таблица символов ELF, где кадр вырождается в символ +0xсмещение: имя верное, номера строки нет. Каждый откат пишет свою причину в журнал.
Core dump
Паника может закончиться не строкой Backtrace:, а чем-то посерьёзнее. С CONFIG_ESP_COREDUMP_ENABLE_TO_UART обработчик паники печатает целый core dump — несколько сотен строк base64 между CORE DUMP START и CORE DUMP END, — а это снимок всех задач, а не только упавшей. Монитор собирает этот блок вместо вывода и рассказывает, что внутри:
[COREDUMP] Начался core dump — собираю блок вместо вывода
[COREDUMP] Core dump: задач 8, Xtensa, 21504 байт в 336 строках
[COREDUMP] Упавшая задача, TCB 0x3ffb2f10:
[DECODE] 0x400d1fb9 → publish() at mqtt.cpp:88
[DECODE] 0x400d2c85 → loop() at main.cpp:142
[COREDUMP] Остальные задачи, по верхнему кадру:
[DECODE] TCB 0x3ffb8a40: 0x4008a1c2 → vTaskDelay() at tasks.c:1580
Core dump checksum='2b4e9f01'
- Адреса идут через тот же декодер, что и обычный backtrace, одним вызовом
addr2line на весь дамп, поэтому с загруженными символами кадры выходят как функция → файл:строка.
- Упавшая задача разворачивается целиком — до 16 кадров — и опознаётся по служебной заметке обработчика паники. От остальных задач берётся верхний кадр: этого хватает, чтобы увидеть, кто чего ждал, и при этом не завалить сам краш.
- Сам блок в терминал не попадает и вторым крашем не считается: строку паники над ним счётчик уже посчитал. Строка
checksum= после маркера конца — обычная строка и остаётся на виду.
- Потерянный маркер конца не съедает сессию. Второй
CORE DUMP START, отключение порта и 6 МБ тела одинаково закрывают блок с пометкой «оборвался», а собранное всё равно разбирается.
- На RISC-V из дампа выходит максимум два кадра (
pc и ra): глубже нужны таблицы CFI прошивки, которых в дампе нет. Xtensa таких таблиц не требует — её кадры лежат в самом стеке.
- В старом двоичном формате дампа (
CONFIG_ESP_COREDUMP_DATA_FORMAT_BIN) регистров нет вовсе, поэтому сообщается только число задач — и сразу называется ключ, который нужно переключить.
monitor_port и monitor_speed читаются из platformio.ini с учётом наследования от [env] и default_envs.
- Если
monitor_port не задан, берётся первый порт, похожий на плату — по USB VID известных USB-UART (CP210x 10c4, CH340 1a86, FTDI 0403, Arduino 2341, Espressif 303a, Adafruit 239a) и по названию устройства. Если на плату не похож ни один, берётся первый доступный.
- При старте задачи upload порт освобождается, чтобы его занял загрузчик.
- После завершения задачи монитор переподключается с повторами, потому что плата возвращается в систему не мгновенно. Если прошивка упала, порт остаётся закрытым.
- При открытии порта DTR и RTS снимаются. Равные уровни на обеих линиях схему авто-сброса не трогают, поэтому кнопка RESET на плате запускает прошивку. При поднятом DTR и снятом RTS — а именно так порт открывает драйвер Windows — то же нажатие уводит плату в
rst:0x1 (POWERON),boot:0x2 (DOWNLOAD(USB/UART0)).
Интеграция с ESP-IDF
Папка считается проектом ESP-IDF, если её CMakeLists.txt подключает $ENV{IDF_PATH}/tools/cmake/project.cmake либо рядом с CMakeLists.txt лежит sdkconfig. Обычные проекты на CMake не трогаются. Первым проверяется platformio.ini, и он побеждает: проект PlatformIO с framework = espidf несёт оба признака, но собирает его PlatformIO, и ELF лежит в .pio/build.
- Скорость берётся из
CONFIG_ESPTOOLPY_MONITOR_BAUD в sdkconfig, затем из плоского CONFIG_MONITOR_BAUD, затем из старого булева ключа CONFIG_MONITOR_BAUD_*B (до IDF 4.3 скорость задавалась отдельным ключом на каждое значение), затем из project_description.json, затем из скорости консоли, и лишь в крайнем случае это 115200. Ключи sdkconfig важнее сгенерированного описания, потому что menuconfig пишет их сразу, а описание отстаёт ровно на одну несобранную правку. До первой сборки читается sdkconfig.defaults.
- Порт берётся из настроек расширения Espressif —
idf.port, на Windows idf.portWin. В sdkconfig порта нет вовсе, а в настройке лежит то, что вы только что выбрали через ESP-IDF: Select Port, — источник точнее. Так же учитываются idf.monitorBaudRate и idf.buildPath. Ключи читаются обычным API настроек, поэтому достаточно прописать их в settings.json: само расширение Espressif ставить не обязательно. Действуют они только на проекты ESP-IDF и никогда — на лежащий рядом проект PlatformIO.
- Символы — самый свежий ELF в
build/: app_elf из project_description.json, если он есть, иначе свежайший .elf, кроме загрузчика. idf.buildPath переносит поиск вместе с папкой сборки.
- Прошивка освобождает порт. У ESP-IDF считаются задачи
flash, upload и monitor: расширение Espressif открывает порт своим монитором, а два монитора на одном порту не уживаются. Русские названия задач тоже распознаются. После задачи монитор переподключается с повторами.
- Встроенный USB Serial/JTAG (VID
303a, PID 1001 — ESP32-S3, C3, C6 и прочие без внешнего USB-UART) сбрасывается своей последовательностью. Чип сам отдаёт свой USB, поэтому при сбросе порт пропадает из системы и появляется заново; монитор отпускает дескриптор и переоткрывает порт на той же скорости вместо того, чтобы показать мёртвое соединение. Если консоль настроена на встроенный USB — основная или вторичная, CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG либо CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG, — автоподключение выбирает именно его, а не внешний мост: отладочная плата показывает оба порта, а лог идёт только в один. Явно заданный idf.port при этом всё равно главнее: перебор портов начинается, только когда порт не задан.
Интеграция с Arduino CLI
Папка считается скетчем Arduino, если в ней лежит sketch.yaml — файл проекта, который arduino-cli понимает с версии 0.21, — а если его нет, то по самому .ino в корне папки: скетчи без файла проекта до сих пор норма. Папки на уровень ниже тоже проверяются, поэтому папка со скетчами работает. Первыми проверяются platformio.ini и признаки ESP-IDF, и они побеждают: проект PlatformIO на фреймворке Arduino держит скетчи в src/, а в проекте IDF .ino может лежать рядом с компонентом arduino, но в обоих случаях ELF собирает другая система.
- Скорость берётся из
port_config.baudrate выбранного профиля, затем из общего default_port_config, затем из Serial.begin() в скетче, и лишь в крайнем случае это 115200. Исходник здесь — полноправный источник, а не догадка от безысходности: аналога monitor_speed у Arduino нет, и в большинстве проектов скорость не записана больше нигде. Считаются только Serial и USBSerial — Serial1.begin(9600) это обычно GPS на втором UART. Аргумент-#define разрешается, закомментированные вызовы не считаются, а главный .ino читается первым, потому что setup() живёт в нём.
- Порт и плата берутся из профиля сборки: из того, что назван в
default_profile, а если такого ключа нет — из единственного профиля в файле. port и fqbn профиля главнее общих default_port и default_fqbn. Порт с протоколом, отличным от serial, игнорируется: при прошивке по сети там лежит IP-адрес, и открывать его как порт нечего. Имя профиля (или FQBN, если профиля нет) панель показывает так же, как окружение PlatformIO.
- Символы — самый свежий ELF в
build/<vendor>.<arch>.<плата>/. Сборка попадает туда только с ключом --export-binaries (в Arduino IDE это «Export compiled binary»); по умолчанию arduino-cli собирает во временную папку с хешем в имени, угадать которую нельзя. Без экспорта укажите ELF кнопкой 🧩 .elf/.map.
addr2line ищется в папке данных arduino-cli — packages/<vendor>/tools/<пакет>/<версия>/bin: это Arduino15 в AppData на Windows, ~/Library на macOS, ~/.arduino15 на остальных системах и всё, на что указывает ARDUINO_DIRECTORIES_DATA. Перебираются оба поколения имён ядра esp32 (xtensa-esp32s3-elf-gcc и riscv32-esp-elf-gcc в 2.x, общие esp-x32 и esp-rv32 в 3.x), и файл с именем чипа предпочитается тулчейну той же семьи: чужая архитектура декодирует молча и в мусор.
- Чип читается из идентификатора платы в FQBN, а не считается равным плате:
esp32:esp32:esp32s3box — это S3, а esp32:esp32:esp32da — обычный ESP32. Если по идентификатору чип не опознать (um_tinys3 и подобные), перебираются все подходящие тулчейны.
- Встроенный USB Serial/JTAG опознаётся не по файлу конфигурации, а по пунктам меню платы в FQBN:
CDCOnBoot=cdc (в старых ядрах cdc_on_boot=1) уводит консоль туда, и автоподключение выбирает этот порт. На S2, S3 и P4 явный USBMode, отличный от hwcdc, это отменяет: консоль тогда идёт в TinyUSB CDC, а это другое устройство с другим PID.
- Прошивка освобождает порт для задач, в названии которых есть Arduino: задачи arduino-cli пишутся руками в
tasks.json, поэтому опознаются по слову в имени. Задача монитора порт оставляет — так же, как у PlatformIO.
Темы оформления
| Тема |
Стиль |
| 🌙 VS Code Dark |
Тёмная, по умолчанию |
| ☀️ VS Code Light |
Светлая |
| 🍬 Monokai |
Классическая тёмная тема редакторов |
| 🧛 Dracula |
Фиолетово-розовые акценты |
| 🌊 Solarized Dark |
Приглушённая, низкий контраст |
| 💻 Hacker (Matrix) |
Моно-зелёный терминальный стиль |
При смене темы перекрашивается интерфейс, палитра линий графика и оси Chart.js. Цвета тегов портов фиксированы специально: плата сохраняет свою метку при любой теме.
Известные ограничения
- Точные
файл:строка на сборке с DWARF 5 требуют addr2line из тулчейна; без него встроенный парсер покрывает только DWARF 2–4, и кадр DWARF 5 разрешается по таблице символов — имя есть, номера строки нет. Отдельная HTML-сборка всегда работает встроенным парсером.
- Файл символов один на все порты: две платы с разными прошивками одновременно точно не декодируются.
- История команд живёт только в пределах сессии — с закрытием панели отправленное забывается. Сохранённые сниппеты живут отдельно, в настройках, и сессию переживают.
- Профиль порта помнит скорость и окончание строки, но не фильтр: фильтры свои у каждого порта и живут только в пределах сессии.
- Плату, у которой ОС не показывает серийный номер, опознаёт пара USB VID/PID, поэтому два одинаковых клона делят один профиль. В отдельной HTML-сборке другого ключа нет вовсе — Web Serial серийный номер не отдаёт, — так что там любые две платы одной модели всегда с одним профилем, и хранится он в
localStorage браузера, а не в рабочей папке.
- График один, а не по одному на порт: навести его на одну плату можно, но смотреть на две сразу, каждую со своей осью, — нет.
- HEX-дамп — окно в последние 8 КиБ каждого порта, а не запись: старые байты из него уезжают (сколько именно — видно по смещению). Весь поток сохраняет 🔴 Запись в файл, но в файл идёт декодированный текст, а не байты.
- Интеграция с проектом покрывает PlatformIO, ESP-IDF и Arduino CLI; обычная папка проектом не считается. Сама панель открывается где угодно — кнопка в строке состояния и палитра от проекта не зависят.
- Скетч Arduino должен быть собран с экспортом бинарника (
--export-binaries, «Export compiled binary»), иначе backtrace сам не декодируется: по умолчанию сборка уходит во временную папку с хешем в имени, и искать там нечего.
- В разобранном core dump задачи названы адресом TCB, а не именем: имя лежит внутри TCB FreeRTOS по смещению, которое зависит от конфигурации сборки, и угаданное смещение печатало бы убедительную чушь. Контрольная сумма дампа тоже не проверяется — её строка просто показывается как пришла.
Если что-то не работает
| Симптом |
Причина и что делать |
Порт занят другой программой |
Порт держит кто-то ещё — обычно терминал монитора PlatformIO или идущая прошивка. Закройте и подключитесь заново. |
No native build was found for platform=… runtime=electron abi=… |
Нативный serialport собран не под ту версию Electron. Пересоберите под версию из «Справка → О программе»: npx @electron/rebuild -v <версия Electron> |
| Нет прав на порт в Linux |
Добавьте пользователя в группу dialout и перезайдите в систему. |
В backtrace символ +0xсмещение |
Адрес разрешён по таблице символов: addr2line не нашёлся в тулчейне или не отработал, а сборка идёт с DWARF 5, который встроенный парсер пропускает. Имя функции верное, номера строки нет. Откройте SUS Monitor: показать журнал — причина отката записана там. |
| Ctrl+K M ничего не делает |
Сначала откройте SUS Monitor: показать журнал: если строки Команда susSerialMonitor.openAndConnect там нет, нажатие до расширения не дошло. Причины по частоте: фокус не в редакторе (сочетание объявлено с editorTextFocus); аккорд набран слишком медленно и VS Code его сбросил; клавишу перехватила встроенная команда «Change Language Mode», которая по умолчанию тоже висит на Ctrl+K M. Проверьте Ctrl+K Ctrl+S → фильтр ctrl+k m → правый клик → «Show Same Keybindings» и при конфликте назначьте своё сочетание. |
| Не происходит вообще ничего |
Откройте SUS Monitor: показать журнал — каждый тихий откат пишет туда свою причину. |
| Интерфейс не на том языке |
Монитор идёт за языком редактора. Выполните из палитры Configure Display Language и перезагрузите окно — панель, названия команд и журнал переключатся вместе. Любой язык, кроме русского, даёт английский интерфейс. |
Работает и без VS Code
Тот же монитор собирается в один самодостаточный HTML-файл поверх Web Serial API: двойной щелчок в Chrome или Edge, без установки и без Node. Обе цели собираются из общего ядра, поэтому возможности появляются сразу в двух. Инструкции по сборке — в README для разработчика.
План развития
План выполнен: backtrace с DWARF 5 через тулчейн, разбор core dump ESP-IDF,
HEX-просмотр, история команд и сниппеты, классификация предупреждений,
профили портов по платам, именованные серии графика, раздельные счётчики и
графики по портам и поддержка Arduino CLI — всё реализовано. Идеи и сообщения
об ошибках — в github.com/iliasnovickov-cmd/SUS-monitor-vs-code/issues.
Лицензия
MIT
| |