added hax/ascii/dec readout and note appending
parent
c5113f2b6c
commit
3b2d3c9502
48
README.md
48
README.md
|
|
@ -1,52 +1,10 @@
|
|||
# Serial Proxy (MITM)
|
||||
# RealRobots 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**.
|
||||
A small Python app that sits between the Arduino IDE and a real COM device. It connects to a serial devices and passes all messages to and from a chosen virtual com device
|
||||
|
||||
## 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.
|
||||
- **Virtual COM pair** https://eterlogic.com/Products.VSPE_Download.html
|
||||
|
|
|
|||
3947
serial_log.txt
3947
serial_log.txt
File diff suppressed because it is too large
Load Diff
|
|
@ -71,6 +71,37 @@ def _is_elevation_error(exc: BaseException) -> bool:
|
|||
return "740" in str(exc) or "elevation" in str(exc).lower()
|
||||
|
||||
|
||||
# Escape sequences for ASCII display (so \r \n etc. show instead of '.')
|
||||
_ASCII_ESCAPES = {
|
||||
0x00: "\\0",
|
||||
0x07: "\\a",
|
||||
0x08: "\\b",
|
||||
0x09: "\\t",
|
||||
0x0A: "\\n",
|
||||
0x0B: "\\v",
|
||||
0x0C: "\\f",
|
||||
0x0D: "\\r",
|
||||
0x1B: "\\e",
|
||||
}
|
||||
|
||||
|
||||
def _format_bytes(data: bytes) -> dict[str, str]:
|
||||
"""Return hex, dec, and ascii representations of data for logging."""
|
||||
hex_str = " ".join(f"{b:02X}" for b in data) if len(data) <= 32 else " ".join(f"{b:02X}" for b in data[:32]) + " ..."
|
||||
dec_str = " ".join(str(b) for b in data) if len(data) <= 48 else " ".join(str(b) for b in data[:48]) + " ..."
|
||||
# Printable ASCII; common controls as \r \n \t etc.; other non-printable as \xNN
|
||||
parts = []
|
||||
for b in data:
|
||||
if b in _ASCII_ESCAPES:
|
||||
parts.append(_ASCII_ESCAPES[b])
|
||||
elif 32 <= b <= 126:
|
||||
parts.append(chr(b))
|
||||
else:
|
||||
parts.append(f"\\x{b:02X}")
|
||||
ascii_str = "".join(parts)
|
||||
return {"hex": hex_str, "dec": dec_str, "ascii": ascii_str}
|
||||
|
||||
|
||||
def relay_loop(
|
||||
name: str,
|
||||
read_ser: serial.Serial,
|
||||
|
|
@ -78,6 +109,7 @@ def relay_loop(
|
|||
direction: str,
|
||||
log_file,
|
||||
log_console: bool,
|
||||
log_lock: threading.Lock,
|
||||
):
|
||||
"""Read from read_ser, write to write_ser, and log each chunk. Does not close ports."""
|
||||
try:
|
||||
|
|
@ -90,17 +122,19 @@ def relay_loop(
|
|||
write_ser.flush()
|
||||
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||
line = f"[{ts}] {direction} ({len(data)} bytes)\n"
|
||||
fmts = _format_bytes(data)
|
||||
if log_file:
|
||||
with log_lock:
|
||||
log_file.write(line)
|
||||
log_file.write(" hex: " + data.hex() + "\n")
|
||||
try:
|
||||
log_file.write(" raw: " + repr(data) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
log_file.write(f" hex: {fmts['hex']}\n")
|
||||
log_file.write(f" dec: {fmts['dec']}\n")
|
||||
log_file.write(f" ascii: {fmts['ascii']}\n")
|
||||
log_file.flush()
|
||||
if log_console:
|
||||
print(line.strip())
|
||||
print(" hex:", data.hex())
|
||||
print(f" hex: {fmts['hex']}")
|
||||
print(f" dec: {fmts['dec']}")
|
||||
print(f" ascii: {fmts['ascii']}")
|
||||
except (serial.SerialException, OSError) as e:
|
||||
if read_ser.is_open or write_ser.is_open:
|
||||
print(f"[{name}] {e}")
|
||||
|
|
@ -204,22 +238,41 @@ def main():
|
|||
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")
|
||||
print("Relaying and recording. Press Ctrl+C to stop.")
|
||||
print("Type a note and press Enter to annotate the log before testing an action.\n")
|
||||
|
||||
log_lock = threading.Lock()
|
||||
|
||||
def note_logger():
|
||||
"""Read lines from stdin and write them to the log as [NOTE] annotations."""
|
||||
while True:
|
||||
try:
|
||||
line = input()
|
||||
except EOFError:
|
||||
break
|
||||
if line.strip() and log_file:
|
||||
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||
with log_lock:
|
||||
log_file.write(f"[{ts}] NOTE: {line}\n")
|
||||
log_file.flush()
|
||||
print(" (note logged)")
|
||||
|
||||
# IDE -> device
|
||||
t1 = threading.Thread(
|
||||
target=relay_loop,
|
||||
args=("IDE->device", ser_virtual, ser_real, "IDE->device", log_file, log_console),
|
||||
args=("IDE->device", ser_virtual, ser_real, "IDE->device", log_file, log_console, log_lock),
|
||||
daemon=True,
|
||||
)
|
||||
# device -> IDE
|
||||
t2 = threading.Thread(
|
||||
target=relay_loop,
|
||||
args=("device->IDE", ser_real, ser_virtual, "device->IDE", log_file, log_console),
|
||||
args=("device->IDE", ser_real, ser_virtual, "device->IDE", log_file, log_console, log_lock),
|
||||
daemon=True,
|
||||
)
|
||||
t_note = threading.Thread(target=note_logger, daemon=True)
|
||||
t1.start()
|
||||
t2.start()
|
||||
t_note.start()
|
||||
|
||||
try:
|
||||
while t1.is_alive() or t2.is_alive():
|
||||
|
|
@ -234,6 +287,7 @@ def main():
|
|||
except Exception:
|
||||
pass
|
||||
if log_file:
|
||||
with log_lock:
|
||||
log_file.write("--- session ended ---\n")
|
||||
log_file.close()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue