Protocols¶
unidesa ¶
Unidesa/Cirsa simulator.
Binary frames, XOR55 checksums, counters read by numeric id. Cirsa speaks the same thing, so it gets the same code under a different name. Saves money on translators.
UnidesaSimulator
dataclass
¶
UnidesaSimulator(
name: str = "unidesa",
state: MachineState = MachineState(),
selected_counters: list[int] = (
lambda: list(DEFAULT_SELECTED_COUNTER_IDS)
)(),
reply_sd: int = 255,
reply_so: int = 0,
)
Simulates Unidesa/Cirsa framing and counter reads.
The readers build frames as STX SD SO NM CMD ND DATA CHK EXT, where CHK is the XOR55 checksum. The counter response payload is: group (1) | first (1) | received (1) | [id (1) | value (4 LE)] * received
Supported: access (0x0B), logout (0x06), custom counter selection (0x2E) and counter reads (0x16). Anything else gets error 0x01. That's enough to keep the reader scripts happy, and keeping the reader scripts happy is the entire point of my existence here.
handle ¶
Answer exactly one frame. A bad frame raises ValueError instead of getting a reply.
Source code in src/morpheus_vlt/protocols/unidesa.py
sas ¶
SAS simulator, at the "shape of the bytes" level.
Real SAS runs on a 9-bit serial line where the ninth bit marks the address byte. A PTY or a TCP socket has eight bits and no opinion on the matter, so the electrical part is not emulated. What is emulated: correctly sized replies, BCD meters at the offsets the readers slice, and a CRC that saspy agrees with. The readers count bytes and cut at fixed offsets, so getting the sizes right is 90% of the job. The other 10% is also getting the sizes right.
SASSimulator
dataclass
¶
SASSimulator(
name: str = "sas",
state: MachineState = MachineState(),
addr: int = 1,
idle_announce_interval: float = 0.5,
_announce_address_until_activity: bool = True,
_last_idle_announce: float = 0.0,
_aft_registered: bool = False,
_last_aft: tuple[int, int, int, int] = (0, 0, 0, 0),
_last_transaction_id: bytes = (
lambda: bytes.fromhex(
"20202020202020202020202020202021"
)
)(),
)
SAS protocol-shape simulator for serial readers.
One request in, one reply out, no buffering. Meters come out of state.meters, mostly as BCD.
AFT transfers actually move the "credits" meter, so a test can push money in and watch it
land. Everything else is read-only and boringly consistent, which is the dream.
handle ¶
Answer one SAS request. Empty data means the line is idle (see the PTY transport).
Source code in src/morpheus_vlt/protocols/sas.py
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 | |
vdai ¶
VDAI simulator. The machine prints its stats like a receipt, the reader regexes the receipt.
Nobody had a real printout lying around, so these layouts are rebuilt backwards from the reader's
own parsing code (oblivion's lib/protocol/vdai.py): which lines its regex keeps, how it pulls the
numbers out of them, and which positions it reads. The labels are made up. The positions, the
number formats and the line shapes are exactly what the reader needs, and the tests run the
reader's own expressions against them to keep it that way.
Money meters are in cents and get printed in euros. Counts get printed as counts.
VDAISimulator
dataclass
¶
VDAISimulator(
name: str = "vdai",
state: MachineState = MachineState(),
protocol_type: str = "m",
)
ASCII-style "printer" protocol simulator.
The reader sends an ESC/P-ish "stats" request ending in a line feed, then reads whatever comes back until the line goes quiet and digs the numbers out with regexes, one set per machine family. Every complete request line gets one stats page. Partial requests wait for the rest. Silence gets silence, which matters on a PTY where the transport pokes the protocol with empty input every 100 ms.
Meters used: coin_in, bill_in, coin_out, bill_out, plays, play and win for the generic and anniversario flavors, plus the in_/in_s_/in_h_/out_ denomination counts for anniversario and bally. tot_in and tot_out are computed, not read.
handle ¶
Return one stats page per complete request line. What the request says is not consulted.
Source code in src/morpheus_vlt/protocols/vdai.py
rfranco ¶
R. Franco simulator. Plain ASCII lines, a password prompt, and Spanish. Lots of Spanish.
RFrancoSimulator
dataclass
¶
RFrancoSimulator(
name: str = "rfranco",
state: MachineState = MachineState(),
key: str = "RF",
require_key: bool = True,
echo_commands: bool = True,
)
ASCII line-oriented R. Franco simulator for the legacy reader flow.
Commands, one per line, case doesn't matter:
C -> "CLAVE:" then the next line is the key ("RF" unless you change it)
ESTADO -> "ESTADO=" plus 7 status bits
TOTALES -> play, win and plays totals
REGISTROS -> the detailed registers, only after the key, plus both door counters
Anything else gets "ERR COMANDO". Each command gets echoed back first unless echo_commands
is off. Lines end in CR, LF or CRLF, and they can arrive in as many pieces as TCP feels like.
authenticated
property
¶
True once the right key went through. Wrong key flips it back to False.
handle ¶
Buffer data, answer every complete line, keep the leftovers for next time.
Source code in src/morpheus_vlt/protocols/rfranco.py
dex ¶
DEX/UCS simulator, playing the machine (VMC) side.
DEX is how vending machines hand over their audit data: two handshakes, then an EVA-DTS text file sent in DLE-framed blocks with a CRC-16 on each one. It's a protocol from the days when a handheld terminal was cutting-edge, and it has been feeling its age ever since.
The dance, from the reader's point of view: 1. reader ENQ -> sim DLE0 2. reader DLE SOH id/op/rev ... -> sim DLE1 (or NAK if the CRC is junk) 3. reader EOT -> sim ENQ (roles swap, the machine drives now) 4. reader DLE0/DLE1 -> sim second handshake (response code, id, revision) 5. reader DLE1 -> sim EOT ENQ 6. reader ACK -> sim first data block 7. reader ACK / NAK -> next block / same block again. After the last one, EOT.
DexState ¶
Bases: Enum
What the simulator is waiting for next. See the module docstring for the full dance.
DexSimulator
dataclass
¶
DexSimulator(
name: str = "dex",
state: MachineState = MachineState(),
communication_id: bytes = DEFAULT_COMMUNICATION_ID,
revision_level: bytes = DEFAULT_REVISION_LEVEL,
response_code: bytes = DEFAULT_RESPONSE_CODE,
evadts_lines: list[str] = list(),
)
DEX/UCS simulator (VMC side).
Does the two handshakes and the data transfer, then resets and waits for the next reader.
Bytes can arrive in any size chunks. Junk between messages gets thrown away. The EVA-DTS
payload comes from a file, a list of lines, or a tiny built-in default if you gave it nothing.
state is carried around for consistency with the other protocols but nothing reads it yet.
configure_identity ¶
configure_identity(
*,
communication_id: bytes | str | None = None,
revision_level: bytes | str | None = None,
response_code: bytes | str | None = None,
) -> None
Change the identity fields. None means leave it alone. Resets any session in flight.
Source code in src/morpheus_vlt/protocols/dex.py
load_evadts_file ¶
Load the EVA-DTS payload from a text file. Bad bytes go out as '?', not as exceptions.
Source code in src/morpheus_vlt/protocols/dex.py
load_evadts_lines ¶
Use lines as the payload and pre-build the data blocks. Empty input sends one CRLF.
Source code in src/morpheus_vlt/protocols/dex.py
load_default_evadts ¶
Smallest EVA-DTS file that still looks legit: header, machine id, zero vends, trailer.
Source code in src/morpheus_vlt/protocols/dex.py
reset_session ¶
Back to square one: wait for ENQ, rewind to the first block, forget the reader's id.
Source code in src/morpheus_vlt/protocols/dex.py
handle ¶
Buffer data and run the state machine as far as it can go on what's buffered.
Source code in src/morpheus_vlt/protocols/dex.py
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | |
dex_crc16_update ¶
Feed one byte into a DEX/UCS CRC-16 (x16 + x15 + x2 + 1), one bit at a time, by hand.
Yes, a lookup table would be faster. At 9600 baud nobody is going to notice.
Source code in src/morpheus_vlt/protocols/dex.py
dex_crc16_bytes ¶
Compute the CRC-16 over data, skipping DLE and SYN bytes unless told otherwise.
It skips every 0x10, including one that's actual data and not framing. EVA-DTS files are printable ASCII, so that hasn't mattered yet. If it ever does, start looking here.