| .. _module-pw_stream: |
| |
| .. cpp:namespace-push:: pw::stream |
| |
| ========= |
| pw_stream |
| ========= |
| |
| ``pw_stream`` provides a foundational interface for streaming data from one part |
| of a system to another. In the simplest use cases, this is basically a memcpy |
| behind a reusable interface that can be passed around the system. On the other |
| hand, the flexibility of this interface means a ``pw_stream`` could terminate is |
| something more complex, like a UART stream or flash memory. |
| |
| -------- |
| Overview |
| -------- |
| At the most basic level, ``pw_stream``'s interfaces provide very simple handles |
| to enabling streaming data from one location in a system to an endpoint. |
| |
| Example: |
| |
| .. code-block:: cpp |
| |
| Status DumpSensorData(pw::stream::Writer& writer) { |
| static char temp[64]; |
| ImuSample imu_sample; |
| imu.GetSample(&info); |
| size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp)); |
| return writer.Write(temp, bytes_written); |
| } |
| |
| In this example, ``DumpSensorData()`` only cares that it has access to a |
| :cpp:class:`Writer` that it can use to stream data to using ``Writer::Write()``. |
| The :cpp:class:`Writer` itself can be backed by anything that can act as a data |
| "sink." |
| |
| --------------------- |
| pw::stream Interfaces |
| --------------------- |
| There are three basic capabilities of a stream: |
| |
| * Reading -- Bytes can be read from the stream. |
| * Writing -- Bytes can be written to the stream. |
| * Seeking -- The position in the stream can be changed. |
| |
| ``pw_stream`` provides a family of stream classes with different capabilities. |
| The most basic class, :cpp:class:`Stream` guarantees no functionality, while the |
| most capable class, :cpp:class:`SeekableReaderWriter` supports reading, writing, |
| and seeking. |
| |
| Usage overview |
| ============== |
| |
| +-------------------------------------------+-----------------+------------------------------+ |
| | pw::stream Interfaces | Accept in APIs? | Extend to create new stream? | |
| +===========================================+=================+==============================+ |
| | :cpp:class:`Stream` | ❌ | ❌ | |
| +-------------------------------------------+-----------------+------------------------------+ |
| | :cpp:class:`Reader` | ✅ | ❌ | |
| | | | | |
| | :cpp:class:`Writer` | | | |
| | | | | |
| | :cpp:class:`ReaderWriter` | | | |
| +-------------------------------------------+-----------------+------------------------------+ |
| | :cpp:class:`SeekableReader` | ✅ | ✅ | |
| | | | | |
| | :cpp:class:`SeekableWriter` | | | |
| | | | | |
| | :cpp:class:`SeekableReaderWriter` | | | |
| +-------------------------------------------+-----------------+------------------------------+ |
| | :cpp:class:`RelativeSeekableReader` | ✅ (rarely) | ✅ | |
| | | | | |
| | :cpp:class:`RelativeSeekableWriter` | | | |
| | | | | |
| | :cpp:class:`RelativeSeekableReaderWriter` | | | |
| +-------------------------------------------+-----------------+------------------------------+ |
| | :cpp:class:`NonSeekableReader` | ❌ | ✅ | |
| | | | | |
| | :cpp:class:`NonSeekableWriter` | | | |
| | | | | |
| | :cpp:class:`NonSeekableReaderWriter` | | | |
| +-------------------------------------------+-----------------+------------------------------+ |
| |
| Interface documentation |
| ======================= |
| Summary documentation for the ``pw_stream`` interfaces is below. See the API |
| comments in `pw_stream/public/pw_stream/stream.h |
| <https://cs.opensource.google/pigweed/pigweed/+/main:pw_stream/public/pw_stream/stream.h>`_ |
| for full details. |
| |
| .. cpp:class:: Stream |
| |
| A generic stream that may support reading, writing, and seeking, but makes no |
| guarantees about whether any operations are supported. Stream serves as the |
| base for the Reader, Writer, and ReaderWriter interfaces. |
| |
| Stream cannot be extended directly. Instead, work with one of the derived |
| classes that explicitly supports the required functionality. Stream should |
| almost never be used in APIs; accept a derived class with the required |
| capabilities instead. |
| |
| All Stream methods are blocking. They return when the requested operation |
| completes. |
| |
| **Public methods** |
| |
| .. cpp:function:: bool readable() const |
| |
| True if :cpp:func:`Read` is supported. |
| |
| .. cpp:function:: bool writable() const |
| |
| True if :cpp:func:`Write` is supported. |
| |
| .. cpp:function:: bool seekable() const |
| |
| True if :cpp:func:`Seek` is supported. |
| |
| .. cpp:function:: Result<ByteSpan> Read(ByteSpan buffer) |
| .. cpp:function:: Result<ByteSpan> Read(void* buffer, size_t size_bytes) |
| |
| Reads data from the stream into the provided buffer, if supported. As many |
| bytes as are available up to the buffer size are copied into the buffer. |
| Remaining bytes may by read in subsequent calls. |
| |
| Returns: |
| |
| * OK - Between 1 and dest.size_bytes() were successfully read. Returns |
| the span of read bytes. |
| * UNIMPLEMENTED - This stream does not support writing. |
| * FAILED_PRECONDITION - The Reader is not in state to read data. |
| * RESOURCE_EXHAUSTED - Unable to read any bytes at this time. No bytes |
| read. Try again once bytes become available. |
| * OUT_OF_RANGE - Reader has been exhausted, similar to EOF. No bytes were |
| read, no more will be read. |
| |
| .. cpp:function:: Status Write(ConstByteSpan data) |
| |
| Writes the provided data to the stream, if supported. |
| |
| Returns: |
| |
| * OK - Data was successfully accepted by the stream. |
| * UNIMPLEMENTED - This stream does not support writing. |
| * FAILED_PRECONDITION - The writer is not in a state to accept data. |
| * RESOURCE_EXHAUSTED - The writer was unable to write all of requested data |
| at this time. No data was written. |
| * OUT_OF_RANGE - The Writer has been exhausted, similar to EOF. No data was |
| written; no more will be written. |
| |
| |
| .. cpp:function:: Status Seek(ptrdiff_t offset, Whence origin = kBeginning) |
| |
| Changes the current read & write position in the stream, if supported. |
| |
| Returns: |
| |
| * OK - Successfully updated the position. |
| * UNIMPLEMENTED - Seeking is not supported for this stream. |
| * OUT_OF_RANGE - Attempted to seek beyond the bounds of the stream. The |
| position is unchanged. |
| |
| .. cpp:function:: size_t Tell() const |
| |
| Returns the current read & write position in the stream, if supported. |
| Returns ``Stream::kUnknownPosition`` (``size_t(-1)``) if unsupported. |
| |
| .. cpp:function:: size_t ConservativeReadLimit() const |
| |
| Likely minimum bytes available to read. Returns ``kUnlimited`` |
| (``size_t(-1)``) if there is no limit or it is unknown. |
| |
| .. cpp:function:: size_t ConservativeWriteLimit() const |
| |
| Likely minimum bytes available to write. Returns ``kUnlimited`` |
| (``size_t(-1)``) if there is no limit or it is unknown. |
| |
| **Private virtual methods** |
| |
| Stream's public methods are non-virtual. The public methods call private |
| virtual methods that are implemented by derived classes. |
| |
| .. cpp:function:: private virtual StatusWithSize DoRead(ByteSpan destination) |
| |
| Virtual :cpp:func:`Read` function implemented by derived classes. |
| |
| .. cpp:function:: private virtual Status DoWrite(ConstByteSpan data) |
| |
| Virtual :cpp:func:`Write` function implemented by derived classes. |
| |
| .. cpp:function:: private virtual Status DoSeek(ptrdiff_t offset, Whence origin) |
| |
| Virtual :cpp:func:`Seek` function implemented by derived classes. |
| |
| .. cpp:function:: private virtual size_t DoTell() const |
| |
| Virtual :cpp:func:`Tell` function optionally implemented by derived classes. |
| The default implementation always returns ``kUnknownPosition``. |
| |
| .. cpp:function:: private virtual size_t ConservativeLimit(LimitType limit_type) |
| |
| Virtual function optionally implemented by derived classes that is used for |
| :cpp:func:`ConservativeReadLimit` and :cpp:func:`ConservativeWriteLimit`. |
| The default implementation returns ``kUnlimited`` or ``0`` depending on |
| whether the stream is readable/writable. |
| |
| Reader interfaces |
| ----------------- |
| .. cpp:class:: Reader : public Stream |
| |
| A Stream that supports writing but not reading. The Write() method is hidden. |
| |
| Use in APIs when: |
| * Must read from, but not write to, a stream. |
| * May or may not need seeking. Use a SeekableReader& if seeking is |
| required. |
| |
| Inherit from when: |
| * Reader cannot be extended directly. Instead, extend SeekableReader, |
| NonSeekableReader, or (rarely) RelativeSeekableReader, as appropriate. |
| |
| A Reader may or may not support seeking. Check seekable() or try calling |
| Seek() to determine if the stream is seekable. |
| |
| .. cpp:class:: SeekableReader : public RelativeSeekableReader |
| |
| A Reader that fully supports seeking. |
| |
| Use in APIs when: |
| * Absolute seeking is required. Use Reader& if seeking is not required or |
| seek failures can be handled gracefully. |
| |
| Inherit from when: |
| * Implementing a reader that supports absolute seeking. |
| |
| .. cpp:class:: RelativeSeekableReader : public Reader |
| |
| A Reader that at least partially supports seeking. Seeking within some range |
| of the current position works, but seeking beyond that or from other origins |
| may or may not be supported. The extent to which seeking is possible is NOT |
| exposed by this API. |
| |
| Use in APIs when: |
| * Relative seeking is required. Usage in APIs should be rare; generally |
| Reader should be used instead. |
| |
| Inherit from when: |
| * Implementing a Reader that can only support seeking near the current |
| position. |
| |
| A buffered Reader that only supports seeking within its buffer is a good |
| example of a RelativeSeekableReader. |
| |
| .. cpp:class:: NonSeekableReader : public Reader |
| |
| A Reader that does not support seeking. The Seek() method is hidden. |
| |
| Use in APIs when: |
| * Do NOT use in APIs! If seeking is not required, use Reader& instead. |
| |
| Inherit from when: |
| * Implementing a Reader that does not support seeking. |
| |
| Writer interfaces |
| ----------------- |
| .. cpp:class:: Writer : public Stream |
| |
| A Stream that supports writing but not reading. The Read() method is hidden. |
| |
| Use in APIs when: |
| * Must write to, but not read from, a stream. |
| * May or may not need seeking. Use a SeekableWriter& if seeking is |
| required. |
| |
| Inherit from when: |
| * Writer cannot be extended directly. Instead, extend SeekableWriter, |
| NonSeekableWriter, or (rarely) RelativeSeekableWriter, as appropriate. |
| |
| A Writer may or may not support seeking. Check seekable() or try calling |
| Seek() to determine if the stream is seekable. |
| |
| .. cpp:class:: SeekableWriter : public RelativeSeekableWriter |
| |
| A Writer that fully supports seeking. |
| |
| Use in APIs when: |
| * Absolute seeking is required. Use Writer& if seeking is not required or |
| seek failures can be handled gracefully. |
| |
| Inherit from when: |
| * Implementing a writer that supports absolute seeking. |
| |
| |
| .. cpp:class:: RelativeSeekableWriter : public Writer |
| |
| A Writer that at least partially supports seeking. Seeking within some range |
| of the current position works, but seeking beyond that or from other origins |
| may or may not be supported. The extent to which seeking is possible is NOT |
| exposed by this API. |
| |
| Use in APIs when: |
| * Relative seeking is required. Usage in APIs should be rare; generally |
| Writer should be used instead. |
| |
| Inherit from when: |
| * Implementing a Writer that can only support seeking near the current |
| position. |
| |
| A buffered Writer that only supports seeking within its buffer is a good |
| example of a RelativeSeekableWriter. |
| |
| .. cpp:class:: NonSeekableWriter : public Writer |
| |
| A Writer that does not support seeking. The Seek() method is hidden. |
| |
| Use in APIs when: |
| * Do NOT use in APIs! If seeking is not required, use Writer& instead. |
| |
| Inherit from when: |
| * Implementing a Writer that does not support seeking. |
| |
| ReaderWriter interfaces |
| ----------------------- |
| .. cpp:class:: ReaderWriter : public Stream |
| |
| A Stream that supports both reading and writing. |
| |
| Use in APIs when: |
| * Must both read from and write to a stream. |
| * May or may not need seeking. Use a SeekableReaderWriter& if seeking is |
| required. |
| |
| Inherit from when: |
| * Cannot extend ReaderWriter directly. Instead, extend |
| SeekableReaderWriter, NonSeekableReaderWriter, or (rarely) |
| RelativeSeekableReaderWriter, as appropriate. |
| |
| A ReaderWriter may or may not support seeking. Check seekable() or try |
| calling Seek() to determine if the stream is seekable. |
| |
| .. cpp:class:: SeekableReaderWriter : public RelativeSeekableReaderWriter |
| |
| A ReaderWriter that fully supports seeking. |
| |
| Use in APIs when: |
| * Absolute seeking is required. Use ReaderWriter& if seeking is not |
| required or seek failures can be handled gracefully. |
| |
| Inherit from when: |
| * Implementing a writer that supports absolute seeking. |
| |
| .. cpp:class:: RelativeSeekableReaderWriter : public ReaderWriter |
| |
| A ReaderWriter that at least partially supports seeking. Seeking within some |
| range of the current position works, but seeking beyond that or from other |
| origins may or may not be supported. The extent to which seeking is possible |
| is NOT exposed by this API. |
| |
| Use in APIs when: |
| * Relative seeking is required. Usage in APIs should be rare; generally |
| ReaderWriter should be used instead. |
| |
| Inherit from when: |
| * Implementing a ReaderWriter that can only support seeking near the |
| current position. |
| |
| A buffered ReaderWriter that only supports seeking within its buffer is a |
| good example of a RelativeSeekableReaderWriter. |
| |
| .. cpp:class:: NonSeekableReaderWriter : public ReaderWriter |
| |
| A ReaderWriter that does not support seeking. The Seek() method is hidden. |
| |
| Use in APIs when: |
| * Do NOT use in APIs! If seeking is not required, use ReaderWriter& |
| instead. |
| |
| Inherit from when: |
| * Implementing a ReaderWriter that does not support seeking. |
| |
| --------------- |
| Implementations |
| --------------- |
| ``pw_stream`` includes a few stream implementations for general use. |
| |
| .. cpp:class:: MemoryWriter : public SeekableWriter |
| |
| The ``MemoryWriter`` class implements the :cpp:class:`Writer` interface by |
| backing the data destination with an **externally-provided** memory buffer. |
| ``MemoryWriterBuffer`` extends ``MemoryWriter`` to internally provide a memory |
| buffer. |
| |
| The ``MemoryWriter`` can be accessed like a standard C++ container. The |
| contents grow as data is written. |
| |
| .. cpp:class:: MemoryReader : public SeekableReader |
| |
| The ``MemoryReader`` class implements the :cpp:class:`Reader` interface by |
| backing the data source with an **externally-provided** memory buffer. |
| |
| .. cpp:class:: NullReaderWriter : public SeekableReaderWriter |
| |
| ``NullReaderWriter`` is a no-op stream implementation, similar to |
| ``/dev/null``. Writes are always dropped. Reads always return |
| ``OUT_OF_RANGE``. Seeks have no effect. |
| |
| .. cpp:class:: StdFileWriter : public SeekableWriter |
| |
| ``StdFileWriter`` wraps an ``std::ofstream`` with the :cpp:class:`Writer` |
| interface. |
| |
| .. cpp:class:: StdFileReader : public SeekableReader |
| |
| ``StdFileReader`` wraps an ``std::ifstream`` with the :cpp:class:`Reader` |
| interface. |
| |
| ------------------ |
| Why use pw_stream? |
| ------------------ |
| |
| Standard API |
| ============ |
| ``pw_stream`` provides a standard way for classes to express that they have the |
| ability to write data. Writing to one sink versus another sink is a matter of |
| just passing a reference to the appropriate :cpp:class:`Writer`. |
| |
| As an example, imagine dumping sensor data. If written against a random HAL |
| or one-off class, there's porting work required to write to a different sink |
| (imagine writing over UART vs dumping to flash memory). Building a "dumping" |
| implementation against the :cpp:class:`Writer` interface prevents a dependency |
| on a bespoke API that would require porting work. |
| |
| Similarly, after building a :cpp:class:`Writer` implementation for a Sink that |
| data could be dumped to, that same :cpp:class:`Writer` can be reused for other |
| contexts that already write data to the :cpp:class:`pw::stream::Writer` |
| interface. |
| |
| Before: |
| |
| .. code-block:: cpp |
| |
| // Not reusable, depends on `Uart`. |
| void DumpSensorData(Uart& uart) { |
| static char temp[64]; |
| ImuSample imu_sample; |
| imu.GetSample(&info); |
| size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp)); |
| uart.Transmit(temp, bytes_written, /*timeout_ms=*/ 200); |
| } |
| |
| After: |
| |
| .. code-block:: cpp |
| |
| // Reusable; no more Uart dependency! |
| Status DumpSensorData(Writer& writer) { |
| static char temp[64]; |
| ImuSample imu_sample; |
| imu.GetSample(&info); |
| size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp)); |
| return writer.Write(temp, bytes_written); |
| } |
| |
| Reduce intermediate buffers |
| =========================== |
| Often functions that write larger blobs of data request a buffer is passed as |
| the destination that data should be written to. This *requires* a buffer to be |
| allocated, even if the data only exists in that buffer for a very short period |
| of time before it's written somewhere else. |
| |
| In situations where data read from somewhere will immediately be written |
| somewhere else, a :cpp:class:`Writer` interface can cut out the middleman |
| buffer. |
| |
| Before: |
| |
| .. code-block:: cpp |
| |
| // Requires an intermediate buffer to write the data as CSV. |
| void DumpSensorData(Uart& uart) { |
| char temp[64]; |
| ImuSample imu_sample; |
| imu.GetSample(&info); |
| size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp)); |
| uart.Transmit(temp, bytes_written, /*timeout_ms=*/ 200); |
| } |
| |
| After: |
| |
| .. code-block:: cpp |
| |
| // Both DumpSensorData() and RawSample::AsCsv() use a Writer, eliminating the |
| // need for an intermediate buffer. |
| Status DumpSensorData(Writer& writer) { |
| RawSample imu_sample; |
| imu.GetSample(&info); |
| return imu_sample.AsCsv(writer); |
| } |
| |
| Prevent buffer overflow |
| ======================= |
| When copying data from one buffer to another, there must be checks to ensure the |
| copy does not overflow the destination buffer. As this sort of logic is |
| duplicated throughout a codebase, there's more opportunities for bound-checking |
| bugs to sneak in. ``Writers`` manage this logic internally rather than pushing |
| the bounds checking to the code that is moving or writing the data. |
| |
| Similarly, since only the :cpp:class:`Writer` has access to any underlying |
| buffers, it's harder for functions that share a :cpp:class:`Writer` to |
| accidentally clobber data written by others using the same buffer. |
| |
| Before: |
| |
| .. code-block:: cpp |
| |
| Status BuildPacket(Id dest, span<const std::byte> payload, |
| span<std::byte> dest) { |
| Header header; |
| if (dest.size_bytes() + payload.size_bytes() < sizeof(Header)) { |
| return Status::ResourceExhausted(); |
| } |
| header.dest = dest; |
| header.src = DeviceId(); |
| header.payload_size = payload.size_bytes(); |
| |
| memcpy(dest.data(), &header, sizeof(header)); |
| // Forgetting this line would clobber buffer contents. Also, using |
| // a temporary span instead could leave `dest` to be misused elsewhere in |
| // the function. |
| dest = dest.subspan(sizeof(header)); |
| memcpy(dest.data(), payload.data(), payload.size_bytes()); |
| } |
| |
| After: |
| |
| .. code-block:: cpp |
| |
| Status BuildPacket(Id dest, span<const std::byte> payload, Writer& writer) { |
| Header header; |
| header.dest = dest; |
| header.src = DeviceId(); |
| header.payload_size = payload.size_bytes(); |
| |
| writer.Write(header); |
| return writer.Write(payload); |
| } |
| |
| ------------ |
| Design notes |
| ------------ |
| |
| Sync & Flush |
| ============ |
| The :cpp:class:`pw::stream::Stream` API does not include ``Sync()`` or |
| ``Flush()`` functions. There no mechanism in the :cpp:class:`Stream` API to |
| synchronize a :cpp:class:`Reader`'s potentially buffered input with its |
| underlying data source. This must be handled by the implementation if required. |
| Similarly, the :cpp:class:`Writer` implementation is responsible for flushing |
| any buffered data to the sink. |
| |
| ``Flush()`` and ``Sync()`` were excluded from :cpp:class:`Stream` for a few |
| reasons: |
| |
| * The semantics of when to call ``Flush()``/``Sync()`` on the stream are |
| unclear. The presence of these methods complicates using a |
| :cpp:class:`Reader` or :cpp:class:`Writer`. |
| * Adding one or two additional virtual calls increases the size of all |
| :cpp:class:`Stream` vtables. |
| |
| Class hierarchy |
| =============== |
| All ``pw_stream`` classes inherit from a single, common base with all possible |
| functionality: :cpp:class:`pw::stream::Stream`. This structure has |
| some similarities with Python's `io module |
| <https://docs.python.org/3/library/io.html>`_ and C#'s `Stream class |
| <https://docs.microsoft.com/en-us/dotnet/api/system.io.stream>`_. |
| |
| An alternative approach is to have the reading, writing, and seeking portions of |
| the interface provided by different entities. This is how Go's `io |
| <https://pkg.go.dev/io package>`_ and C++'s `input/output library |
| <https://en.cppreference.com/w/cpp/io>`_ are structured. |
| |
| We chose to use a single base class for a few reasons: |
| |
| * The inheritance hierarchy is simple and linear. Despite the linear |
| hierarchy, combining capabilities is natural with classes like |
| :cpp:class:`ReaderWriter`. |
| |
| In C++, separate interfaces for each capability requires either a complex |
| virtual inheritance hierarchy or entirely separate hierarchies for each |
| capability. Separate hierarchies can become cumbersome when trying to |
| combine multiple capabilities. A :cpp:class:`SeekableReaderWriter` would |
| have to implement three different interfaces, which means three different |
| vtables and three vtable pointers in each instance. |
| * Stream capabilities are clearly expressed in the type system, while |
| naturally supporting optional functionality. A :cpp:class:`Reader` may |
| or may not support :cpp:func:`Stream::Seek`. Applications that can handle |
| seek failures gracefully way use seek on any :cpp:class:`Reader`. If seeking |
| is strictly necessary, an API can accept a :cpp:class:`SeekableReader` |
| instead. |
| |
| Expressing optional functionality in the type system is cumbersome when |
| there are distinct interfaces for each capability. ``Reader``, ``Writer``, |
| and ``Seeker`` interfaces would not be sufficient. To match the flexibility |
| of the current structure, there would have to be separate optional versions |
| of each interface, and classes for various combinations. :cpp:class:`Stream` |
| would be an "OptionalReaderOptionalWriterOptionalSeeker" in this model. |
| * Code reuse is maximized. For example, a single |
| :cpp:func:`Stream::ConservativeLimit` implementation supports many stream |
| implementations. |
| |
| Virtual interfaces |
| ================== |
| ``pw_stream`` uses virtual functions. Virtual functions enable runtime |
| polymorphism. The same code can be used with any stream implementation. |
| |
| Virtual functions have inherently has more overhead than a regular function |
| call. However, this is true of any polymorphic API. Using a C-style ``struct`` |
| of function pointers makes different trade-offs but still has more overhead than |
| a regular function call. |
| |
| For many use cases, the overhead of virtual calls insignificant. However, in |
| some extremely performance-sensitive contexts, the flexibility of the virtual |
| interface may not justify the performance cost. |
| |
| Asynchronous APIs |
| ================= |
| At present, ``pw_stream`` is synchronous. All :cpp:class:`Stream` API calls are |
| expected to block until the operation is complete. This might be undesirable |
| for slow operations, like writing to NOR flash. |
| |
| Pigweed has not yet established a pattern for asynchronous C++ APIs. The |
| :cpp:class:`Stream` class may be extended in the future to add asynchronous |
| capabilities, or a separate ``AsyncStream`` could be created. |
| |
| ------------ |
| Dependencies |
| ------------ |
| * :ref:`module-pw_assert` |
| * :ref:`module-pw_preprocessor` |
| * :ref:`module-pw_status` |
| * :ref:`module-pw_span` |
| |
| .. cpp:namespace-pop:: |
| |
| Zephyr |
| ====== |
| To enable ``pw_stream`` for Zephyr add ``CONFIG_PIGWEED_STREAM=y`` to the |
| project's configuration. |