This is an automated email from the ASF dual-hosted git repository. acassis pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/nuttx-ntfc.git
commit 374333029598d56b66fcc91ffc0284f098148a6b Author: Marco Casaroli <[email protected]> AuthorDate: Sun Jul 26 14:55:18 2026 +0200 ntfc: Add optional line-buffered transport writes. Add the line_buffered per-core configuration option, disabled by default, to retain the existing byte-wise transport behavior. When enabled, the simulator and serial transports write a complete command in one operation. Buffered simulator writes also disable pexpect per-send delay. This improves local simulator performance and supports serial transports with reliable flow control. Document the option and cover both default and buffered command writes. Assisted-by: ChatGPT:GPT-5.6-Terra Signed-off-by: Marco Casaroli <[email protected]> --- Documentation/config-yaml.rst | 24 ++++++++++++++++++++++ Documentation/config.yaml | 3 +++ src/ntfc/coreconfig.py | 5 +++++ src/ntfc/device/serial.py | 18 +++++++++++------ src/ntfc/device/sim.py | 12 ++++++++++- tests/device/test_serial.py | 26 ++++++++++++++++++++++++ tests/device/test_sim.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 128 insertions(+), 7 deletions(-) diff --git a/Documentation/config-yaml.rst b/Documentation/config-yaml.rst index 3b66322..e23ad76 100644 --- a/Documentation/config-yaml.rst +++ b/Documentation/config-yaml.rst @@ -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 diff --git a/Documentation/config.yaml b/Documentation/config.yaml index 1b38027..6d17628 100644 --- a/Documentation/config.yaml +++ b/Documentation/config.yaml @@ -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" diff --git a/src/ntfc/coreconfig.py b/src/ntfc/coreconfig.py index 263725a..7026170 100644 --- a/src/ntfc/coreconfig.py +++ b/src/ntfc/coreconfig.py @@ -193,6 +193,11 @@ class CoreConfig: 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. diff --git a/src/ntfc/device/serial.py b/src/ntfc/device/serial.py index fca70fb..808bb98 100644 --- a/src/ntfc/device/serial.py +++ b/src/ntfc/device/serial.py @@ -101,13 +101,19 @@ class DeviceSerial(DeviceCommon): 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) diff --git a/src/ntfc/device/sim.py b/src/ntfc/device/sim.py index 720c5fc..6a19eaa 100644 --- a/src/ntfc/device/sim.py +++ b/src/ntfc/device/sim.py @@ -49,7 +49,9 @@ class DeviceSim(DeviceHost): uptime = self._conf.uptime # open host-based emulation - self.host_open(cmd, uptime) + child = self.host_open(cmd, uptime) + if self._conf.line_buffered: + child.delaybeforesend = 0 @property def name(self) -> str: @@ -63,6 +65,14 @@ class DeviceSim(DeviceHost): 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])) diff --git a/tests/device/test_serial.py b/tests/device/test_serial.py index 74822a2..b08c56d 100644 --- a/tests/device/test_serial.py +++ b/tests/device/test_serial.py @@ -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) diff --git a/tests/device/test_sim.py b/tests/device/test_sim.py index f9523c3..b7c5293 100644 --- a/tests/device/test_sim.py +++ b/tests/device/test_sim.py @@ -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) @@ -64,12 +65,31 @@ def test_device_sim_start_opens_host(): assert called["uptime"] == 3 +def test_device_sim_line_buffered_start_disables_send_delay(): + with patch("ntfc.coreconfig.CoreConfig") as mockdevice: + config = mockdevice.return_value + config.elf_path = "/tmp/nuttx-sim" + config.line_buffered = True + config.uptime = 0 + sim = DeviceSim(config) + + class FakeChild: + delaybeforesend = 0.05 + + child = FakeChild() + sim.host_open = lambda *_args: child + + sim.start() + assert child.delaybeforesend == 0 + + def test_device_sim_write_adds_newline(): with patch("ntfc.coreconfig.CoreConfig") as mockdevice: config = mockdevice.return_value config.os = "nuttx" config.read_poll_interval = 0.1 config.elf_path = "/tmp/nuttx-sim" + config.line_buffered = False sim = DeviceSim(config) sent = [] @@ -94,6 +114,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 = [] @@ -109,3 +130,29 @@ def test_device_sim_write_no_extra_newline(): 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 + 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"]
