Expand shared Sprinter MCP with managed launch, raw reads and key input
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from .server import rpc_call
|
||||
from .server import RPC_GENERATION_MUTATIONS, rpc_call
|
||||
from .session import SessionError
|
||||
|
||||
|
||||
@@ -13,9 +14,23 @@ class McpSession:
|
||||
self.socket_path = str(Path(socket_path).expanduser())
|
||||
self.owner = 'mcp:' + uuid.uuid4().hex
|
||||
self._rpc = rpc
|
||||
self.identity: dict[str, str] = {}
|
||||
|
||||
def call(self, method: str, arguments: dict | None = None, timeout: float = 10):
|
||||
return self._rpc(self.socket_path, method, arguments or {}, timeout=timeout)
|
||||
if method != 'status' and not self.identity:
|
||||
self.status()
|
||||
generation = None
|
||||
if method in RPC_GENERATION_MUTATIONS:
|
||||
generation = self.status().get('generation')
|
||||
identity = dict(self.identity)
|
||||
if method in RPC_GENERATION_MUTATIONS:
|
||||
identity['generation'] = generation
|
||||
result = self._rpc(self.socket_path, method, arguments or {},
|
||||
timeout=timeout, **identity)
|
||||
if method == 'status' and 'session_id' in result:
|
||||
self.identity = {'session_id': result['session_id'],
|
||||
'build_id': result['build_id']}
|
||||
return result
|
||||
|
||||
def status(self):
|
||||
return self.call('status')
|
||||
@@ -27,11 +42,86 @@ class McpSession:
|
||||
return self.call('registers')
|
||||
|
||||
def read_memory(self, address: str, length: int = 16):
|
||||
return self.call('read_memory', {'address': self._address(address),
|
||||
'length': length})
|
||||
|
||||
@staticmethod
|
||||
def _address(address: str) -> int:
|
||||
try:
|
||||
parsed = int(address, 16 if address.lower().startswith('0x') else 10)
|
||||
except ValueError as error:
|
||||
return int(address, 16 if address.lower().startswith('0x') else 10)
|
||||
except (AttributeError, ValueError) as error:
|
||||
raise SessionError('Адрес памяти должен быть десятичным или 0xHEX') from error
|
||||
return self.call('read_memory', {'address': parsed, 'length': length})
|
||||
|
||||
def list_shares(self):
|
||||
return self.call('list_shares')
|
||||
|
||||
def list_ports(self):
|
||||
return self.call('list_ports')
|
||||
|
||||
def read_program_memory(self, address: str, length: int = 16):
|
||||
return self.call('read_program_memory', {'address': self._address(address),
|
||||
'length': length})
|
||||
|
||||
def read_share(self, tag: str, address: str, length: int = 16):
|
||||
return self.call('read_share', {'tag': tag, 'address': self._address(address),
|
||||
'length': length})
|
||||
|
||||
def read_vram(self, address: str, length: int = 16):
|
||||
return self.call('read_vram', {'address': self._address(address),
|
||||
'length': length})
|
||||
|
||||
def read_screen_pixels(self, x: int, y: int, width: int = 1, height: int = 1):
|
||||
return self.call('read_screen_pixels', {'x': x, 'y': y,
|
||||
'width': width, 'height': height})
|
||||
|
||||
def screenshot(self):
|
||||
return self.call('screenshot')
|
||||
|
||||
def press_key(self, key: str, frames: int = 3):
|
||||
"""Один физический PC-key с отпусканием даже при stop/ошибке."""
|
||||
from mame_interactive import SHIFT_KEY, resolve
|
||||
|
||||
if not isinstance(key, str) or len(key) == 0 or len(key) > 16:
|
||||
raise SessionError('Нужна одна клавиша или enter/space/tab')
|
||||
if not isinstance(frames, int) or frames < 1 or frames > 60:
|
||||
raise SessionError('frames должен быть 1..60')
|
||||
named = {'enter': '\n', 'space': ' ', 'tab': '\t'}
|
||||
symbol = named.get(key.lower(), key)
|
||||
if len(symbol) != 1:
|
||||
raise SessionError('Пока поддерживаются один символ, enter, space, tab')
|
||||
try:
|
||||
tag, mask, shift = resolve(symbol)
|
||||
except ValueError as error:
|
||||
raise SessionError(str(error)) from error
|
||||
if not self.status()['running']:
|
||||
raise SessionError('Нажатие возможно только при running CPU')
|
||||
held = []
|
||||
try:
|
||||
if shift:
|
||||
self.call('input_key', {'tag': SHIFT_KEY[0], 'mask': SHIFT_KEY[1],
|
||||
'down': True, 'owner': self.owner})
|
||||
held.append(SHIFT_KEY)
|
||||
self.call('input_key', {'tag': tag, 'mask': mask,
|
||||
'down': True, 'owner': self.owner})
|
||||
held.append((tag, mask))
|
||||
start = self.call('snapshot')['frame']
|
||||
deadline = time.monotonic() + 4
|
||||
last = start
|
||||
while time.monotonic() < deadline:
|
||||
snapshot = self.call('snapshot')
|
||||
last = snapshot['frame']
|
||||
if last - start >= frames or snapshot['state'] != 'running':
|
||||
break
|
||||
time.sleep(.01)
|
||||
else:
|
||||
raise SessionError('Клавиша не получила нужное число кадров за 4 с')
|
||||
return {'key': key, 'requested_frames': frames,
|
||||
'observed_frames': max(0, last - start),
|
||||
'stopped': snapshot['state'] != 'running'}
|
||||
finally:
|
||||
for release_tag, release_mask in reversed(held):
|
||||
self.call('input_key', {'tag': release_tag, 'mask': release_mask,
|
||||
'down': False, 'owner': self.owner})
|
||||
|
||||
def variables(self):
|
||||
return self.call('variables')
|
||||
|
||||
Reference in New Issue
Block a user