Core¶
simulator ¶
The thin layer between a transport and a protocol, where chaos gets its turn.
ProtocolSimulator ¶
Bases: Protocol
Anything with a name and a bytes-in, bytes-out handle qualifies. The bar is low.
Simulator
dataclass
¶
Ask the protocol for a reply, then let chaos ruin it if chaos is enabled.
handle ¶
Return the protocol's reply, possibly dropped, mangled, delayed or sent twice.
Duplicates come back glued together in one buffer, which is exactly how they'd look on the wire anyway.
Source code in src/morpheus_vlt/core/simulator.py
build_handler ¶
Wrap sim in the bytes handler a transport wants. The CLI and the pytest fixture share it.
Unidesa frames arrive however the kernel feels like delivering them: half a frame now, three glued together later. For that family the handler buffers the stream, cuts it into whole frames and answers each one. Everybody else gets their bytes as they come and buffers on their own.
Either way, if the protocol blows up, the error gets logged and the reply is empty. The client sits there wondering, which is honestly what real hardware does, and the server lives to answer the next request instead of taking the whole line down with it.
Source code in src/morpheus_vlt/core/simulator.py
chaos ¶
Fault injection for outgoing replies.
Real serial lines drop bytes, double up, flip bits and take their sweet time. Your parser has to survive all of that, and the only way to find out is to do it to it on purpose. Chaos is love.
ChaosProfile
dataclass
¶
ChaosProfile(
enabled: bool = False,
drop_rate: float = 0.0,
duplicate_rate: float = 0.0,
corrupt_rate: float = 0.0,
flip_bit_rate: float = 0.0,
delay_ms_min: int = 0,
delay_ms_max: int = 0,
seed: int | None = None,
)
Fault injection for outgoing bytes.
Every rate is a probability between 0 and 1, and none of them matter unless enabled is True.
Want the same disaster twice? Pass seed, or set MORPHEUS_VLT_CHAOS_SEED. Heads up: the seed
goes into the global random module, so anything else in the process using random gets
reproducible too, whether it asked or not.
apply ¶
Mess with payload and return 0, 1 or 2 copies of whatever survived.
The order is: drop (game over, empty list), corrupt one byte, flip one bit, sleep, then maybe duplicate. A duplicate is a copy of the already mangled reply, because if it's going to be wrong it might as well be consistently wrong.
Source code in src/morpheus_vlt/core/chaos.py
transport ¶
Byte pipes: a TCP server and a fake serial port.
Neither one knows or cares what protocol is running. Bytes come in, the handler gets called, bytes go out. The dumbest possible design, which is why it works.
ServerConfig
dataclass
¶
Where the TCP server listens and how much it reads per gulp.
PtyConfig
dataclass
¶
PTY settings. link_path is the friendly symlink, raw turns off the tty's opinions.
TCPByteServer ¶
Minimal TCP server: request bytes in, response bytes out.
One thread per client and no framing at all. A single recv can hold half a message or three
of them glued together, and sorting that out is the handler's problem. start blocks, so run
it in a thread if you want your terminal back.
Source code in src/morpheus_vlt/core/transport.py
start ¶
Bind, listen, and accept clients until stop closes the socket out from under us.
Source code in src/morpheus_vlt/core/transport.py
stop ¶
Close the listening socket. Clients already connected finish their current request.
PtyByteServer ¶
PTY-backed byte server for serial-style clients.
Opens a pseudo-terminal, optionally drops a symlink to it at a name you can remember, and serves whoever opens the other end. Your pyserial code thinks it found a real port. It didn't. Linux/macOS only, because Windows and PTYs have never been on speaking terms.
When nothing arrives for 100 ms the handler gets called with b"". Most protocols shrug and return nothing. SAS uses it to keep announcing its address until the host shows up.
Source code in src/morpheus_vlt/core/transport.py
start ¶
Open the PTY, make the symlink, then pump bytes until stop. Blocks, like its cousin.
Source code in src/morpheus_vlt/core/transport.py
stop ¶
Close both PTY ends, remove the symlink. Leave the campsite cleaner than you found it.
Source code in src/morpheus_vlt/core/transport.py
framing ¶
Unidesa/Cirsa frame helpers.
A frame looks like STX SD SO NM CMD ND DATA CHK EXT. STX is 0x02, EXT is 0x03, ND is the data
length and CHK is an "XOR55" checksum, which is a byte sum wearing a 0x55 mask and calling itself
security. Eight bytes of overhead, ND bytes of payload, zero bytes of mercy.
UnidesaMessage
dataclass
¶
UnidesaMessage(
stx: int,
sd: int,
so: int,
nm: int,
cmd: int,
nd: int,
data: bytes,
chk: int,
ext: int,
)
One parsed Unidesa frame, field by field, exactly as it came off the wire.
parse
classmethod
¶
Slice raw into fields. Doesn't check anything yet, that's what validate is for.
Source code in src/morpheus_vlt/core/framing.py
validate ¶
Raise ValueError if the delimiters are wrong or the checksum doesn't add up.
Source code in src/morpheus_vlt/core/framing.py
xor55_checksum ¶
Add up every byte, XOR with 0x55, keep the low byte. That's the whole algorithm.
build_unidesa_reply ¶
Wrap data in a complete frame with STX, length, checksum and EXT.
Source code in src/morpheus_vlt/core/framing.py
split_unidesa_frames ¶
Yield every complete frame in buf, eating them as it goes.
Garbage in front of an STX gets thrown away. A partial frame at the end stays in buf and
waits for the rest to show up. TCP doesn't care where your frames end, so somebody has to.
Source code in src/morpheus_vlt/core/framing.py
factory ¶
Turns a protocol name into a ready-to-lie simulator.
build_protocol ¶
build_protocol(
name: str,
state: MachineState,
protocol_type: str | None,
addr: int,
dex_file: str | None = None,
dex_comm_id: str | None = None,
dex_revision: str | None = None,
dex_response_code: str | None = None,
) -> Any
Build the simulator for name and hand it the shared state.
protocol_type only matters to VDAI, addr only to SAS, and the dex_* knobs only to DEX.
Everybody else ignores what they don't need, like a sane coworker. An unknown name exits the
program instead of guessing, because guessing is how you end up debugging at 2 AM.