Expand shared Sprinter MCP with managed launch, raw reads and key input

This commit is contained in:
Александр Петров
2026-09-17 22:57:27 +03:00
parent a43e7bda89
commit 5dc7998324
22 changed files with 1120 additions and 67 deletions
+95 -5
View File
@@ -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')