connects to real port, and pair port, and prints traffic

main
Jake 2026-02-16 14:59:06 +08:00
commit c5113f2b6c
4 changed files with 931 additions and 0 deletions

52
README.md Normal file
View File

@ -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 dont install com0com, the app cannot create a virtual pair. Youll 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.

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
pyserial>=3.5

633
serial_log.txt Normal file
View File

@ -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 ---

245
serial_proxy.py Normal file
View File

@ -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())