Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ _Avoid_: IPC layer, transport
A stand-in for iMotions used when no real iMotions instance is available.
_Avoid_: Fake iMotions

**Mirror**:
A second destination the **Relay** copies every packet to, so the Python side can read a flight while iMotions records it. The copy is best effort: packets are dropped if the mirror falls behind, and the iMotions stream is never held up for it (`docs/adr/0003`).
_Avoid_: Python stream, fan-out, secondary connection, sink

### Performance measurement

**Airspeed**:
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,24 @@ Two more behaviors that catch people out:
- The eight dataref toggles do not affect what is sent over TCP. All eight values are always transmitted. The toggles control only which `<Sample>` entries appear in the generated `iMotions.xml`.
- Losing X-Plane also tears down the TCP connection. After restarting the simulator, press `Connect TCP` again.

### Sending to iMotions and Python at the same time

`Connect TCP` has one destination. To feed iMotions and the Python side from the same flight, use the mirror:

1. Connect TCP to iMotions as above.
2. Start the logger, which listens on port 5001 by default: `uv run python -m pilot_inference.capture.data_logger` from `inference/`.
3. Type its address, for example `127.0.0.1:5001`, into the second box and press `Connect mirror`.

Both then receive the same packets, byte for byte. The mirror's socket is written from its own thread, so the iMotions stream never waits on it: if the Python process is missing, slow or killed, copies are dropped or the mirror is closed, a line appears in the event log, and iMotions keeps recording at full rate. Reconnect it with `Connect mirror` once Python is listening again.

What the mirror does not promise is every packet. A reader that falls behind loses copies, which suits statistics over a stream and does not suit anything needing a complete record. Whatever reads this socket should keep reading it, on its own thread if it also does slow work.

Reconnecting opens a new connection, and the logger starts a new `flight_id` for each one unless you passed `--flight-id`. Pass it for a real session, or one flight will be split across two ids in the CSV.

With no iMotions to record to, skip the mirror and point `Connect TCP` straight at the logger instead. The mirror lives inside the iMotions connection, so it needs that connection first.

`Send Packet` goes to both destinations, which makes it a check of both links. It also means the logger writes a row of zeros, so avoid it mid-recording.

## 3. iMotions

With the relay running, generate the event-source definition that iMotions needs:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# The relay mirrors its iMotions stream rather than holding two equal connections

A flight has to reach iMotions and the Python side at the same time (#245). The relay keeps one primary TCP connection, the one `Connect TCP` opens, and an optional **Mirror** that receives a copy of every packet. Both are written from one packet list, so they cannot drift apart. The mirror's socket is owned by its own writer thread, and the TCP thread reaches it only through a bounded queue.

## Considered Options

- **Mirror inside the existing TCP thread.** Simplest, and how this was first written. Rejected after review: the thread wrote each packet to iMotions and then to the mirror, so a mirror write that blocked delayed the next iMotions write, and a connect attempt stalled the stream for the length of its timeout. The recording is the thing that cannot be redone, so it must not wait on a copy.
- **Mirror on its own writer thread, fed by a bounded queue (chosen).** The TCP thread's only mirror cost is pushing into a queue that either accepts the packet or refuses it. Connecting and writing both happen off that thread.
- **Two equal connections, either optional.** Cleaner in principle and would let the Python side run with no iMotions at all. Rejected because it doubles the connection state machine (`tcp_connect`, `tcp_disconnect`, `reap_tcp_thread`, the status line and the saved-address list all assume one stream), and the case it buys is already covered: with no iMotions, point `Connect TCP` straight at the Python listener.
- **Let the Python side read the CSV the logger already writes.** Rejected. Live grouping needs samples as they arrive, and the logger only has them because it is the thing the relay is connected to.

## Consequences

What is guaranteed: the TCP thread never waits on the mirror. Connecting, writing and failing all happen on the writer thread, and the queue is pushed with a non-blocking try-send. What is not guaranteed is delivery of every copy. When the queue is full the packet is dropped, which is the right trade for a consumer doing statistics over a stream, and the wrong one for anything that needs a complete record.

A mirror falling behind is reported once when it starts dropping and once when it catches up, not per packet, because the event log is capped at 200 entries and 160 packets a second would empty it.

The Python side has to read its socket continuously, on a thread of its own if it also does slow work. A clustering refit that stops reading for long enough will first lose copies and then, once the write timeout expires, lose the mirror, which does not reconnect by itself.

Nothing reconnects the mirror automatically. If Python restarts, press `Connect mirror` again.

`Send Packet` reaches both destinations, so it checks both links at once, and it writes a row of zeros to the logger as well as to iMotions.

If the Python side later needs to run without iMotions in the same session, revisit this and promote the mirror to an equal connection.
5 changes: 5 additions & 0 deletions inference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ Without it the rows are written as "unknown". `--flight-id` is optional, but pas
session: without it each relay connection gets its own generated id, so a reconnect mid-flight
splits one flight into two. The logger writes `data/session_flight_data.csv`.

Point the relay at this listener with `Connect TCP`, or, when iMotions is recording the same
flight, with `Connect mirror` so both receive the stream (see the root README). A mirror reader
must keep reading: if it stops while it does slow work it loses copies, and then the mirror
itself, which does not reconnect on its own.

## Performance groups

| Step | Module | Purpose | Input | Output |
Expand Down
18 changes: 17 additions & 1 deletion relay/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ pub(crate) enum Message {
TcpAddrFieldUpdate(String),
SavedTcpAddrSelected(String),

// Second destination for the same packets, read by the Python side
MirrorAddrFieldUpdate(String),
ConnectMirror,
DisconnectMirror,

SendPacket,
}

Expand All @@ -55,6 +60,10 @@ pub(crate) enum ToTcpThreadMessage {

// a fully built packet to write as-is, used by the Send Packet button
SendRaw(String),

/// Point the mirror at an address, or `None` to drop it. The mirror gets a
/// copy of every packet iMotions receives.
SetMirror(Option<String>),
}

pub(crate) enum FromTcpThreadMessage {
Expand All @@ -64,6 +73,13 @@ pub(crate) enum FromTcpThreadMessage {
Disconnected(String),
/// A packet was sent to remote (In bytes)
Sent(usize),
/// Error reported
/// Error reported
SendError(String),
/// Mirror destination accepted the connection
MirrorConnected(String),
/// Mirror was dropped, by the user or by a failed connect or write
MirrorDisconnected(String),
/// Mirror is not keeping up, so copies are being dropped. The iMotions
/// stream is unaffected.
MirrorLagging(String),
}
Loading
Loading