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"]

Reply via email to