Skip to content
Merged
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
24 changes: 24 additions & 0 deletions Documentation/config-yaml.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,30 @@ checks.
flash_only: true
# core1 is built/flashed only

Line-buffered command writes
============================

By default, NTFC sends a command one byte at a time. This is the safest mode
for serial targets without flow control and remains the default.

Set ``line_buffered: true`` for a core to write a complete command in one
transport operation instead. This reduces host-side overhead for ``sim`` and
can also be enabled for serial targets with reliable flow control.

.. code-block:: yaml

product:
name: "product-name"
cores:
core0:
name: "main"
device: "sim"
line_buffered: true

For the simulator, line-buffered mode also disables the pexpect per-send
delay. Do not enable this option for serial targets that cannot
reliably accept a full command at once.

**SMP (Symmetric Multi-Processing)**

In SMP mode, all cores share the same device instance, coordinated by
Expand Down
3 changes: 3 additions & 0 deletions Documentation/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ product: # many products can be supported in tests (pro
poweroff: '' # (optional) System command to power off DUT
flash_only: false # (optional) build/flash this core but skip boot checks,
# requirement validation, logs, and test execution on it
line_buffered: false # (optional) send each command in one transport write.
# Keep false for the default byte-wise behavior.
# Enable for sim or a serial transport with flow control.
dcmake: # (optional) Defines passed to CMake build
DEFINE1: "VALUE1"
DEFINE2: "VALUE2"
Expand Down
5 changes: 5 additions & 0 deletions src/ntfc/coreconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ def app_bindir(self) -> Optional[str]:
return os.path.join(os.path.dirname(self.elf_path), "bin")
return None

@property
def line_buffered(self) -> bool:
"""Return whether commands are sent in one transport write."""
return bool(self._config.get("line_buffered", False))

def kv_check(self, cfg: str) -> Any:
"""Check Kconfig option and return its value.

Expand Down
11 changes: 10 additions & 1 deletion src/ntfc/device/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,19 @@ def host_open(self, cmd: List[str], uptime: int = 0) -> pexpect.spawn:
self._cmd = cmd

logger.info(f"spawn cmd: {''.join(cmd)}")
self._child = pexpect.spawn(
child = pexpect.spawn(
"".join(cmd), timeout=10, maxread=20000, cwd=self._cwd
)

if self._conf.line_buffered:
# A line-buffered transport writes a whole command at once, so the
# per-send delay pexpect inserts between characters buys nothing.
# This is set here rather than by the caller so that it survives
# _dev_reopen(), which comes back through this function.
child.delaybeforesend = 0

self._child = child

time.sleep(uptime)

ret = self._wait_for_boot(self._conf.boot_timeout)
Expand Down
18 changes: 12 additions & 6 deletions src/ntfc/device/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,19 @@ def _write(self, data: bytes) -> None:

assert self._ser

# send char by char to avoid line length full
for c in data:
self._ser.write(bytes([c]))
if self._conf.line_buffered:
if data[-1] != ord("\n"):
data += b"\n"

# add new line if missing
if data[-1] != ord("\n"):
self._ser.write(b"\n") # pragma: no cover
self._ser.write(data)
else:
# send char by char to avoid line length full
for c in data:
self._ser.write(bytes([c]))

# add new line if missing
if data[-1] != ord("\n"):
self._ser.write(b"\n") # pragma: no cover

# read all garbage left by character echo
_ = self._read_all(timeout=0)
Expand Down
8 changes: 8 additions & 0 deletions src/ntfc/device/sim.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ def _write(self, data: bytes) -> None:

assert self._child

if self._conf.line_buffered:
if data[-1] != ord("\n"):
# Sometimes sim misses a single trailing newline.
data += b"\n\n"

self._child.send(data)
return

# send char by char to avoid line length full
for c in data:
self._child.send(bytes([c]))
Expand Down
20 changes: 20 additions & 0 deletions tests/device/test_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ def _write(self, data):
self._child.send(b"\n\n")


def test_device_host_line_buffered_survives_reopen(envconfig_dummy):
"""A line-buffered device keeps its send delay off across a reopen."""

conf = envconfig_dummy.product[0].cfg_core(0)
conf._config["line_buffered"] = True

path = "./tests/resources/nuttx/sim/nuttx"
dev = DeviceHost2(conf)

child = dev.host_open([path])
assert child.delaybeforesend == 0

# a crash brings the device back through host_open() rather than through
# the device's own start, so the setting has to be made there
child = dev._dev_reopen()
assert child.delaybeforesend == 0

dev.stop()


def test_device_host_open(envconfig_dummy, monkeypatch):

conf = envconfig_dummy.product[0].cfg_core(0)
Expand Down
26 changes: 26 additions & 0 deletions tests/device/test_serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,32 @@ def test_device_sim_internals(serial_config, serial_pair):
assert ser._write(b"a") is None


def test_device_serial_line_buffered_write(serial_config):
ser = DeviceSerial(serial_config)
sent = []

class FakeSerial:
def write(self, data):
sent.append(data)

def read(self, size):
return b""

ser._ser = FakeSerial()

assert ser._write(b"abc") is None
assert sent == [b"a", b"b", b"c", b"\n"]

sent.clear()
serial_config._config["line_buffered"] = True
assert ser._write(b"abc") is None
assert sent == [b"abc\n"]

sent.clear()
assert ser._write(b"abc\n") is None
assert sent == [b"abc\n"]


def test_device_sim_init(serial_config, serial_pair):

ser = DeviceSerial(serial_config)
Expand Down
30 changes: 30 additions & 0 deletions tests/device/test_sim.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def test_device_sim_start_opens_host():
config.os = "nuttx"
config.read_poll_interval = 0.1
config.elf_path = "/tmp/nuttx-sim"
config.line_buffered = False
config.uptime = 3
sim = DeviceSim(config)

Expand All @@ -70,6 +71,7 @@ def test_device_sim_write_adds_newline():
config.os = "nuttx"
config.read_poll_interval = 0.1
config.elf_path = "/tmp/nuttx-sim"
config.line_buffered = False
sim = DeviceSim(config)

sent = []
Expand All @@ -94,6 +96,7 @@ def test_device_sim_write_no_extra_newline():
config.os = "nuttx"
config.read_poll_interval = 0.1
config.elf_path = "/tmp/nuttx-sim"
config.line_buffered = False
sim = DeviceSim(config)

sent = []
Expand All @@ -109,3 +112,30 @@ def send(self, data):

sim._write(b"abc\n")
assert sent == [b"a", b"b", b"c", b"\n"]


def test_device_sim_line_buffered_write():
with patch("ntfc.coreconfig.CoreConfig") as mockdevice:
config = mockdevice.return_value
config.elf_path = "/tmp/nuttx-sim"
config.line_buffered = True
config.os = "nuttx"
sim = DeviceSim(config)

sent = []

class FakeChild:
def isalive(self):
return True

def send(self, data):
sent.append(data)

sim._child = FakeChild()

sim._write(b"abc")
assert sent == [b"abc\n\n"]

sent.clear()
sim._write(b"abc\n")
assert sent == [b"abc\n"]