From c5113f2b6ca37e93fc64e741c6fceb732cd9c0f1 Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 16 Feb 2026 14:59:06 +0800 Subject: [PATCH] connects to real port, and pair port, and prints traffic --- README.md | 52 ++++ requirements.txt | 1 + serial_log.txt | 633 +++++++++++++++++++++++++++++++++++++++++++++++ serial_proxy.py | 245 ++++++++++++++++++ 4 files changed, 931 insertions(+) create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 serial_log.txt create mode 100644 serial_proxy.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a4db97 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Serial Proxy (MITM) + +A small Python app that sits between the Arduino IDE and a real COM device. It creates (or uses) a **virtual COM port** that you open in the Arduino IDE, while it talks to the **real device** on another port. All traffic is **recorded** both ways and **passed through unchanged**. + +## What it does + +1. **CLI** — Lists connected COM ports; you choose the real device (e.g. your Arduino). +2. **Virtual port** — Tries to create a virtual COM pair with [com0com](https://sourceforge.net/projects/com0com/); if that fails, you pick one end of an existing pair. +3. **Relay** — Data from Arduino IDE → virtual port → this app → real device, and the reverse. No modification, only copying. +4. **Logging** — Every byte in both directions is logged to `serial_log.txt` (and optionally to the console) with timestamps and hex. + +## Requirements + +- Python 3.10+ +- [pyserial](https://pypi.org/project/pyserial/): `pip install -r requirements.txt` +- **Virtual COM pair** (for “create virtual port” to work): install [com0com](https://sourceforge.net/projects/com0com/) and, if you want the app to create pairs automatically, run the script (or `setupc.exe`) with **Administrator** rights so it can create port pairs. + +## Usage + +1. Install dependencies: + ```bash + pip install -r requirements.txt + ``` +2. (Optional) Install com0com and, if needed, run the app as Administrator so it can create a virtual pair. +3. Run the proxy: + ```bash + python serial_proxy.py + ``` +4. Select the **real device** COM port (e.g. the actual Arduino). +5. If the app created a virtual pair, it will tell you which port to select in the Arduino IDE (e.g. COM252). Otherwise, select the end of the pair that **this app** will use; in the Arduino IDE you must select the **other** end of that pair. +6. Enter baud rate (default 115200) or press Enter. +7. In the Arduino IDE, choose the indicated virtual port and use Serial Monitor / upload as usual. All traffic is logged to `serial_log.txt`. + +Stop with **Ctrl+C**. + +## Log file + +- Path: `serial_log.txt` in the same folder as `serial_proxy.py`. +- Each line has timestamp, direction (`IDE->device` or `device->IDE`), byte count, hex dump, and a raw repr of the bytes. + +## Without com0com + +If you don’t install com0com, the app cannot create a virtual pair. You’ll need another way to get a **pair** of linked virtual ports (e.g. another null-modem/virtual serial driver). Then run the app, select the **real** device port, and when asked, select the port that **this app** should open (one end of the pair). Use the **other** end of that pair in the Arduino IDE. + +## Summary + +| You select in the app | You select in Arduino IDE | +|------------------------|----------------------------| +| Real device (e.g. COM3) | — | +| Virtual port “for this app” (e.g. COM251) | The **other** end of the pair (e.g. COM252) | + +Traffic flows: **Arduino IDE ↔ virtual pair ↔ this app ↔ real device**, with full logging both ways. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ad05ef --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyserial>=3.5 diff --git a/serial_log.txt b/serial_log.txt new file mode 100644 index 0000000..8af2e56 --- /dev/null +++ b/serial_log.txt @@ -0,0 +1,633 @@ + +--- session started 2026-02-16T14:35:58.705768 --- +real=COM77 virtual_ours=COM251 virtual_ide=COM252 baud=115200 + +--- session started 2026-02-16T14:38:30.817526 --- +real=COM77 virtual_ours=COM251 virtual_ide=COM252 baud=115200 + +--- session started 2026-02-16T14:50:38.812196 --- +real=COM77 virtual_ours=COM1 virtual_ide=COM2 baud=115200 +[14:50:39.067] device->IDE (8 bytes) + hex: 3732303130360d0a + raw: b'720106\r\n' +[14:50:39.561] device->IDE (8 bytes) + hex: 3732303630360d0a + raw: b'720606\r\n' +[14:50:40.065] device->IDE (8 bytes) + hex: 3732313130360d0a + raw: b'721106\r\n' +[14:50:40.561] device->IDE (8 bytes) + hex: 3732313630360d0a + raw: b'721606\r\n' +[14:50:41.067] device->IDE (8 bytes) + hex: 3732323130360d0a + raw: b'722106\r\n' +[14:50:41.561] device->IDE (8 bytes) + hex: 3732323630360d0a + raw: b'722606\r\n' +[14:50:42.066] device->IDE (8 bytes) + hex: 3732333130360d0a + raw: b'723106\r\n' +[14:50:42.562] device->IDE (8 bytes) + hex: 3732333630360d0a + raw: b'723606\r\n' +[14:50:43.067] device->IDE (8 bytes) + hex: 3732343130360d0a + raw: b'724106\r\n' +[14:50:43.564] device->IDE (8 bytes) + hex: 3732343630360d0a + raw: b'724606\r\n' +[14:50:44.070] device->IDE (8 bytes) + hex: 3732353130360d0a + raw: b'725106\r\n' +[14:50:44.568] device->IDE (8 bytes) + hex: 3732353630360d0a + raw: b'725606\r\n' +[14:50:45.065] device->IDE (8 bytes) + hex: 3732363130360d0a + raw: b'726106\r\n' +[14:50:45.562] device->IDE (8 bytes) + hex: 3732363630360d0a + raw: b'726606\r\n' +[14:50:46.066] device->IDE (8 bytes) + hex: 3732373130360d0a + raw: b'727106\r\n' +[14:50:46.563] device->IDE (8 bytes) + hex: 3732373630360d0a + raw: b'727606\r\n' +[14:50:47.067] device->IDE (8 bytes) + hex: 3732383130360d0a + raw: b'728106\r\n' +[14:50:47.563] device->IDE (8 bytes) + hex: 3732383630360d0a + raw: b'728606\r\n' +[14:50:48.071] device->IDE (8 bytes) + hex: 3732393130360d0a + raw: b'729106\r\n' +[14:50:48.566] device->IDE (8 bytes) + hex: 3732393630360d0a + raw: b'729606\r\n' +[14:50:49.064] device->IDE (8 bytes) + hex: 3733303130360d0a + raw: b'730106\r\n' +[14:50:49.565] device->IDE (8 bytes) + hex: 3733303630360d0a + raw: b'730606\r\n' +[14:50:50.061] device->IDE (8 bytes) + hex: 3733313130360d0a + raw: b'731106\r\n' +[14:50:50.564] device->IDE (8 bytes) + hex: 3733313630360d0a + raw: b'731606\r\n' +[14:50:51.070] device->IDE (8 bytes) + hex: 3733323130360d0a + raw: b'732106\r\n' +[14:50:51.566] device->IDE (8 bytes) + hex: 3733323630360d0a + raw: b'732606\r\n' +[14:50:52.069] device->IDE (8 bytes) + hex: 3733333130360d0a + raw: b'733106\r\n' +[14:50:52.561] device->IDE (8 bytes) + hex: 3733333630360d0a + raw: b'733606\r\n' +[14:50:53.064] device->IDE (8 bytes) + hex: 3733343130360d0a + raw: b'734106\r\n' +[14:50:53.568] device->IDE (8 bytes) + hex: 3733343630360d0a + raw: b'734606\r\n' +[14:50:54.061] device->IDE (8 bytes) + hex: 3733353130360d0a + raw: b'735106\r\n' +--- session ended --- + +--- session started 2026-02-16T14:51:04.003069 --- +real=COM77 virtual_ours=COM1 virtual_ide=COM2 baud=115200 +[14:51:04.288] device->IDE (6 bytes) + hex: 393530360d0a + raw: b'9506\r\n' +[14:51:04.783] device->IDE (7 bytes) + hex: 31303030360d0a + raw: b'10006\r\n' +[14:51:05.285] device->IDE (7 bytes) + hex: 31303530360d0a + raw: b'10506\r\n' +[14:51:05.788] device->IDE (7 bytes) + hex: 31313030360d0a + raw: b'11006\r\n' +[14:51:06.281] device->IDE (7 bytes) + hex: 31313530360d0a + raw: b'11506\r\n' +[14:51:06.788] device->IDE (7 bytes) + hex: 31323030360d0a + raw: b'12006\r\n' +[14:51:07.284] device->IDE (7 bytes) + hex: 31323530360d0a + raw: b'12506\r\n' +[14:51:07.788] device->IDE (7 bytes) + hex: 31333030360d0a + raw: b'13006\r\n' +[14:51:08.282] device->IDE (7 bytes) + hex: 31333530360d0a + raw: b'13506\r\n' +[14:51:08.788] device->IDE (7 bytes) + hex: 31343030360d0a + raw: b'14006\r\n' +[14:51:09.284] device->IDE (7 bytes) + hex: 31343530360d0a + raw: b'14506\r\n' +[14:51:09.779] device->IDE (7 bytes) + hex: 31353030360d0a + raw: b'15006\r\n' +[14:51:10.284] device->IDE (7 bytes) + hex: 31353530360d0a + raw: b'15506\r\n' +[14:51:10.778] device->IDE (7 bytes) + hex: 31363030360d0a + raw: b'16006\r\n' +[14:51:11.284] device->IDE (7 bytes) + hex: 31363530360d0a + raw: b'16506\r\n' +[14:51:11.786] device->IDE (7 bytes) + hex: 31373030360d0a + raw: b'17006\r\n' +[14:51:12.280] device->IDE (7 bytes) + hex: 31373530360d0a + raw: b'17506\r\n' +[14:51:12.784] device->IDE (7 bytes) + hex: 31383030360d0a + raw: b'18006\r\n' +[14:51:13.280] device->IDE (7 bytes) + hex: 31383530360d0a + raw: b'18506\r\n' +[14:51:13.781] device->IDE (7 bytes) + hex: 31393030360d0a + raw: b'19006\r\n' +[14:51:14.279] device->IDE (7 bytes) + hex: 31393530360d0a + raw: b'19506\r\n' +[14:51:14.784] device->IDE (7 bytes) + hex: 32303030360d0a + raw: b'20006\r\n' +[14:51:15.282] device->IDE (7 bytes) + hex: 32303530360d0a + raw: b'20506\r\n' +[14:51:15.788] device->IDE (7 bytes) + hex: 32313030360d0a + raw: b'21006\r\n' +[14:51:16.281] device->IDE (7 bytes) + hex: 32313530360d0a + raw: b'21506\r\n' +[14:51:16.785] device->IDE (7 bytes) + hex: 32323030360d0a + raw: b'22006\r\n' +[14:51:17.280] device->IDE (7 bytes) + hex: 32323530360d0a + raw: b'22506\r\n' +[14:51:17.784] device->IDE (7 bytes) + hex: 32333030360d0a + raw: b'23006\r\n' +[14:51:18.288] device->IDE (7 bytes) + hex: 32333530360d0a + raw: b'23506\r\n' +[14:51:18.779] device->IDE (7 bytes) + hex: 32343030360d0a + raw: b'24006\r\n' +[14:51:19.282] device->IDE (7 bytes) + hex: 32343530360d0a + raw: b'24506\r\n' +[14:51:19.784] device->IDE (7 bytes) + hex: 32353030360d0a + raw: b'25006\r\n' +[14:51:20.279] device->IDE (7 bytes) + hex: 32353530360d0a + raw: b'25506\r\n' +[14:51:20.782] device->IDE (7 bytes) + hex: 32363030360d0a + raw: b'26006\r\n' +[14:51:21.283] device->IDE (7 bytes) + hex: 32363530360d0a + raw: b'26506\r\n' +[14:51:21.787] device->IDE (7 bytes) + hex: 32373030360d0a + raw: b'27006\r\n' +[14:51:22.288] device->IDE (7 bytes) + hex: 32373530360d0a + raw: b'27506\r\n' +[14:51:22.780] device->IDE (7 bytes) + hex: 32383030360d0a + raw: b'28006\r\n' +[14:51:23.282] device->IDE (7 bytes) + hex: 32383530360d0a + raw: b'28506\r\n' +[14:51:23.785] device->IDE (7 bytes) + hex: 32393030360d0a + raw: b'29006\r\n' +[14:51:24.281] device->IDE (7 bytes) + hex: 32393530360d0a + raw: b'29506\r\n' +[14:51:24.784] device->IDE (7 bytes) + hex: 33303030360d0a + raw: b'30006\r\n' +[14:51:25.280] device->IDE (7 bytes) + hex: 33303530360d0a + raw: b'30506\r\n' +[14:51:25.785] device->IDE (7 bytes) + hex: 33313030360d0a + raw: b'31006\r\n' +[14:51:26.288] device->IDE (7 bytes) + hex: 33313530360d0a + raw: b'31506\r\n' +[14:51:26.782] device->IDE (7 bytes) + hex: 33323030360d0a + raw: b'32006\r\n' +[14:51:27.284] device->IDE (7 bytes) + hex: 33323530360d0a + raw: b'32506\r\n' +[14:51:27.788] device->IDE (7 bytes) + hex: 33333030360d0a + raw: b'33006\r\n' +[14:51:28.281] device->IDE (7 bytes) + hex: 33333530360d0a + raw: b'33506\r\n' +[14:51:28.783] device->IDE (7 bytes) + hex: 33343030360d0a + raw: b'34006\r\n' +[14:51:29.289] device->IDE (7 bytes) + hex: 33343530360d0a + raw: b'34506\r\n' +[14:51:29.786] device->IDE (7 bytes) + hex: 33353030360d0a + raw: b'35006\r\n' +[14:51:30.283] device->IDE (7 bytes) + hex: 33353530360d0a + raw: b'35506\r\n' +[14:51:30.789] device->IDE (7 bytes) + hex: 33363030360d0a + raw: b'36006\r\n' +[14:51:31.288] device->IDE (7 bytes) + hex: 33363530360d0a + raw: b'36506\r\n' +[14:51:31.787] device->IDE (7 bytes) + hex: 33373030360d0a + raw: b'37006\r\n' +[14:51:32.285] device->IDE (7 bytes) + hex: 33373530360d0a + raw: b'37506\r\n' +[14:51:32.830] device->IDE (7 bytes) + hex: 33383030360d0a + raw: b'38006\r\n' +[14:51:33.287] device->IDE (7 bytes) + hex: 33383530360d0a + raw: b'38506\r\n' +[14:51:33.785] device->IDE (7 bytes) + hex: 33393030360d0a + raw: b'39006\r\n' +[14:51:34.330] device->IDE (7 bytes) + hex: 33393530360d0a + raw: b'39506\r\n' +[14:51:34.789] device->IDE (7 bytes) + hex: 34303030360d0a + raw: b'40006\r\n' +[14:51:35.284] device->IDE (7 bytes) + hex: 34303530360d0a + raw: b'40506\r\n' +[14:51:35.780] device->IDE (7 bytes) + hex: 34313030360d0a + raw: b'41006\r\n' +[14:51:36.286] device->IDE (7 bytes) + hex: 34313530360d0a + raw: b'41506\r\n' +[14:51:36.785] device->IDE (7 bytes) + hex: 34323030360d0a + raw: b'42006\r\n' +[14:51:37.285] device->IDE (7 bytes) + hex: 34323530360d0a + raw: b'42506\r\n' +[14:51:37.784] device->IDE (7 bytes) + hex: 34333030360d0a + raw: b'43006\r\n' +[14:51:38.281] device->IDE (7 bytes) + hex: 34333530360d0a + raw: b'43506\r\n' +[14:51:38.787] device->IDE (7 bytes) + hex: 34343030360d0a + raw: b'44006\r\n' +[14:51:39.285] device->IDE (7 bytes) + hex: 34343530360d0a + raw: b'44506\r\n' +[14:51:39.780] device->IDE (7 bytes) + hex: 34353030360d0a + raw: b'45006\r\n' +[14:51:40.289] device->IDE (7 bytes) + hex: 34353530360d0a + raw: b'45506\r\n' +[14:51:40.838] device->IDE (7 bytes) + hex: 34363030360d0a + raw: b'46006\r\n' +[14:51:41.284] device->IDE (7 bytes) + hex: 34363530360d0a + raw: b'46506\r\n' +[14:51:41.784] device->IDE (7 bytes) + hex: 34373030360d0a + raw: b'47006\r\n' +[14:51:42.283] device->IDE (7 bytes) + hex: 34373530360d0a + raw: b'47506\r\n' +[14:51:42.782] device->IDE (7 bytes) + hex: 34383030360d0a + raw: b'48006\r\n' +[14:51:43.280] device->IDE (7 bytes) + hex: 34383530360d0a + raw: b'48506\r\n' +[14:51:43.780] device->IDE (7 bytes) + hex: 34393030360d0a + raw: b'49006\r\n' +[14:51:44.282] device->IDE (7 bytes) + hex: 34393530360d0a + raw: b'49506\r\n' +[14:51:44.783] device->IDE (7 bytes) + hex: 35303030360d0a + raw: b'50006\r\n' +[14:51:45.280] device->IDE (7 bytes) + hex: 35303530360d0a + raw: b'50506\r\n' +[14:51:45.787] device->IDE (7 bytes) + hex: 35313030360d0a + raw: b'51006\r\n' +[14:51:46.286] device->IDE (7 bytes) + hex: 35313530360d0a + raw: b'51506\r\n' +[14:51:46.783] device->IDE (7 bytes) + hex: 35323030360d0a + raw: b'52006\r\n' +[14:51:47.282] device->IDE (7 bytes) + hex: 35323530360d0a + raw: b'52506\r\n' +[14:51:47.780] device->IDE (7 bytes) + hex: 35333030360d0a + raw: b'53006\r\n' +[14:51:48.290] device->IDE (7 bytes) + hex: 35333530360d0a + raw: b'53506\r\n' +[14:51:48.787] device->IDE (7 bytes) + hex: 35343030360d0a + raw: b'54006\r\n' +[14:51:49.284] device->IDE (7 bytes) + hex: 35343530360d0a + raw: b'54506\r\n' +[14:51:49.782] device->IDE (7 bytes) + hex: 35353030360d0a + raw: b'55006\r\n' +[14:51:50.282] device->IDE (7 bytes) + hex: 35353530360d0a + raw: b'55506\r\n' +[14:51:50.782] device->IDE (7 bytes) + hex: 35363030360d0a + raw: b'56006\r\n' +[14:51:51.280] device->IDE (7 bytes) + hex: 35363530360d0a + raw: b'56506\r\n' +[14:51:51.786] device->IDE (7 bytes) + hex: 35373030360d0a + raw: b'57006\r\n' +[14:51:52.284] device->IDE (7 bytes) + hex: 35373530360d0a + raw: b'57506\r\n' +[14:51:52.782] device->IDE (7 bytes) + hex: 35383030360d0a + raw: b'58006\r\n' +[14:51:53.281] device->IDE (7 bytes) + hex: 35383530360d0a + raw: b'58506\r\n' +[14:51:53.788] device->IDE (7 bytes) + hex: 35393030360d0a + raw: b'59006\r\n' +[14:51:54.286] device->IDE (7 bytes) + hex: 35393530360d0a + raw: b'59506\r\n' +[14:51:54.781] device->IDE (7 bytes) + hex: 36303030360d0a + raw: b'60006\r\n' +[14:51:55.289] device->IDE (7 bytes) + hex: 36303530360d0a + raw: b'60506\r\n' +[14:51:55.787] device->IDE (7 bytes) + hex: 36313030360d0a + raw: b'61006\r\n' +[14:51:56.286] device->IDE (7 bytes) + hex: 36313530360d0a + raw: b'61506\r\n' +[14:51:56.780] device->IDE (7 bytes) + hex: 36323030360d0a + raw: b'62006\r\n' +[14:51:57.290] device->IDE (7 bytes) + hex: 36323530360d0a + raw: b'62506\r\n' +[14:51:57.788] device->IDE (7 bytes) + hex: 36333030360d0a + raw: b'63006\r\n' +[14:51:58.337] device->IDE (7 bytes) + hex: 36333530360d0a + raw: b'63506\r\n' +[14:51:58.781] device->IDE (7 bytes) + hex: 36343030360d0a + raw: b'64006\r\n' +[14:51:59.289] device->IDE (7 bytes) + hex: 36343530360d0a + raw: b'64506\r\n' +[14:51:59.787] device->IDE (7 bytes) + hex: 36353030360d0a + raw: b'65006\r\n' +[14:52:00.285] device->IDE (7 bytes) + hex: 36353530360d0a + raw: b'65506\r\n' +[14:52:00.781] device->IDE (7 bytes) + hex: 36363030360d0a + raw: b'66006\r\n' +[14:52:01.285] device->IDE (7 bytes) + hex: 36363530360d0a + raw: b'66506\r\n' +[14:52:01.840] device->IDE (7 bytes) + hex: 36373030360d0a + raw: b'67006\r\n' +[14:52:02.287] device->IDE (7 bytes) + hex: 36373530360d0a + raw: b'67506\r\n' +[14:52:02.782] device->IDE (7 bytes) + hex: 36383030360d0a + raw: b'68006\r\n' +[14:52:03.286] device->IDE (7 bytes) + hex: 36383530360d0a + raw: b'68506\r\n' +[14:52:03.789] device->IDE (7 bytes) + hex: 36393030360d0a + raw: b'69006\r\n' +[14:52:04.283] device->IDE (7 bytes) + hex: 36393530360d0a + raw: b'69506\r\n' +[14:52:04.786] device->IDE (7 bytes) + hex: 37303030360d0a + raw: b'70006\r\n' +[14:52:05.281] device->IDE (7 bytes) + hex: 37303530360d0a + raw: b'70506\r\n' +[14:52:05.784] device->IDE (7 bytes) + hex: 37313030360d0a + raw: b'71006\r\n' +[14:52:06.286] device->IDE (7 bytes) + hex: 37313530360d0a + raw: b'71506\r\n' +[14:52:06.833] device->IDE (7 bytes) + hex: 37323030360d0a + raw: b'72006\r\n' +[14:52:07.289] device->IDE (7 bytes) + hex: 37323530360d0a + raw: b'72506\r\n' +[14:52:07.787] device->IDE (7 bytes) + hex: 37333030360d0a + raw: b'73006\r\n' +[14:52:08.280] device->IDE (7 bytes) + hex: 37333530360d0a + raw: b'73506\r\n' +--- session ended --- + +--- session started 2026-02-16T14:55:12.502691 --- +real=COM77 virtual_ours=COM251 virtual_ide=COM252 baud=115200 + +--- session started 2026-02-16T14:58:14.662362 --- +real=COM77 virtual_ours=COM1 virtual_ide=None baud=115200 +[14:58:14.999] device->IDE (264 bytes) + hex: 3136333630360d0a3136343130360d0a3136343630360d0a3136353130360d0a3136353630360d0a3136363130360d0a3136363630360d0a3136373130360d0a3136373630360d0a3136383130360d0a3136383630360d0a3136393130360d0a3136393630360d0a3137303130360d0a3137303630360d0a3137313130360d0a3137313630360d0a3137323130360d0a3137323630360d0a3137333130360d0a3137333630360d0a3137343130360d0a3137343630360d0a3137353130360d0a3137353630360d0a3137363130360d0a3137363630360d0a3137373130360d0a3137373630360d0a3137383130360d0a3137383630360d0a3137393130360d0a3137393630360d0a + raw: b'163606\r\n164106\r\n164606\r\n165106\r\n165606\r\n166106\r\n166606\r\n167106\r\n167606\r\n168106\r\n168606\r\n169106\r\n169606\r\n170106\r\n170606\r\n171106\r\n171606\r\n172106\r\n172606\r\n173106\r\n173606\r\n174106\r\n174606\r\n175106\r\n175606\r\n176106\r\n176606\r\n177106\r\n177606\r\n178106\r\n178606\r\n179106\r\n179606\r\n' +[14:58:15.505] device->IDE (8 bytes) + hex: 3138303130360d0a + raw: b'180106\r\n' +[14:58:16.009] device->IDE (8 bytes) + hex: 3138303630360d0a + raw: b'180606\r\n' +[14:58:16.504] device->IDE (8 bytes) + hex: 3138313130360d0a + raw: b'181106\r\n' +[14:58:17.009] device->IDE (8 bytes) + hex: 3138313630360d0a + raw: b'181606\r\n' +[14:58:17.504] device->IDE (8 bytes) + hex: 3138323130360d0a + raw: b'182106\r\n' +[14:58:18.002] device->IDE (8 bytes) + hex: 3138323630360d0a + raw: b'182606\r\n' +[14:58:18.500] device->IDE (8 bytes) + hex: 3138333130360d0a + raw: b'183106\r\n' +[14:58:18.999] device->IDE (8 bytes) + hex: 3138333630360d0a + raw: b'183606\r\n' +[14:58:19.506] device->IDE (8 bytes) + hex: 3138343130360d0a + raw: b'184106\r\n' +[14:58:20.003] device->IDE (8 bytes) + hex: 3138343630360d0a + raw: b'184606\r\n' +[14:58:20.501] device->IDE (8 bytes) + hex: 3138353130360d0a + raw: b'185106\r\n' +[14:58:21.001] device->IDE (8 bytes) + hex: 3138353630360d0a + raw: b'185606\r\n' +[14:58:21.508] device->IDE (8 bytes) + hex: 3138363130360d0a + raw: b'186106\r\n' +[14:58:22.002] device->IDE (8 bytes) + hex: 3138363630360d0a + raw: b'186606\r\n' +[14:58:22.505] device->IDE (8 bytes) + hex: 3138373130360d0a + raw: b'187106\r\n' +[14:58:23.009] device->IDE (8 bytes) + hex: 3138373630360d0a + raw: b'187606\r\n' +[14:58:23.502] device->IDE (8 bytes) + hex: 3138383130360d0a + raw: b'188106\r\n' +[14:58:24.007] device->IDE (8 bytes) + hex: 3138383630360d0a + raw: b'188606\r\n' +[14:58:24.500] device->IDE (8 bytes) + hex: 3138393130360d0a + raw: b'189106\r\n' +[14:58:25.006] device->IDE (8 bytes) + hex: 3138393630360d0a + raw: b'189606\r\n' +[14:58:25.507] device->IDE (8 bytes) + hex: 3139303130360d0a + raw: b'190106\r\n' +[14:58:26.000] device->IDE (8 bytes) + hex: 3139303630360d0a + raw: b'190606\r\n' +[14:58:26.504] device->IDE (8 bytes) + hex: 3139313130360d0a + raw: b'191106\r\n' +[14:58:27.008] device->IDE (8 bytes) + hex: 3139313630360d0a + raw: b'191606\r\n' +[14:58:27.501] device->IDE (8 bytes) + hex: 3139323130360d0a + raw: b'192106\r\n' +[14:58:28.005] device->IDE (8 bytes) + hex: 3139323630360d0a + raw: b'192606\r\n' +[14:58:28.500] device->IDE (8 bytes) + hex: 3139333130360d0a + raw: b'193106\r\n' +[14:58:29.002] device->IDE (8 bytes) + hex: 3139333630360d0a + raw: b'193606\r\n' +[14:58:29.505] device->IDE (8 bytes) + hex: 3139343130360d0a + raw: b'194106\r\n' +[14:58:30.002] device->IDE (8 bytes) + hex: 3139343630360d0a + raw: b'194606\r\n' +[14:58:30.508] device->IDE (8 bytes) + hex: 3139353130360d0a + raw: b'195106\r\n' +[14:58:31.002] device->IDE (8 bytes) + hex: 3139353630360d0a + raw: b'195606\r\n' +[14:58:31.507] device->IDE (8 bytes) + hex: 3139363130360d0a + raw: b'196106\r\n' +[14:58:32.002] device->IDE (8 bytes) + hex: 3139363630360d0a + raw: b'196606\r\n' +[14:58:32.507] device->IDE (8 bytes) + hex: 3139373130360d0a + raw: b'197106\r\n' +[14:58:33.003] device->IDE (8 bytes) + hex: 3139373630360d0a + raw: b'197606\r\n' +[14:58:33.501] device->IDE (8 bytes) + hex: 3139383130360d0a + raw: b'198106\r\n' +[14:58:34.006] device->IDE (8 bytes) + hex: 3139383630360d0a + raw: b'198606\r\n' +[14:58:34.503] device->IDE (8 bytes) + hex: 3139393130360d0a + raw: b'199106\r\n' +[14:58:35.000] device->IDE (8 bytes) + hex: 3139393630360d0a + raw: b'199606\r\n' +[14:58:35.500] device->IDE (8 bytes) + hex: 3230303130360d0a + raw: b'200106\r\n' +[14:58:36.002] device->IDE (8 bytes) + hex: 3230303630360d0a + raw: b'200606\r\n' +[14:58:36.500] device->IDE (8 bytes) + hex: 3230313130360d0a + raw: b'201106\r\n' +--- session ended --- diff --git a/serial_proxy.py b/serial_proxy.py new file mode 100644 index 0000000..614b77b --- /dev/null +++ b/serial_proxy.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +""" +Serial port proxy (MITM): connects to a real COM device and exposes a virtual COM port +for the Arduino IDE. Records all traffic both ways while passing it through unchanged. +""" + +import sys +import threading +import time +from datetime import datetime +from pathlib import Path + +import serial +import serial.tools.list_ports + + +# Default baud rate (Arduino Uno/Nano typically use 115200 for serial monitor) +DEFAULT_BAUD = 115200 + + +def list_ports(): + """Return list of (port, description) for all COM ports.""" + return [ + (p.device, f"{p.device} — {p.description or 'Unknown'}") + for p in serial.tools.list_ports.comports() + ] + + +def select_port(prompt: str, exclude: list[str] | None = None) -> str | None: + """Show numbered list of COM ports and return selected port or None.""" + ports = list_ports() + exclude = exclude or [] + ports = [(dev, desc) for dev, desc in ports if dev not in exclude] + if not ports: + print("No COM ports found.") + return None + print(prompt) + for i, (dev, desc) in enumerate(ports, 1): + print(f" {i}. {desc}") + print(" 0. Cancel") + while True: + try: + choice = input("Choice: ").strip() + if choice == "0": + return None + idx = int(choice) + if 1 <= idx <= len(ports): + return ports[idx - 1][0] + except ValueError: + pass + print("Invalid choice.") + + +def _is_port_in_use_error(exc: BaseException) -> bool: + """True if the exception indicates the port is already open by another process.""" + msg = str(exc).lower() + return ( + "in use" in msg + or "access is denied" in msg + or "permission" in msg + or "permissionerror" in msg + or "error 5" in msg + or "error 32" in msg + ) + + +def _is_elevation_error(exc: BaseException) -> bool: + """True if the exception is WinError 740 (operation requires elevation).""" + if isinstance(exc, OSError) and getattr(exc, "winerror", None) == 740: + return True + return "740" in str(exc) or "elevation" in str(exc).lower() + + +def relay_loop( + name: str, + read_ser: serial.Serial, + write_ser: serial.Serial, + direction: str, + log_file, + log_console: bool, +): + """Read from read_ser, write to write_ser, and log each chunk. Does not close ports.""" + try: + while read_ser.is_open and write_ser.is_open: + data = read_ser.read(read_ser.in_waiting or 1) + if not data: + time.sleep(0.01) + continue + write_ser.write(data) + write_ser.flush() + ts = datetime.now().strftime("%H:%M:%S.%f")[:-3] + line = f"[{ts}] {direction} ({len(data)} bytes)\n" + if log_file: + log_file.write(line) + log_file.write(" hex: " + data.hex() + "\n") + try: + log_file.write(" raw: " + repr(data) + "\n") + except Exception: + pass + log_file.flush() + if log_console: + print(line.strip()) + print(" hex:", data.hex()) + except (serial.SerialException, OSError) as e: + if read_ser.is_open or write_ser.is_open: + print(f"[{name}] {e}") + + +def main(): + print("Serial proxy (MITM) — connect Arduino IDE to a virtual port while we talk to the real device.\n") + + # 1) Select real device port + real_port = select_port("Select the REAL device port (e.g. your Arduino):") + if not real_port: + print("Aborted.") + return 1 + + # 2) Virtual port: you already have a pair (e.g. VSPE); choose which end this app uses + other_ports = [(d, desc) for d, desc in list_ports() if d != real_port] + if not other_ports: + print("No other COM ports found. You only have your device connected.") + print("Create a virtual port pair (e.g. in VSPE), then run this script again.") + return 1 + print() + print("Select the virtual port that THIS APP will use (e.g. COM1 for VSPE).") + print("In Arduino IDE, open the OTHER end of the pair (e.g. COM2).\n") + virtual_port_ours = select_port("Virtual port for this app:", exclude=[real_port]) + if not virtual_port_ours: + print("Aborted.") + return 1 + virtual_port_ide = None + + # Baud rate + try: + baud_str = input(f"\nBaud rate [{DEFAULT_BAUD}]: ").strip() or str(DEFAULT_BAUD) + baud = int(baud_str) + except ValueError: + baud = DEFAULT_BAUD + print(f"Using baud rate: {baud}") + + # Log file + log_path = Path(__file__).resolve().parent / "serial_log.txt" + log_console = True + try: + log_file = open(log_path, "a", encoding="utf-8") + log_file.write("\n--- session started " + datetime.now().isoformat() + " ---\n") + log_file.write(f"real={real_port} virtual_ours={virtual_port_ours} virtual_ide={virtual_port_ide} baud={baud}\n") + log_file.flush() + except OSError as e: + print(f"Could not open log file: {e}") + log_file = None + + print(f"\nLogging to: {log_path}") + print("Opening ports...") + + try: + ser_real = serial.Serial(real_port, baud, timeout=0) + except (serial.SerialException, OSError) as e: + print(f"Failed to open real port {real_port}: {e}") + if _is_elevation_error(e): + print(" → Windows is requesting administrator rights. Try running this script as a NORMAL user (not 'Run as administrator').") + print(" Serial ports often work without admin; running elevated can sometimes cause this error.") + elif _is_port_in_use_error(e): + print(" → Close any app using that port (Serial Monitor, another terminal, previous script), then try again.") + if log_file: + log_file.close() + return 1 + + # Virtual port: retry a few times in case it's slow to become available + ser_virtual = None + for attempt in range(5): + try: + ser_virtual = serial.Serial(virtual_port_ours, baud, timeout=0) + break + except (serial.SerialException, OSError) as e: + in_use = _is_port_in_use_error(e) + if _is_elevation_error(e): + ser_real.close() + print(f"Failed to open virtual port {virtual_port_ours}: {e}") + print(" → Try running this script as a NORMAL user (not 'Run as administrator').") + if log_file: + log_file.close() + return 1 + if attempt < 4: + print(f" Could not open {virtual_port_ours} (attempt {attempt + 1}/5), retrying in 2s...") + if in_use: + print(" → Close Arduino IDE Serial Monitor and any other app using that port, then wait for retry.") + time.sleep(2) + else: + ser_real.close() + print(f"Failed to open virtual port {virtual_port_ours}: {e}") + if in_use: + print("\nPort is in use by another process. Do this then run the script again:") + print(" • Close Arduino IDE Serial Monitor (and any other app using that port).") + print(" • Close any other window running this script.") + else: + print("\nMake sure the virtual port exists and is available, then run this script again.") + if log_file: + log_file.close() + return 1 + + if virtual_port_ide: + print(f"\n>>> In Arduino IDE, select port: {virtual_port_ide} <<<\n") + else: + print(f"\n>>> In Arduino IDE, select the OTHER end of the pair (not {virtual_port_ours}) <<<\n") + + print("Relaying and recording. Press Ctrl+C to stop.\n") + + # IDE -> device + t1 = threading.Thread( + target=relay_loop, + args=("IDE->device", ser_virtual, ser_real, "IDE->device", log_file, log_console), + daemon=True, + ) + # device -> IDE + t2 = threading.Thread( + target=relay_loop, + args=("device->IDE", ser_real, ser_virtual, "device->IDE", log_file, log_console), + daemon=True, + ) + t1.start() + t2.start() + + try: + while t1.is_alive() or t2.is_alive(): + t1.join(timeout=0.5) + t2.join(timeout=0.5) + except KeyboardInterrupt: + print("\nStopping...") + finally: + for s in (ser_real, ser_virtual): + try: + s.close() + except Exception: + pass + if log_file: + log_file.write("--- session ended ---\n") + log_file.close() + + print("Done.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())