Skip to content

Commit f192213

Browse files
committed
Add Python interface for socket server
1 parent 15c5ffd commit f192213

File tree

11 files changed

+713
-6
lines changed

11 files changed

+713
-6
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ $(RUST_PROGRAMS): %: build/rust/%.bin
2323
# see https://stackoverflow.com/a/61960833
2424
build/%-0.mlog: build/%.bin scripts/bin_to_mlog.py
2525
-rm -f build/$*-[0-9].mlog build/$*-[0-9][0-9].mlog
26-
python scripts/bin_to_mlog.py build/$*.bin
26+
python -m mlogv32.scripts.bin_to_mlog build/$*.bin
2727

2828
build/rust/%.bin: FORCE | build/rust
2929
cd rust/$* && cargo robjcopy ../../build/rust/$*.bin

pyproject.toml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,120 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
15
[project]
26
name = "mlogv32"
37
version = "0.1.0"
48
requires-python = ">=3.12"
59
dependencies = [
10+
"pydantic>=2.11.5",
611
"pymsch>=0.0.11",
712
"pyperclip>=1.9.0",
13+
"riscof",
14+
"riscv-isac",
815
"typer>=0.15.4",
916
]
17+
18+
[dependency-groups]
19+
dev = [
20+
"ruff>=0.11.12",
21+
]
22+
23+
[tool.uv.sources]
24+
riscof = { git = "https://github.com/riscv/riscof", rev = "9fe3597d75" }
25+
riscv-isac = { git = "https://github.com/riscv-software-src/riscv-isac", rev = "777d2b4762" }
26+
27+
[tool.hatch.build.targets.wheel]
28+
packages = [
29+
"python/src/mlogv32",
30+
]
31+
32+
[tool.ruff]
33+
extend-exclude = [
34+
"**/venv",
35+
"**/.*",
36+
"**/node_modules",
37+
"**/__pycache__",
38+
]
39+
40+
[tool.ruff.lint]
41+
extend-select = [
42+
"I", # import sorting
43+
"RUF022", # __all__ sorting
44+
]
45+
extend-ignore = [
46+
# covered by Pyright
47+
"F821", # undefined variable
48+
"F401", # imported but unused
49+
"F841", # unused variable
50+
]
51+
52+
[tool.ruff.lint.isort]
53+
combine-as-imports = true
54+
55+
[tool.pyright]
56+
pythonVersion = "3.12"
57+
pythonPlatform = "All"
58+
59+
extraPaths = [
60+
"python/src",
61+
]
62+
include = [
63+
"python/*",
64+
]
65+
exclude = [
66+
"**/venv",
67+
"**/.*",
68+
"**/node_modules",
69+
"**/__pycache__",
70+
]
71+
72+
typeCheckingMode = "basic"
73+
74+
strictDictionaryInference = true
75+
strictListInference = true
76+
strictSetInference = true
77+
78+
reportAssertAlwaysTrue = "error"
79+
reportConstantRedefinition = "error"
80+
reportDeprecated = "error"
81+
reportFunctionMemberAccess = "error"
82+
reportIncompatibleMethodOverride = "error"
83+
reportIncompatibleVariableOverride = "error"
84+
reportIncompleteStub = "error"
85+
reportInconsistentConstructor = "error"
86+
reportInvalidStringEscapeSequence = "error"
87+
reportInvalidStubStatement = "error"
88+
reportInvalidTypeVarUse = "error"
89+
reportMatchNotExhaustive = "error"
90+
reportMissingParameterType = "error"
91+
reportOverlappingOverload = "error"
92+
reportSelfClsParameterName = "error"
93+
reportTypeCommentUsage = "error"
94+
reportUnknownParameterType = "error"
95+
reportUnnecessaryCast = "error"
96+
reportUnnecessaryContains = "error"
97+
reportUnsupportedDunderAll = "error"
98+
reportUntypedBaseClass = "error"
99+
reportUntypedClassDecorator = "error"
100+
reportUntypedFunctionDecorator = "error"
101+
reportUntypedNamedTuple = "error"
102+
reportWildcardImportFromLibrary = "error"
103+
104+
reportMissingTypeArgument = "warning"
105+
reportPrivateUsage = "warning"
106+
reportUnknownLambdaType = "warning"
107+
reportUnknownMemberType = "warning"
108+
reportUnnecessaryComparison = "warning"
109+
reportUnnecessaryIsInstance = "warning"
110+
reportUnusedClass = "warning"
111+
reportUnusedExpression = "warning"
112+
reportUnusedFunction = "warning"
113+
reportUnusedImport = "warning"
114+
reportUnusedVariable = "warning"
115+
116+
reportUnknownArgumentType = "information"
117+
reportUnknownVariableType = "information"
118+
119+
reportMissingTypeStubs = "none"
120+
reportDuplicateImport = "none"

python/src/mlogv32/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
__all__ = [
2+
"ProcessorAccess",
3+
]
4+
5+
from .processor_access import ProcessorAccess
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from __future__ import annotations
2+
3+
import socket
4+
from pathlib import Path
5+
from typing import Annotated, Any, Literal
6+
7+
from pydantic import (
8+
BaseModel as _BaseModel,
9+
ConfigDict,
10+
Field,
11+
TypeAdapter,
12+
alias_generators,
13+
)
14+
15+
16+
class ProcessorAccess:
17+
def __init__(self, hostname: str, port: int):
18+
self.hostname = hostname
19+
self.port = port
20+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
21+
22+
def socket_connect(self):
23+
self.socket.connect((self.hostname, self.port))
24+
25+
def socket_close(self):
26+
self.socket.close()
27+
28+
def flash(self, path: str | Path):
29+
path = Path(path)
30+
if not path.is_absolute():
31+
raise ValueError("Path must be absolute.")
32+
33+
self._send_request(FlashRequest(path=path))
34+
return self._recv_response()
35+
36+
def dump(self, path: str | Path):
37+
path = Path(path)
38+
if not path.is_absolute():
39+
raise ValueError("Path must be absolute.")
40+
41+
self._send_request(DumpRequest(path=path))
42+
return self._recv_response()
43+
44+
def start(self, wait: bool):
45+
self._send_request(StartRequest(wait=wait))
46+
return self._recv_response()
47+
48+
def stop(self):
49+
self._send_request(StopRequest())
50+
return self._recv_response()
51+
52+
def _send_request(self, request: Request) -> None:
53+
message = request.model_dump_json() + "\n"
54+
self.socket.sendall(message.encode("utf-8"))
55+
56+
def _recv_response(self) -> Response:
57+
with self.socket.makefile("r", encoding="utf-8") as f:
58+
line = f.readline()
59+
return response_ta.validate_json(line)
60+
61+
def __enter__(self):
62+
self.socket_connect()
63+
return self
64+
65+
def __exit__(self, *_: Any):
66+
self.socket_close()
67+
return False # propagate exceptions
68+
69+
70+
class BaseModel(_BaseModel):
71+
model_config = ConfigDict(
72+
alias_generator=alias_generators.to_camel,
73+
)
74+
75+
76+
class FlashRequest(BaseModel):
77+
type: Literal["flash"] = "flash"
78+
path: Path
79+
80+
81+
class DumpRequest(BaseModel):
82+
type: Literal["dump"] = "dump"
83+
path: Path
84+
85+
86+
class StartRequest(BaseModel):
87+
type: Literal["start"] = "start"
88+
wait: bool
89+
90+
91+
class StopRequest(BaseModel):
92+
type: Literal["stop"] = "stop"
93+
94+
95+
type Request = Annotated[
96+
FlashRequest | DumpRequest | StartRequest | StopRequest,
97+
Field(discriminator="type"),
98+
]
99+
100+
101+
class SuccessResponse(BaseModel):
102+
type: Literal["success"]
103+
message: str
104+
105+
106+
class ErrorResponse(BaseModel):
107+
type: Literal["error"]
108+
message: str
109+
110+
111+
type Response = Annotated[
112+
SuccessResponse | ErrorResponse,
113+
Field(discriminator="type"),
114+
]
115+
116+
response_ta = TypeAdapter[Response](Response)

python/src/mlogv32/py.typed

Whitespace-only changes.

python/src/mlogv32/scripts/__init__.py

Whitespace-only changes.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def flush_output(is_done: bool):
7474
# we didn't break the loop, so flush the rest of the output
7575
flush_output(is_done=True)
7676

77-
print(f"Generated {output_index} mlog file{'' if output_index == 1 else 's'}.")
77+
print(f"Generated {output_index} mlog file{'' if output_index == 1 else 's'}.") # pyright: ignore[reportUnnecessaryComparison]
7878

7979

8080
if __name__ == "__main__":
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def main(
2424
amount = ByteUtils.pop_int(data, 2, True)
2525
output[f"@{ctype}Count"] = amount
2626

27-
for i in range(0, amount):
27+
for _ in range(0, amount):
2828
name = ByteUtils.pop_UTF(data)
2929
output[ctype].append(name)
3030

@@ -37,12 +37,12 @@ class ByteUtils:
3737
@staticmethod
3838
def pop_bytes(data: bytearray, byte_count: int):
3939
out_bytes = bytearray()
40-
for i in range(byte_count):
40+
for _ in range(byte_count):
4141
out_bytes.append(data.pop(0))
4242
return out_bytes
4343

4444
@staticmethod
45-
def pop_int(data: bytearray, byte_count: int, signed=False):
45+
def pop_int(data: bytearray, byte_count: int, signed: bool = False):
4646
return int.from_bytes(ByteUtils.pop_bytes(data, byte_count), signed=signed)
4747

4848
@staticmethod

0 commit comments

Comments
 (0)