Implementation Details¶
Technical details about the pysiphon implementation.
Architecture¶
pysiphon is built as a gRPC client that communicates with the Siphon service.
graph TB
A[Python Application] --> B[pysiphon.client.SiphonClient]
C[pysiphon.cli] --> B
B --> D[gRPC Stub]
D --> E[gRPC Channel]
E --> F[Siphon Server]
F --> G[Target Process]
Package Structure¶
pysiphon/
├── __init__.py # Package exports
├── client.py # Core SiphonClient class (~650 lines)
├── cli.py # CLI interface (~630 lines)
├── utils.py # Helper functions (~150 lines)
└── generated/ # Generated gRPC code
├── __init__.py
├── siphon_service_pb2.py # Protobuf messages
└── siphon_service_pb2_grpc.py # gRPC service stubs
Core Components¶
SiphonClient (client.py)¶
The main client class that wraps all gRPC communication.
Key Features:
- Context manager support for automatic cleanup
- 100MB message size limits (matching C++ client)
- Consistent dictionary return values
- Comprehensive error handling
Method Categories:
- Initialization: Config loading, memory/input/capture setup
- Attributes: Get/set with type conversion
- Input: Keyboard and mouse control
- Capture: Screenshot functionality
- Execution: Remote command execution
- Recording: High-frequency data recording
CLI (cli.py)¶
Dual-mode CLI built with Click framework.
Modes:
- Interactive: REPL-style session with command history
- Single-command: Direct execution with proper exit codes
Features:
- Global
--hostoption for server selection - Comprehensive help system
- Shell completion support
- Error handling with meaningful messages
Utilities (utils.py)¶
Helper functions for common operations.
Functions:
hex_to_bytes()/bytes_to_hex()- Hex string conversionparse_config_file()- TOML configuration parsingsave_frame_image()- Image saving with format detectionformat_bytes_size()- Human-readable byte formatting
gRPC Integration¶
Channel Configuration¶
options = [
('grpc.max_receive_message_length', 100 * 1024 * 1024), # 100MB
('grpc.max_send_message_length', 100 * 1024 * 1024), # 100MB
]
channel = grpc.insecure_channel(host, options=options)
stub = pb2_grpc.SiphonServiceStub(channel)
RPC Call Pattern¶
All RPC methods follow this pattern:
def rpc_method(self, params) -> Dict[str, Any]:
try:
request = pb2.RequestType()
# Populate request
response = self.stub.RpcMethod(request)
return {
"success": response.success,
"message": response.message,
# Additional fields
}
except grpc.RpcError as e:
return {
"success": False,
"message": f"RPC failed: {e.details()}"
}
Streaming RPC (Download)¶
def download_recording(self, session_id: str, output_path: str) -> bool:
request = pb2.DownloadRecordingRequest()
request.session_id = session_id
with open(output_path, 'wb') as f:
for chunk in self.stub.DownloadRecording(request):
f.write(chunk.data)
# Progress tracking
if chunk.is_final:
break
return True
Type Handling¶
Oneof Fields¶
Protobuf oneof fields require special handling:
# Get attribute (oneof value)
value_case = response.WhichOneof("value")
if value_case == "int_value":
return {"value": response.int_value, "value_type": "int"}
elif value_case == "float_value":
return {"value": response.float_value, "value_type": "float"}
# etc.
# Set attribute (oneof value)
if value_type == "int":
request.int_value = int(value)
elif value_type == "float":
request.float_value = float(value)
# etc.
Type Conversions¶
| Python Type | Protobuf Type | Notes |
|---|---|---|
int |
int32 |
32-bit signed |
float |
float |
32-bit IEEE 754 |
bytes |
bytes |
Direct mapping |
bool |
bool |
Direct mapping |
Image Processing¶
Frames are captured in BGRA format and converted to RGBA for PIL:
# BGRA to RGBA conversion
pixel_array = bytearray(pixels)
for i in range(0, len(pixel_array), 4):
# Swap B and R channels
pixel_array[i], pixel_array[i+2] = pixel_array[i+2], pixel_array[i]
# Create PIL Image
img = Image.frombytes("RGBA", (width, height), bytes(pixel_array))
Format Detection¶
File format is determined by extension:
# Auto-convert for JPEG (no alpha channel)
if filename.lower().endswith(('.jpg', '.jpeg')):
img = img.convert("RGB")
img.save(filename) # PIL detects format from extension
Configuration Parsing¶
TOML files are parsed using tomli:
with open(config_path, "rb") as f:
config = tomli.load(f)
# Extract process info
process_name = config["process_info"]["name"]
# Extract attributes
for attr_name, attr_config in config["attributes"].items():
# Build protobuf message
attr_proto = request.attributes.add()
attr_proto.name = attr_name
attr_proto.pattern = attr_config["pattern"]
attr_proto.offsets.extend(attr_config["offsets"])
# etc.
Error Handling¶
Consistent Return Values¶
All methods return dictionaries with at least:
{
"success": bool, # Operation succeeded
"message": str, # Human-readable message
# ... method-specific fields
}
Exception Handling¶
try:
response = self.stub.Method(request)
return {"success": response.success, ...}
except grpc.RpcError as e:
return {"success": False, "message": f"RPC failed: {e.details()}"}
except Exception as e:
return {"success": False, "message": f"Error: {str(e)}"}
CLI Implementation¶
Interactive Mode¶
Uses a simple command loop:
while True:
command_line = input("\n> ").strip()
parts = command_line.split()
command = parts[0].lower()
args = parts[1:]
if command == "get":
# Handle get command
elif command == "set":
# Handle set command
# etc.
Single-Command Mode¶
Uses Click's command groups:
@cli.group()
@click.option('--host', default='localhost:50051')
def cli(host):
# Setup
pass
@cli.command()
@click.argument('attribute')
def get(attribute):
# Handle get command
pass
Performance Considerations¶
Recording Performance¶
Target: 60 FPS (16.67ms per frame)
Factors affecting performance: - Number of attributes - Attribute complexity (array vs int) - Network latency - System load
Optimization Techniques¶
- Reuse connections: Single channel for all operations
- Binary format: Protobuf is efficient
- Streaming: Download uses streaming RPC
- Context managers: Automatic cleanup
Testing Approach¶
Manual testing covers:
- Imports: Package structure
- CLI: Both modes, all commands
- API: All public methods
- Error handling: Invalid inputs, connection failures
- Performance: Recording at target FPS
Code Generation¶
gRPC code is generated from siphon_service.proto:
python -m grpc_tools.protoc -I. \
--python_out=pysiphon/generated \
--grpc_python_out=pysiphon/generated \
siphon_service.proto
Post-generation fix required:
# In pysiphon/generated/siphon_service_pb2_grpc.py
# Change:
import siphon_service_pb2 as siphon__service__pb2
# To:
from . import siphon_service_pb2 as siphon__service__pb2
Dependencies¶
Runtime Dependencies¶
- grpcio: gRPC framework
- grpcio-tools: Protobuf compiler
- tomli: TOML parsing (Python 3.10 compatible)
- Pillow: Image processing
- click: CLI framework
Development Dependencies¶
- mkdocs: Documentation generator
- mkdocs-material: Material theme
- mkdocstrings: API documentation
- pymdown-extensions: Markdown extensions
Differences from C++ Client¶
Python implementation adds:
- Context managers:
with SiphonClient() as client: - PIL integration: Direct PIL Image objects
- Dictionary returns: Consistent return format
- Type inference: Automatic in
get_attribute() - CLI flexibility: Both interactive and single-command modes
Maintains:
- All RPC methods: Complete feature parity
- Message sizes: 100MB limits
- Error handling: Consistent patterns
- Recording: Same HDF5 format
Future Enhancements¶
Potential improvements:
- Async API support
- Connection pooling
- Caching layer
- Type stubs (PEP 561)
- Progress callbacks
- Batch operations
- WebSocket alternative