1 // Copyright 2024 The Pigweed Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 // use this file except in compliance with the License. You may obtain a copy of 5 // the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 // License for the specific language governing permissions and limitations under 13 // the License. 14 #pragma once 15 16 // __ ___ ___ _ _ ___ _ _ ___ 17 // \ \ / /_\ | _ \ \| |_ _| \| |/ __| 18 // \ \/\/ / _ \| / .` || || .` | (_ | 19 // \_/\_/_/ \_\_|_\_|\_|___|_|\_|\___| 20 // _____ _____ ___ ___ ___ __ __ ___ _ _ _____ _ _ 21 // | __\ \/ / _ \ __| _ \_ _| \/ | __| \| |_ _/_\ | | 22 // | _| > <| _/ _|| /| || |\/| | _|| .` | | |/ _ \| |__ 23 // |___/_/\_\_| |___|_|_\___|_| |_|___|_|\_| |_/_/ \_\____| 24 // 25 // This module is in an early, experimental state. The APIs are in flux and may 26 // change without notice. Please do not rely on it in production code, but feel 27 // free to explore and share feedback with the Pigweed team! 28 29 #include <cstddef> 30 #include <cstdint> 31 #include <limits> 32 #include <utility> 33 34 #include "pw_async2/dispatcher.h" 35 #include "pw_async2/poll.h" 36 #include "pw_bytes/span.h" 37 #include "pw_channel/properties.h" 38 #include "pw_multibuf/multibuf.h" 39 #include "pw_result/result.h" 40 #include "pw_status/status.h" 41 42 namespace pw::channel { 43 44 /// @addtogroup pw_channel 45 /// @{ 46 47 /// The basic `Channel` type. Unlike `AnyChannel`, the `Channel`'s properties 48 /// are expressed in template parameters and thus reflected in the type. 49 /// 50 /// Properties must be specified in order (`kDatagram`, `kReliable`, 51 /// `kReadable`, `kWritable`, `kSeekable`) and without duplicates. 52 /// 53 /// To implement a `Channel`, inherit from `ChannelImpl` with the specified 54 /// properties. 55 template <DataType kDataType, Property... kProperties> 56 class Channel { 57 public: 58 /// Returns the data type of this channel. data_type()59 [[nodiscard]] static constexpr DataType data_type() { return kDataType; } 60 61 /// Returns whether the channel type is reliable. reliable()62 [[nodiscard]] static constexpr bool reliable() { 63 return ((kProperties == Property::kReliable) || ...); 64 } 65 /// Returns whether the channel type is seekable. seekable()66 [[nodiscard]] static constexpr bool seekable() { 67 return ((kProperties == Property::kSeekable) || ...); 68 } 69 /// Returns whether the channel type is readable. readable()70 [[nodiscard]] static constexpr bool readable() { 71 return ((kProperties == Property::kReadable) || ...); 72 } 73 /// Returns whether the channel type is writable. writable()74 [[nodiscard]] static constexpr bool writable() { 75 return ((kProperties == Property::kWritable) || ...); 76 } 77 78 /// True if the channel type supports and is open for reading. Always false 79 /// for write-only channels. 80 [[nodiscard]] constexpr bool is_read_open() const; 81 82 /// True if the channel type supports and is open for writing. Always false 83 /// for read-only channels. 84 [[nodiscard]] constexpr bool is_write_open() const; 85 86 /// True if the channel is open for reading or writing. is_read_or_write_open()87 [[nodiscard]] constexpr bool is_read_or_write_open() const { 88 return is_read_open() || is_write_open(); 89 } 90 91 /// @copydoc AnyChannel::PendRead 92 async2::Poll<Result<multibuf::MultiBuf>> PendRead(async2::Context& cx); 93 /// @copydoc AnyChannel::PendReadyToWrite 94 async2::Poll<Status> PendReadyToWrite(pw::async2::Context& cx); 95 /// @copydoc AnyChannel::PendAllocateBuffer 96 async2::Poll<std::optional<multibuf::MultiBuf>> PendAllocateWriteBuffer( 97 async2::Context& cx, size_t min_bytes); 98 /// @copydoc AnyChannel::StageWrite 99 Status StageWrite(multibuf::MultiBuf&& data); 100 /// @copydoc AnyChannel::PendWrite 101 async2::Poll<Status> PendWrite(async2::Context& cx); 102 /// @copydoc AnyChannel::PendClose 103 async2::Poll<pw::Status> PendClose(async2::Context& cx); 104 105 // Conversions 106 107 /// Channels may be implicitly converted to other compatible channel types. 108 template <typename Sibling, 109 typename = internal::EnableIfConversionIsValid<Channel, Sibling>> 110 constexpr operator Sibling&() { 111 return as<Sibling>(); 112 } 113 template <typename Sibling, 114 typename = internal::EnableIfConversionIsValid<Channel, Sibling>> 115 constexpr operator const Sibling&() const { 116 return as<Sibling>(); 117 } 118 119 /// Returns a reference to this channel as another compatible channel type. 120 template <typename Sibling> as()121 [[nodiscard]] Sibling& as() { 122 internal::CheckThatConversionIsValid<Channel, Sibling>(); 123 return static_cast<Sibling&>(static_cast<AnyChannel&>(*this)); 124 } 125 template <typename Sibling> as()126 [[nodiscard]] const Sibling& as() const { 127 internal::CheckThatConversionIsValid<Channel, Sibling>(); 128 return static_cast<const Sibling&>(static_cast<const AnyChannel&>(*this)); 129 } 130 131 /// Returns a reference to this channel as another channel with the specified 132 /// properties, which must be compatible. 133 template <Property... kOtherChannelProperties> as()134 [[nodiscard]] auto& as() { 135 return as<Channel<data_type(), kOtherChannelProperties...>>(); 136 } 137 template <Property... kOtherChannelProperties> as()138 [[nodiscard]] const auto& as() const { 139 return as<Channel<data_type(), kOtherChannelProperties...>>(); 140 } 141 142 [[nodiscard]] Channel<DataType::kByte, kProperties...>& IgnoreDatagramBoundaries()143 IgnoreDatagramBoundaries() { 144 static_assert(kDataType == DataType::kDatagram, 145 "IgnoreDatagramBoundaries() may only be called to use a " 146 "datagram channel to a byte channel"); 147 return static_cast<Channel<DataType::kByte, kProperties...>&>( 148 static_cast<AnyChannel&>(*this)); 149 } 150 151 [[nodiscard]] const Channel<DataType::kByte, kProperties...>& IgnoreDatagramBoundaries()152 IgnoreDatagramBoundaries() const { 153 static_assert(kDataType == DataType::kDatagram, 154 "IgnoreDatagramBoundaries() may only be called to use a " 155 "datagram channel to a byte channel"); 156 return static_cast<const Channel<DataType::kByte, kProperties...>&>( 157 static_cast<const AnyChannel&>(*this)); 158 } 159 160 private: 161 static_assert(internal::PropertiesAreValid<kProperties...>()); 162 163 friend class AnyChannel; 164 165 explicit constexpr Channel() = default; 166 }; 167 168 /// A generic data channel that may support reading or writing bytes or 169 /// datagrams. 170 /// 171 /// Note that this channel should be used from only one ``pw::async::Task`` 172 /// at a time, as the ``Pend`` methods are only required to remember the 173 /// latest ``pw::async2::Context`` that was provided. Notably, this means 174 /// that it is not possible to read from the channel in one task while 175 /// writing to it from another task: a single task must own and operate 176 /// the channel. In the future, a wrapper will be offered which will 177 /// allow the channel to be split into a read half and a write half which 178 /// can be used from independent tasks. 179 /// 180 /// To implement a `Channel`, inherit from `ChannelImpl` with the specified 181 /// properties. 182 class AnyChannel 183 : private Channel<DataType::kByte, kReadable>, 184 private Channel<DataType::kByte, kWritable>, 185 private Channel<DataType::kByte, kReadable, kWritable>, 186 private Channel<DataType::kByte, kReliable, kReadable>, 187 private Channel<DataType::kByte, kReliable, kWritable>, 188 private Channel<DataType::kByte, kReliable, kReadable, kWritable>, 189 private Channel<DataType::kDatagram, kReadable>, 190 private Channel<DataType::kDatagram, kWritable>, 191 private Channel<DataType::kDatagram, kReadable, kWritable>, 192 private Channel<DataType::kDatagram, kReliable, kReadable>, 193 private Channel<DataType::kDatagram, kReliable, kWritable>, 194 private Channel<DataType::kDatagram, kReliable, kReadable, kWritable> { 195 public: 196 virtual ~AnyChannel() = default; 197 198 // Returned by Position() if getting the position is not supported. 199 // TODO: b/323622630 - `Seek` and `Position` are not yet implemented. 200 // static constexpr size_t kUnknownPosition = 201 // std::numeric_limits<size_t>::max(); 202 203 // Channel properties 204 205 /// Returns the data type of the channel implementation. data_type()206 [[nodiscard]] constexpr DataType data_type() const { return data_type_; } 207 208 /// Returns whether the channel implementation is reliable. reliable()209 [[nodiscard]] constexpr bool reliable() const { 210 return (properties_ & Property::kReliable) != 0; 211 } 212 213 /// Returns whether the channel implementation is seekable. seekable()214 [[nodiscard]] constexpr bool seekable() const { 215 return (properties_ & Property::kSeekable) != 0; 216 } 217 218 /// Returns whether the channel implementation is readable. readable()219 [[nodiscard]] constexpr bool readable() const { 220 return (properties_ & Property::kReadable) != 0; 221 } 222 223 /// Returns whether the channel implementation is writable. writable()224 [[nodiscard]] constexpr bool writable() const { 225 return (properties_ & Property::kWritable) != 0; 226 } 227 228 /// True if the channel is open for reading. Always false for write-only 229 /// channels. is_read_open()230 [[nodiscard]] constexpr bool is_read_open() const { return read_open_; } 231 232 /// True if the channel is open for writing. Always false for read-only 233 /// channels. is_write_open()234 [[nodiscard]] constexpr bool is_write_open() const { return write_open_; } 235 236 /// True if the channel is open for either reading or writing. is_read_or_write_open()237 [[nodiscard]] constexpr bool is_read_or_write_open() const { 238 return read_open_ || write_open_; 239 } 240 241 // Read API 242 243 /// Returns a `pw::multibuf::MultiBuf` with read data, if available. If data 244 /// is not available, invokes `cx.waker()` when it becomes available. 245 /// 246 /// For datagram channels, each successful read yields one complete 247 /// datagram, which may contain zero or more bytes. For byte stream channels, 248 /// each successful read yields one or more bytes. 249 /// 250 /// Channels only support one read operation / waker at a time. 251 /// 252 /// @returns @rst 253 /// 254 /// .. pw-status-codes:: 255 /// 256 /// OK: Data was read into a MultiBuf. 257 /// 258 /// UNIMPLEMENTED: The channel does not support reading. 259 /// 260 /// FAILED_PRECONDITION: The channel is closed. 261 /// 262 /// OUT_OF_RANGE: The end of the stream was reached. This may be though 263 /// of as reaching the end of a file. Future reads may succeed after 264 /// ``Seek`` ing backwards, but no more new data will be produced. The 265 /// channel is still open; writes and seeks may succeed. 266 /// 267 /// @endrst PendRead(async2::Context & cx)268 async2::Poll<Result<multibuf::MultiBuf>> PendRead(async2::Context& cx) { 269 if (!is_read_open()) { 270 return Status::FailedPrecondition(); 271 } 272 async2::Poll<Result<multibuf::MultiBuf>> result = DoPendRead(cx); 273 if (result.IsReady() && result->status().IsFailedPrecondition()) { 274 set_read_closed(); 275 } 276 return result; 277 } 278 279 // Write API 280 281 /// Checks whether a writeable channel is *currently* writeable. 282 /// 283 /// This should be called before attempting to ``Write``, and may be called 284 /// before allocating a write buffer if trying to reduce memory pressure. 285 /// 286 /// This method will return: 287 /// 288 /// * Ready(OK) - The channel is currently writeable, and a single caller 289 /// may proceed to ``Write``. 290 /// * Ready(UNIMPLEMENTED) - The channel does not support writing. 291 /// * Ready(FAILED_PRECONDITION) - The channel is closed for writing. 292 /// * Pending - ``cx`` will be awoken when the channel becomes writeable 293 /// again. 294 /// 295 /// Note: this method will always return ``Ready`` for non-writeable 296 /// channels. PendReadyToWrite(pw::async2::Context & cx)297 async2::Poll<Status> PendReadyToWrite(pw::async2::Context& cx) { 298 if (!is_write_open()) { 299 return Status::FailedPrecondition(); 300 } 301 async2::Poll<Status> result = DoPendReadyToWrite(cx); 302 if (result.IsReady() && result->IsFailedPrecondition()) { 303 set_write_closed(); 304 } 305 return result; 306 } 307 308 /// Attempts to allocate a write buffer of at least `min_bytes` bytes. 309 /// 310 /// On success, returns a `MultiBuf` of at least `min_bytes`. 311 /// This buffer should be filled with data and then passed back into 312 /// `StageWrite`. The user may shrink or fragment the `MultiBuf` during 313 /// its own usage of the buffer, but the `MultiBuf` should be restored 314 /// to its original shape before it is passed to `StageWrite`. 315 /// 316 /// Users should not wait on the result of `PendAllocateWriteBuffer` while 317 /// holding an existing `MultiBuf` from `PendAllocateWriteBuffer`, as this 318 /// can result in deadlocks. 319 /// 320 /// This method will return: 321 /// 322 /// * Ready(buffer) - A buffer of the requested size is provided. 323 /// * Ready(std::nullopt) - `min_bytes` is larger than the maximum buffer 324 /// size this channel can allocate. 325 /// * Pending - No buffer of at least `min_bytes` is available. The task 326 /// associated with the provided `pw::async2::Context` will be awoken 327 /// when a sufficiently-sized buffer becomes available. PendAllocateWriteBuffer(async2::Context & cx,size_t min_bytes)328 async2::Poll<std::optional<multibuf::MultiBuf>> PendAllocateWriteBuffer( 329 async2::Context& cx, size_t min_bytes) { 330 return DoPendAllocateWriteBuffer(cx, min_bytes); 331 } 332 333 /// Writes using a previously allocated MultiBuf. Returns a token that 334 /// refers to this write. These tokens are monotonically increasing, and 335 /// PendWrite() returns the value of the latest token it has flushed. 336 /// 337 /// The ``MultiBuf`` argument to ``Write`` may consist of either: 338 /// (1) A single ``MultiBuf`` allocated by ``PendAllocateWriteBuffer`` 339 /// that has not been combined with any other ``MultiBuf`` s 340 /// or ``Chunk``s OR 341 /// (2) A ``MultiBuf`` containing any combination of buffers from sources 342 /// other than ``PendAllocateWriteBuffer``. 343 /// 344 /// This requirement allows for more efficient use of memory in case (1). 345 /// For example, a ring-buffer implementation of a ``Channel`` may 346 /// specialize ``PendAllocateWriteBuffer`` to return the next section of the 347 /// buffer available for writing. 348 /// 349 /// @returns @rst 350 /// May fail with the following error codes: 351 /// 352 /// .. pw-status-codes:: 353 /// 354 /// OK: Data was accepted by the channel. 355 /// 356 /// UNIMPLEMENTED: The channel does not support writing. 357 /// 358 /// UNAVAILABLE: The write failed due to a transient error (only applies 359 /// to unreliable channels). 360 /// 361 /// FAILED_PRECONDITION: The channel is closed. 362 /// 363 /// @endrst StageWrite(multibuf::MultiBuf && data)364 Status StageWrite(multibuf::MultiBuf&& data) { 365 if (!is_write_open()) { 366 return Status::FailedPrecondition(); 367 } 368 Status status = DoStageWrite(std::move(data)); 369 if (status.IsFailedPrecondition()) { 370 set_write_closed(); 371 } 372 return status; 373 } 374 375 /// Completes pending writes. 376 /// 377 /// Returns a ``async2::Poll`` indicating whether or the write has completed. 378 /// 379 /// * Ready(OK) - All data has been successfully written. 380 /// * Ready(UNIMPLEMENTED) - The channel does not support writing. 381 /// * Ready(FAILED_PRECONDITION) - The channel is closed. 382 /// * Pending - Writing is not complete. PendWrite(async2::Context & cx)383 async2::Poll<Status> PendWrite(async2::Context& cx) { 384 if (!is_write_open()) { 385 return Status::FailedPrecondition(); 386 } 387 async2::Poll<Status> status = DoPendWrite(cx); 388 if (status.IsReady() && status->IsFailedPrecondition()) { 389 set_write_closed(); 390 } 391 return status; 392 } 393 394 /// Seek changes the position in the stream. 395 /// 396 /// TODO: b/323622630 - `Seek` and `Position` are not yet implemented. 397 /// 398 /// Any ``PendRead`` or ``Write`` calls following a call to ``Seek`` will be 399 /// relative to the new position. Already-written data still being flushed 400 /// will be output relative to the old position. 401 /// 402 /// @returns @rst 403 /// 404 /// .. pw-status-codes:: 405 /// 406 /// OK: The current position was successfully changed. 407 /// 408 /// UNIMPLEMENTED: The channel does not support seeking. 409 /// 410 /// FAILED_PRECONDITION: The channel is closed. 411 /// 412 /// NOT_FOUND: The seek was to a valid position, but the channel is no 413 /// longer capable of seeking to this position (partially seekable 414 /// channels only). 415 /// 416 /// OUT_OF_RANGE: The seek went beyond the end of the stream. 417 /// 418 /// @endrst 419 Status Seek(async2::Context& cx, ptrdiff_t position, Whence whence); 420 421 /// Returns the current position in the stream, or `kUnknownPosition` if 422 /// unsupported. 423 /// 424 /// TODO: b/323622630 - `Seek` and `Position` are not yet implemented. 425 size_t Position() const; 426 427 /// Closes the channel, flushing any data. 428 /// 429 /// @returns @rst 430 /// 431 /// .. pw-status-codes:: 432 /// 433 /// OK: The channel was closed and all data was sent successfully. 434 /// 435 /// DATA_LOSS: The channel was closed, but not all previously written 436 /// data was delivered. 437 /// 438 /// FAILED_PRECONDITION: Channel was already closed, which can happen 439 /// out-of-band due to errors. 440 /// 441 /// @endrst PendClose(async2::Context & cx)442 async2::Poll<pw::Status> PendClose(async2::Context& cx) { 443 if (!is_read_or_write_open()) { 444 return Status::FailedPrecondition(); 445 } 446 auto result = DoPendClose(cx); 447 if (result.IsReady()) { 448 set_read_closed(); 449 set_write_closed(); 450 } 451 return result; 452 } 453 454 protected: 455 // Marks the channel as closed for reading, but does nothing else. 456 // 457 // PendClose() always marks the channel closed when DoPendClose() returns 458 // Ready(), regardless of the status. set_read_closed()459 void set_read_closed() { read_open_ = false; } 460 461 // Marks the channel as closed for writing, but does nothing else. 462 // 463 // PendClose() always marks the channel closed when DoPendClose() returns 464 // Ready(), regardless of the status. set_write_closed()465 void set_write_closed() { write_open_ = false; } 466 467 private: 468 template <DataType, Property...> 469 friend class Channel; // Allow static_casts to AnyChannel 470 471 template <DataType, Property...> 472 friend class internal::BaseChannelImpl; // Allow inheritance 473 474 // `AnyChannel` may only be constructed by deriving from `ChannelImpl`. AnyChannel(DataType type,uint8_t properties)475 explicit constexpr AnyChannel(DataType type, uint8_t properties) 476 : data_type_(type), 477 properties_(properties), 478 read_open_(readable()), 479 write_open_(writable()) {} 480 481 // Virtual interface 482 483 // Read functions 484 485 virtual async2::Poll<Result<multibuf::MultiBuf>> DoPendRead( 486 async2::Context& cx) = 0; 487 488 // Write functions 489 490 virtual async2::Poll<std::optional<multibuf::MultiBuf>> 491 DoPendAllocateWriteBuffer(async2::Context& cx, size_t min_bytes) = 0; 492 493 virtual pw::async2::Poll<Status> DoPendReadyToWrite(async2::Context& cx) = 0; 494 495 virtual Status DoStageWrite(multibuf::MultiBuf&& buffer) = 0; 496 497 virtual pw::async2::Poll<Status> DoPendWrite(async2::Context& cx) = 0; 498 499 // Seek functions 500 /// TODO: b/323622630 - `Seek` and `Position` are not yet implemented. 501 502 // virtual Status DoSeek(ptrdiff_t position, Whence whence) = 0; 503 504 // virtual size_t DoPosition() const = 0; 505 506 // Common functions 507 virtual async2::Poll<Status> DoPendClose(async2::Context& cx) = 0; 508 509 DataType data_type_; 510 uint8_t properties_; 511 bool read_open_; 512 bool write_open_; 513 }; 514 515 /// A `ByteChannel` exchanges data as a stream of bytes. 516 template <Property... kProperties> 517 using ByteChannel = Channel<DataType::kByte, kProperties...>; 518 519 /// A `DatagramChannel` exchanges data as a series of datagrams. 520 template <Property... kProperties> 521 using DatagramChannel = Channel<DataType::kDatagram, kProperties...>; 522 523 /// Unreliable byte-oriented `Channel` that supports reading. 524 using ByteReader = ByteChannel<kReadable>; 525 /// Unreliable byte-oriented `Channel` that supports writing. 526 using ByteWriter = ByteChannel<kWritable>; 527 /// Unreliable byte-oriented `Channel` that supports reading and writing. 528 using ByteReaderWriter = ByteChannel<kReadable, kWritable>; 529 530 /// Reliable byte-oriented `Channel` that supports reading. 531 using ReliableByteReader = ByteChannel<kReliable, kReadable>; 532 /// Reliable byte-oriented `Channel` that supports writing. 533 using ReliableByteWriter = ByteChannel<kReliable, kWritable>; 534 /// Reliable byte-oriented `Channel` that supports reading and writing. 535 using ReliableByteReaderWriter = ByteChannel<kReliable, kReadable, kWritable>; 536 537 /// Unreliable datagram-oriented `Channel` that supports reading. 538 using DatagramReader = DatagramChannel<kReadable>; 539 /// Unreliable datagram-oriented `Channel` that supports writing. 540 using DatagramWriter = DatagramChannel<kWritable>; 541 /// Unreliable datagram-oriented `Channel` that supports reading and writing. 542 using DatagramReaderWriter = DatagramChannel<kReadable, kWritable>; 543 544 /// Reliable datagram-oriented `Channel` that supports reading. 545 using ReliableDatagramReader = DatagramChannel<kReliable, kReadable>; 546 /// Reliable datagram-oriented `Channel` that supports writing. 547 using ReliableDatagramWriter = DatagramChannel<kReliable, kWritable>; 548 /// Reliable datagram-oriented `Channel` that supports reading and writing. 549 using ReliableDatagramReaderWriter = 550 DatagramChannel<kReliable, kReadable, kWritable>; 551 552 // Implementation extension classes. 553 554 /// Extend `ChannelImpl` to implement a channel with the specified properties. 555 /// Unavailable methods on the channel will be stubbed out. 556 /// 557 /// Alternately, inherit from `pw::channel::Implement` with a channel 558 /// reader/writer alias as the template parameter. 559 /// 560 /// A `ChannelImpl` has a corresponding `Channel` type 561 /// (`ChannelImpl<>::Channel`). Call the `channel()` method to convert the 562 /// `ChannelImpl` to its corresponding `Channel`. 563 template <DataType kDataType, Property... kProperties> 564 class ChannelImpl { 565 static_assert(internal::PropertiesAreValid<kProperties...>()); 566 }; 567 568 /// Implement a byte-oriented `Channel` with the specified properties. 569 template <Property... kProperties> 570 using ByteChannelImpl = ChannelImpl<DataType::kByte, kProperties...>; 571 572 /// Implement a datagram-oriented `Channel` with the specified properties. 573 template <Property... kProperties> 574 using DatagramChannelImpl = ChannelImpl<DataType::kDatagram, kProperties...>; 575 576 /// Implement the specified `Channel` type. This is intended for use with the 577 /// reader/writer aliases: 578 /// 579 /// @code{.cpp} 580 /// class MyChannel : public pw::channel::Implement<pw::channel::ByteReader> {}; 581 /// @endcode 582 template <typename ChannelType> 583 class Implement; 584 585 /// @} 586 587 template <DataType kDataType, Property... kProperties> 588 class Implement<Channel<kDataType, kProperties...>> 589 : public ChannelImpl<kDataType, kProperties...> { 590 protected: 591 explicit constexpr Implement() = default; 592 }; 593 594 } // namespace pw::channel 595 596 // Include specializations for supported Channel types. 597 #include "pw_channel/internal/channel_specializations.h" 598