Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ external/
.vscode
build/
**/.vscode

coverage.xml
**/__pycache__/
.DS_*
.coverage
.cache/

15 changes: 3 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,15 @@ A framework to support you in performing experiments with the elastic-ai.Hardwar
- [x] Vivado remote
- [x] Cached Synthesis

## Quick Start
## Testing

```bash
$ uvx --from "git+https://github.com/es-ude/elastic-ai.experiment-framework.git" eaixp --help
```
or
```bash
$ uv tool install "git+https://github.com/es-ude/elastic-ai.experiment-framework.git"
$ uv tool run eaixp --help
```
or
```bash
$ pip install "git+https://github.com/es-ude/elastic-ai.experiment-framework.git"
$ eaixp --help
$ uv run python -m pytest tests
```




### Communicating with the elastic node via elasticai runtime

1. Checkout the [elastic ai runtime](https://github.com/es-ude/elastic-ai.runtime.enV5/)
Expand Down
15 changes: 12 additions & 3 deletions devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
${uv_run} coverage run
${uv_run} coverage xml
'';
before = ["check:tests"];
};

"check:types" = {
Expand Down Expand Up @@ -79,7 +78,17 @@
"check:code-lint" = {
};

"check:tests" = {
}; # this is triggered in CI with --mode before flag
"check:unit_tests_python" = {
exec = ''
${uv_run} python -m pytest tests/unit
'';
};

"check:integration_tests" = {
exec = ''
${uv_run} python -m pytest tests/integration -m "not hardware"
'';
};

};
}
28 changes: 25 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ readme = "README.md"
authors = [{ name = "Lukas Einhaus", email = "lukas.einhaus@uni-due.de" }]
requires-python = ">=3.12"
dependencies = [
"black>=26.3.1",
"click>=8.3.1",
"crc>=7.1.0",
"fabric>=3.2.2",
"invoke>=2.2.1",
"isort>=8.0.1",
"pyrefly>=0.57.1",
"pyserial>=3.4",
"pyserial-asyncio>=0.6",
"pytest>=9.0.3",
"pytest-asyncio>=1.3.0",
"ruff>=0.15.12",
]


Expand Down Expand Up @@ -59,13 +66,28 @@ extra-dev = ["pyrefly>=0.57.1", "ty>=0.0.24"]
[tool.ty.environment]
root = ["./src"]


[tool.coverage.run]
omit = [
"tests/*py",
"src/elasticai/experiment_framework/*_test.py", # not testable
"src/elasticai/experiment_framework/*_test.py",
]
source = ["src/elasticai/experiment_framework"]
command_line = "-m pytest tests/unit"

[tool.pytest]
testpaths = ["tests/unit"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
testpaths = ["tests"]
markers = [
"hardware: requires physical hardware to be connected",
]

[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I"]

[tool.ruff.lint.isort]
known-first-party = ["elasticai"]
12 changes: 4 additions & 8 deletions src/elasticai/experiment_framework/cli.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import click
import elasticai.experiment_framework.synthesis as synth

import elasticai.experiment_framework.remote_control as rc
import elasticai.experiment_framework.synthesis as synth


@click.group()
def cli():
@click.group
def main():
pass
pass

main.add_command(rc.main, name="rc")
main.add_command(synth.main, "synth")

main()
cli.add_command(synth.main, name="synth")


if __name__ == "__main__":
Expand Down
27 changes: 23 additions & 4 deletions src/elasticai/experiment_framework/remote_control/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
from .remote_control import main, RemoteControl
from .remote_control_protocol import RemoteControlProtocol
from .devices import probe_for_devices
from .commands import Command
from .connection_provider import ConnectionProvider
from .exceptions import * # noqa: F403
from .flags import Flags
from .header import Header
from .io_stream import IOStream
from .message import Message
from .message_io import MessageIO
from .task import Task
from .task_manager import TaskManager
from .tcp_protocol_stream import TCPProtocolStream

__all__ = ["main", "RemoteControlProtocol", "probe_for_devices", "RemoteControl"]
__all__ = [
"Command",
"Flags",
"Message",
"Header",
"Task",
"IOStream",
"MessageIO",
"TaskManager",
"ConnectionProvider",
"TCPProtocolStream",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from dataclasses import dataclass


@dataclass
class NoAction:
pass


@dataclass
class SendChunk:
data: bytes
need_ack: bool = False


@dataclass
class CloseTask:
need_ack: bool = False


CallbackAction = SendChunk | NoAction | CloseTask
26 changes: 15 additions & 11 deletions src/elasticai/experiment_framework/remote_control/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@


class Command(IntEnum):
NAK = 0
ACK = 1
READ_SKELETON_ID = 2
GET_FLASH_CHUNK_SIZE = 3
WRITE_TO_FLASH = 4
READ_FROM_FLASH = 5
FPGA_POWER = 6
FPGA_LEDS = 7
MCU_LEDS = 8
INFERENCE = 9
DEPLOY_MODEL = 10
OPEN_TASK = 0x01
CLOSE_TASK = 0x02
RETURN = 0x03
DATA_CHUNK = 0x04
ACK = 0x05
NACK = 0x06
HANDSHAKE = 0x07

@classmethod
def from_value(cls, data: bytes) -> "Command":
value = int.from_bytes(data, byteorder="little")
try:
return cls(value)
except ValueError:
raise ValueError(f"Unknown command: {value:#x}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import asyncio
import logging
from contextlib import asynccontextmanager
from typing import AsyncIterator

import serial_asyncio

from .constants import (
NUM_MAX_RETRIES,
RETRY_DELAY_SECONDS,
)
from .io_stream import IOStream
from .serial_protocol_stream import SerialTransportStream
from .tcp_protocol_stream import TCPProtocolStream

_logger = logging.getLogger(__name__)


class ConnectionProvider:
def __init__(self, max_trials: int = NUM_MAX_RETRIES):
self._max_trials = max_trials

@asynccontextmanager
async def connectTCP(
self, host: str, port: int, max_trials: int | None = None
) -> AsyncIterator[IOStream]:
if max_trials is not None:
self._max_trials = max_trials

loop = asyncio.get_running_loop()

protocol = None
transport: asyncio.Transport | None = None

for attempt in range(self._max_trials):
try:

def factory():
return TCPProtocolStream(self._on_lost)

transport, protocol = await loop.create_connection(factory, host, port)

_logger.debug("[CLIENT] connected via TCP")
break

except Exception as e:
_logger.error(
f"[CLIENT] attempt TCP Connection {attempt + 1} failed: {e}"
)
await asyncio.sleep(RETRY_DELAY_SECONDS)

else:
_logger.error(f"[CLIENT] fail to connect after {self._max_trials}:")
raise ConnectionError(f"[CLIENT] fail to connect after {self._max_trials}")

try:
yield protocol
finally:
if transport is not None:
transport.close()

@asynccontextmanager
async def connectSerial(
self, port: str, baudrate: int, max_trials: int | None = None
) -> AsyncIterator[IOStream]:
if max_trials is not None:
self._max_trials = max_trials
loop = asyncio.get_running_loop()

protocol = None
transport = None

for attempt in range(self._max_trials):
try:

def factory():
return SerialTransportStream(self._on_lost)

transport, protocol = await serial_asyncio.create_serial_connection(
loop, factory, port, baudrate
)

_logger.debug("[CLIENT] connected via Serial")
break

except Exception as e:
_logger.error(
f"[CLIENT] attempt Serial Connection {attempt + 1} failed: {e}"
)
await asyncio.sleep(RETRY_DELAY_SECONDS)

else:
_logger.error(f"[CLIENT] fail to connect after {self._max_trials}:")

raise ConnectionError(f"[CLIENT] fail to connect after {self._max_trials}")

try:
yield protocol
finally:
if transport is not None:
transport.close()

def _on_lost(self, exc):
_logger.warning(f"connection lost: {exc}")
32 changes: 32 additions & 0 deletions src/elasticai/experiment_framework/remote_control/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import struct
from enum import Enum

SYNC_BYTE = 0xAA
HEADER_FORMAT = "<BBBBBH"
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
NUM_MAX_RETRIES = 3
RETRY_DELAY_SECONDS = 1.0
MAX_TASKS = 0xFF
MAX_MESSAGE_PER_TASK = 0xFF
MAX_PAYLOAD_lEN = 0xFFFF
NUM_BYTES_CHECKSUM: int = 1


CONNECTION_TIMEOUT_SECONDS = 10

RESPONSE_TIMEOUT = 5.0

NUM_BYTES_FOR_ID = 1

MAX_MSG_ID = 255


class TransportType(Enum):
TCP = 0x01
UDP = 0x02
SERIAL = 0x03


MAX_CONNECTED_DEVICES = 4

NUM_BYTES_OFFSET_msg_id_IN_PAYLOAD = 0
Loading
Loading