FastLED 3.9.15
Loading...
Searching...
No Matches
manager.cpp.hpp
Go to the documentation of this file.
1
3
6#include "fl/stl/singleton.h"
7#include "fl/log/log.h"
8#include "fl/log/log.h"
9#include "fl/log/log.h"
11#include "fl/stl/chrono.h"
12#include "fl/stl/algorithm.h"
13#include "fl/stl/move.h"
14#include "fl/system/trace.h"
15#include "fl/task/executor.h"
17#include "platforms/init_channel_driver.h"
18#include "platforms/is_platform.h"
19#include "fl/stl/noexcept.h"
20
21namespace fl {
22
25 // Lazy initialization of platform-specific channel drivers
26 // C++11 guarantees thread-safe static initialization
27 static bool sInitialized = false; // okay static in header
28 if (!sInitialized) {
29 sInitialized = true;
30 platforms::initChannelDrivers();
31 }
32 return out;
33}
34
38 FL_DBG("ChannelManager: Initializing");
39
40 // Register as frame event listener for per-frame reset
42}
43
45 FL_DBG("ChannelManager: Destructor called");
46
47 // Remove self from EngineEvents listener list
49
50 for (auto& entry : mDrivers) {
51 if (entry.driver) {
52 entry.driver->setPollNeededCallback(IChannelDriver::PollNeededCallback());
53 }
54 }
55
56 // Shared drivers automatically cleaned up by shared_ptr destructors
57}
58
62
64 if (context == nullptr) {
65 return;
66 }
67 static_cast<ChannelManager*>(context)->notifyPollNeeded();
68}
69
71 return mPollNeededSignal.wait(timeoutMs);
72}
73
74u32 ChannelManager::pollNeededWaitSliceMs(u32 startTime, u32 timeoutMs) const FL_NOEXCEPT {
75 constexpr u32 kPollNeededFallbackSliceMs = 1;
76 if (timeoutMs == 0) {
77 return kPollNeededFallbackSliceMs;
78 }
79 const u32 elapsed = millis() - startTime;
80 if (elapsed >= timeoutMs) {
81 return 0;
82 }
83 const u32 remaining = timeoutMs - elapsed;
84 return remaining < kPollNeededFallbackSliceMs ? remaining : kPollNeededFallbackSliceMs;
85}
86
88 if (!driver) {
89 FL_WARN("ChannelManager::addDriver() - Null driver provided");
90 return;
91 }
92
93 // Get driver name from the driver itself
94 fl::string engineName = driver->getName();
95
96 // Reject drivers with empty names
97 if (engineName.empty()) {
98 FL_WARN("ChannelManager::addDriver() - Engine has empty name (driver->getName() returned empty string)");
99 return;
100 }
101
102 // Check if driver with this name already exists
103 bool replacing = false;
104 for (const auto& entry : mDrivers) {
105 if (entry.name == engineName) {
106 // True-duplicate fast path: same shared_ptr at same priority is
107 // a no-op (legacy clockless controllers may pre-bind the same
108 // driver singleton from many template instantiations). Skip the
109 // replace flow entirely so we don't waitForReady() or emit a
110 // spurious "Replacing" warning.
111 if (entry.driver == driver && entry.priority == priority) {
112 FL_DBG("ChannelManager::addDriver() - '" << engineName.c_str()
113 << "' already registered at priority " << priority
114 << " (idempotent no-op)");
115 return;
116 }
117 replacing = true;
118 FL_WARN("ChannelManager::addDriver() - Replacing existing driver '" << engineName.c_str() << "'");
119 break;
120 }
121 }
122
123 // If replacing, wait for all drivers to become READY
124 if (replacing) {
125 FL_DBG("ChannelManager: Waiting for all drivers to become READY before replacement");
126 waitForReady();
127
128 // Remove the old driver with matching name (shared_ptr may trigger deletion)
129 for (size_t i = 0; i < mDrivers.size(); ++i) {
130 if (mDrivers[i].name == engineName) {
131 FL_DBG("ChannelManager: Removing old driver '" << engineName.c_str() << "' (shared_ptr may delete)");
132 if (mDrivers[i].driver) {
133 mDrivers[i].driver->setPollNeededCallback(IChannelDriver::PollNeededCallback());
134 }
135 mDrivers.erase(mDrivers.begin() + i);
136 break;
137 }
138 }
139 }
140
141 // Respect exclusive driver mode: auto-disable if name doesn't match exclusive driver
142 bool enabled = true; // Default: enabled
143 if (!mExclusiveDriver.empty()) {
144 enabled = (engineName == mExclusiveDriver); // Only enable if matches exclusive driver
145 }
146
147 mDrivers.push_back({priority, driver, engineName, enabled});
148 driver->setPollNeededCallback(mPollNeededCallback);
149
150 // Build capability string for debug output. Gate the entire block behind
151 // FASTLED_HAS_DBG because the `capStr` exists ONLY to feed the FL_DBG
152 // line below. On release builds (FASTLED_HAS_DBG=0 — i.e. the default
153 // SKETCH_HAS_LARGE_MEMORY=0 path AND any -DFASTLED_LOG_VERBOSITY=0
154 // opt-in build via the gating in fl/log/log.h) the FL_DBG itself is a
155 // no-op, but without this guard the `fl::string capStr` allocation +
156 // two `if` branches still emitted code. See #2773 item 2.3 follow-up.
157#if FASTLED_HAS_DBG
158 IChannelDriver::Capabilities caps = driver->getCapabilities();
159 fl::string capStr;
160 if (caps.supportsClockless) {
161 capStr += "CLOCKLESS";
162 }
163 if (caps.supportsSpi) {
164 if (!capStr.empty()) capStr += "|";
165 capStr += "SPI";
166 }
167 if (capStr.empty()) {
168 capStr = "NONE";
169 }
170
171 FL_DBG("ChannelManager: Added driver '" << engineName.c_str() << "' (priority " << priority << ", caps: " << capStr.c_str() << ")");
172#endif
173
174 // Sort drivers by priority descending (higher values first) after each insertion
175 // Higher priority values = higher precedence (e.g., priority 50 selected over priority 10)
176 // Only 1-4 drivers expected — sort_small skips the quicksort_impl
177 // instantiation entirely (see #2907 for the bloat motivation).
178 fl::sort_small(mDrivers.begin(), mDrivers.end());
179}
180
182 if (!driver) {
183 FL_WARN("ChannelManager::removeDriver() - Null driver provided");
184 return false;
185 }
186
187 // Find and remove the driver from the list
188 for (size_t i = 0; i < mDrivers.size(); ++i) {
189 if (mDrivers[i].driver == driver) {
190 FL_DBG("ChannelManager: Removing driver '" << mDrivers[i].name << "'");
191
192 mDrivers[i].driver->setPollNeededCallback(IChannelDriver::PollNeededCallback());
193
194 // Remove using vector::erase (preserves sort order)
195 mDrivers.erase(mDrivers.begin() + i);
196 return true; // Engine found and removed
197 }
198 }
199
200 // Engine not found
201 FL_WARN("ChannelManager::removeDriver() - Engine " << driver.get() << " not found in registry");
202 return false;
203}
204
206 FL_DBG("ChannelManager: Waiting for all drivers to become READY before clearing");
207
208 // Wait for all drivers to become READY before clearing
209 // This prevents clearing drivers that are still transmitting
210 waitForReady();
211
212 FL_DBG("ChannelManager: Clearing " << mDrivers.size() << " drivers");
213
214 for (auto& entry : mDrivers) {
215 if (entry.driver) {
216 entry.driver->setPollNeededCallback(IChannelDriver::PollNeededCallback());
217 }
218 }
219
220 // Clear all drivers (shared_ptr handles cleanup automatically)
221 mDrivers.clear();
222}
223
224void ChannelManager::setDriverEnabled(const char* name, bool enabled) {
225 if (!name) {
226 FL_ERROR("ChannelManager::setDriverEnabled() - Null driver name provided");
227 return;
228 }
229
230 bool found = false;
231 for (auto& entry : mDrivers) {
232 if (entry.name == name) {
233 entry.enabled = enabled;
234 found = true;
235 FL_DBG("ChannelManager: Driver '" << name << "' " << (enabled ? "enabled" : "disabled"));
236 }
237 }
238
239 if (!found) {
240 FL_ERROR("ChannelManager::setDriverEnabled() - Driver '" << name << "' not found in registry");
241 }
242}
243
247
249 // Handle null or empty name: disable everything.
250 if (!name || !name[0]) {
251 FL_ERROR("ChannelManager::setExclusiveDriverByName() - Null or empty driver name provided");
252 mExclusiveDriver.clear();
253 for (auto& entry : mDrivers) {
254 entry.enabled = false;
255 }
256 return false;
257 }
258
259 // Store exclusive driver name for forward compatibility.
260 // When non-empty, addDriver() will auto-disable non-matching drivers.
261 mExclusiveDriver = name;
262
263 // Single-pass: enable only drivers matching the given name.
264 bool found = false;
265 for (auto& entry : mDrivers) {
266 entry.enabled = (entry.name == name);
267 found = found || entry.enabled;
268 }
269
270 if (!found) {
271 FL_ERROR("ChannelManager::setExclusiveDriverByName() - Driver '" << name << "' not found in registry");
272 }
273 return found;
274}
275
276bool ChannelManager::setDriverPriority(const fl::string& name, int priority) {
277 if (name.empty()) {
278 FL_ERROR("ChannelManager::setDriverPriority() - Empty driver name provided");
279 return false;
280 }
281
282 // Find driver and update priority
283 bool found = false;
284 for (auto& entry : mDrivers) {
285 if (entry.name == name) {
286 entry.priority = priority;
287 found = true;
288 FL_DBG("ChannelManager: Driver '" << name << "' priority changed to " << priority);
289 break;
290 }
291 }
292
293 if (!found) {
294 FL_ERROR("ChannelManager::setDriverPriority() - Driver '" << name << "' not found in registry");
295 return false;
296 }
297
298 // Re-sort drivers by priority (descending: higher values first).
299 // 1-4 drivers expected here too — sort_small avoids the quicksort body.
300 fl::sort_small(mDrivers.begin(), mDrivers.end());
301
302 FL_DBG("ChannelManager: Engine list re-sorted after priority change");
303 return true;
304}
305
306bool ChannelManager::isDriverEnabled(const char* name) const {
307 if (!name) {
308 FL_ERROR("ChannelManager::isDriverEnabled() - Null driver name provided");
309 return false;
310 }
311
312 for (const auto& entry : mDrivers) {
313 if (entry.name == name) {
314 return entry.enabled;
315 }
316 }
317
318 FL_ERROR("ChannelManager::isDriverEnabled() - Driver '" << name << "' not found in registry");
319 return false;
320}
321
323 if (name.empty()) {
325 }
326 for (const auto& entry : mDrivers) {
327 if (entry.name == name) {
328 return entry.enabled ? DriverStatus::STATUS_ENABLED
330 }
331 }
333}
334
336 return mDrivers.size();
337}
338
340 // Update cache with current driver state
341 mCachedDriverInfo.clear();
342 mCachedDriverInfo.reserve(mDrivers.size());
343
344 for (const auto& entry : mDrivers) {
345 // fl::string copy is cheap (shared pointer internally, no heap allocation)
346 mCachedDriverInfo.push_back({
347 entry.name,
348 entry.priority,
349 entry.enabled
350 });
351 }
352
353 return mCachedDriverInfo;
354}
355
357 if (name.empty()) {
359 }
360 for (const auto& entry : mDrivers) {
361 if (entry.enabled && entry.name == name) {
362 return entry.driver;
363 }
364 }
366}
367
369 if (name.empty()) {
370 FL_ERROR("ChannelManager::getDriverByName() - Empty driver name provided");
372 }
373 auto driver = findDriverByName(name);
374 if (!driver) {
375 FL_ERROR("ChannelManager::getDriverByName() - Driver '" << name.c_str() << "' not found or not enabled");
376 }
377 return driver;
378}
379
381 if (!data) {
382 FL_ERROR("ChannelManager::selectDriverForChannel() - Null channel data");
384 }
385
386 // If affinity is specified, look up by name. Misses fall through to
387 // priority dispatch below — per-frame logging is intentionally silent
388 // here because `Channel::showPixels` now emits a one-shot, actionable
389 // FL_ERROR with the enableDrivers<...>() / enableAllDrivers() hint
390 // (#2455). Use `findDriverByName` (silent) rather than `getDriverByName`
391 // (logs on miss) so the silent fall-through actually IS silent.
392 do {
393 if (affinity.empty()) {
394 break;
395 }
396 auto driver = findDriverByName(affinity);
397 if (!driver) {
398 break; // diagnostic emitted at the channel layer
399 }
400 if (!driver->canHandle(data)) {
401 FL_WARN_ONCE("ChannelManager: Affinity driver '" << affinity
402 << "' cannot handle channel data (chipset/bus mismatch). "
403 << "Falling back to AUTO/priority dispatch.");
404 break;
405 }
406 return driver;
407 } while (false);
408
409
410 // No affinity: iterate drivers by priority (already sorted descending)
411 for (const auto& entry : mDrivers) {
412 if (!entry.enabled) continue;
413 if (entry.driver->canHandle(data)) {
414 return entry.driver; // Return shared_ptr
415 }
416 }
417
418 FL_ERROR("ChannelManager: No compatible driver found for channel data");
420}
421
422
423template<typename Condition>
424bool ChannelManager::waitForCondition(Condition condition, u32 timeoutMs) {
425 const u32 startTime = timeoutMs > 0 ? millis() : 0;
426
427 // Tier 1: instant non-blocking check (avoid micros() / millis() cost on
428 // the common already-ready path).
429 if (condition()) {
430 return true;
431 }
432
433 // Tier 2: bounded microsecond spin (#2818). Catches short DMA tails
434 // (APA102 small strips, WS2812B <=8 LEDs) without paying the >=1-tick
435 // floor of the cooperator yield below. Budget is runtime-tunable via
436 // FastLED.setWaitSpinBudgetUs(N); set to 0 to disable.
437 {
438 const u32 spinBudget = fl::detail::getWaitSpinBudgetUs();
439 if (spinBudget > 0) {
440 const u32 spinStart = fl::micros();
441 while ((fl::micros() - spinStart) < spinBudget) {
442 if (condition()) {
443 return true;
444 }
445 if (timeoutMs > 0 && (millis() - startTime) >= timeoutMs) {
446 FL_ERROR("ChannelManager: Timeout occurred while waiting for condition");
447 return false;
448 }
449 }
450 }
451 }
452
453 while (!condition()) {
454 // Check timeout if specified
455 if (timeoutMs > 0 && (millis() - startTime >= timeoutMs)) {
456 FL_ERROR("ChannelManager: Timeout occurred while waiting for condition");
457 return false; // Timeout occurred
458 }
459
460 const u32 sliceMs = pollNeededWaitSliceMs(startTime, timeoutMs);
461 if (sliceMs == 0) {
462 return false;
463 }
464 if (waitForPollNeededSignal(sliceMs)) {
465 continue;
466 }
467
468 // Adaptive yield (refs #2815, generalizes the #2493 ESP32-P4 carve-out):
469 //
470 // The 1-tick (>=1 ms at CONFIG_FREERTOS_HZ=1000) floor only exists to
471 // keep WiFi / lwIP / BT controller tasks alive while we are inside the
472 // channel wait loop (#2254). When no radio is actually up, that floor
473 // is pure timing drift -- visible as the regression reported in #2420
474 // and as the per-frame cost the #2493 ESP32-P4 carve-out was avoiding.
475 //
476 // NetworkDetector::isAnyNetworkActive() is the runtime version of the
477 // "is a radio up?" question. On non-ESP32 platforms and on ESP32-P4
478 // (no radio silicon) it folds to a constant `false`, so this is
479 // strictly a perf win for the common single-strip / no-WiFi case
480 // without losing the WiFi-friendly behavior when a radio is active.
482 // Radio active: keep WiFi/lwIP/BT alive with the deep yield.
484 } else {
485 // No radio: fast yield, no FreeRTOS tick floor.
487 }
488 }
489
490 return true; // Condition met
491}
492
494 // Poll all registered drivers and return aggregate state
495 // Priority order: ERROR > BUSY > DRAINING > READY
496 bool anyBusy = false;
497 bool anyDraining = false;
498 fl::string firstError;
499
500 for (auto& entry : mDrivers) {
501 IChannelDriver::DriverState result = entry.driver->poll();
503 anyBusy = true;
504 } else if (result.state == IChannelDriver::DriverState::DRAINING) {
505 anyDraining = true;
506 }
507 // Capture first error encountered
508 if (result.state == IChannelDriver::DriverState::ERROR && firstError.empty()) {
509 firstError = result.error;
510 }
511 }
512
513 // Return error if any driver reported error
514 if (!firstError.empty()) {
516 }
517
518 if (anyBusy) {
520 }
521 if (anyDraining) {
523 }
525}
526
527bool ChannelManager::waitForReady(u32 timeoutMs) {
528 bool ok = waitForCondition([this]() {
530 }, timeoutMs);
531 if (!ok) {
532 FL_ERROR("ChannelManager: Timeout occurred while waiting for READY state");
533 }
534 return ok;
535}
536
538 bool ok = waitForCondition([this]() {
539 auto state = poll();
540 bool draining_or_done = (
543 );
544 return draining_or_done;
545 }, timeoutMs);
546 if (!ok) {
547 FL_ERROR("ChannelManager: Timeout occurred while waiting for READY or DRAINING state");
548 }
549 return ok;
550}
551
552
554 waitForReady(); // Wait for all drivers to become READY before clearing previous frame state.
555}
556
558 // Call show() on all drivers to trigger transmission
559 // Channels have enqueued data directly to drivers during showPixels()
560 // Now we trigger transmission by calling show() on each driver
561 for (auto& entry : mDrivers) {
562 if (entry.enabled) {
563 entry.driver->show();
564 }
565 }
567}
568
570 // Allow all channel drivers to clean up
571 waitForReady();
572 FL_DBG("ChannelManager: reset() - all drivers ready");
573}
574
578
579} // namespace fl
TestState state
FastLED chrono implementation - duration types for time measurements.
bool isDriverEnabled(const char *name) const FL_NOEXCEPT
Check if a driver is enabled by name.
void setDriverEnabled(const char *name, bool enabled) FL_NOEXCEPT
Enable or disable a driver by name at runtime.
bool removeDriver(fl::shared_ptr< IChannelDriver > driver) FL_NOEXCEPT
Remove a driver from the manager.
fl::shared_ptr< IChannelDriver > selectDriverForChannel(const ChannelDataPtr &data, const fl::string &affinity) FL_NOEXCEPT
Select best driver for channel data (used by Channel::showPixels)
void setExclusiveDriver() FL_NOEXCEPT
Register a single driver at a priority above the platform default and disable all others (compile-tim...
Definition manager.h:322
bool waitForPollNeededSignal(u32 timeoutMs) FL_NOEXCEPT
~ChannelManager() FL_NOEXCEPT override
Destructor - cleanup shared drivers (automatic via shared_ptr)
void reset() FL_NOEXCEPT
Reset bus manager state, clearing all enqueued and transmitting channels.
fl::shared_ptr< IChannelDriver > getDriverByName(const fl::string &name) const FL_NOEXCEPT
Get driver by name for affinity binding.
u32 pollNeededWaitSliceMs(u32 startTime, u32 timeoutMs) const FL_NOEXCEPT
IChannelDriver::PollNeededCallback mPollNeededCallback
Shared callback installed on drivers that can signal poll-needed events.
Definition manager.h:299
static void notifyPollNeededThunk(void *context) FL_NOEXCEPT
fl::shared_ptr< IChannelDriver > findDriverByName(const fl::string &name) const FL_NOEXCEPT
Silent counterpart to getDriverByName().
bool setDriverPriority(const fl::string &name, int priority) FL_NOEXCEPT
Change the priority of a registered driver.
fl::span< const DriverInfo > getDriverInfos() const FL_NOEXCEPT
Get full state of all registered drivers.
fl::size getDriverCount() const FL_NOEXCEPT
Get count of registered drivers (including unnamed ones)
void addDriver(int priority, fl::shared_ptr< IChannelDriver > driver) FL_NOEXCEPT
Add a driver with priority (higher priority = preferred)
bool waitForReady(u32 timeoutMs=1000) FL_NOEXCEPT
Wait for all drivers to become READY.
platforms::ChannelPollSignal mPollNeededSignal
Platform wait primitive owned by the manager.
Definition manager.h:302
void clearAllDrivers() FL_NOEXCEPT
Remove all drivers from the manager.
IChannelDriver::DriverState poll() FL_NOEXCEPT
Poll all registered drivers and return aggregate state.
fl::vector< DriverInfo > mCachedDriverInfo
Cached driver info for getDriverInfos() to avoid allocations.
Definition manager.h:291
DriverStatus
Registration status of a driver by name (silent lookup)
Definition manager.h:170
@ NOT_REGISTERED
No driver with that name is registered.
Definition manager.h:171
@ STATUS_ENABLED
Driver is registered and enabled.
Definition manager.h:173
@ STATUS_DISABLED
Driver is registered but disabled.
Definition manager.h:172
void onEndFrame() FL_NOEXCEPT override
Trigger transmission of batched channel data.
bool waitForReadyOrDraining(u32 timeoutMs=1000) FL_NOEXCEPT
fl::vector< EngineEntry > mDrivers
Shared drivers sorted by priority descending (higher values first)
Definition manager.h:287
bool waitForCondition(Condition condition, u32 timeoutMs=1000) FL_NOEXCEPT
Wait until a condition is met, with check-pump-delay logic.
fl::string mExclusiveDriver
Exclusive driver name (empty if no exclusive mode)
Definition manager.h:296
void onBeginFrame() FL_NOEXCEPT override
Poll drivers before frame starts to clear previous frame state.
ChannelManager() FL_NOEXCEPT
Constructor.
bool setExclusiveDriverByName(const char *name) FL_NOEXCEPT
Enable only one driver exclusively (disables all others) — by-name escape hatch.
void notifyPollNeeded() FL_NOEXCEPT
static ChannelManager & instance() FL_NOEXCEPT
Get the global singleton instance.
DriverStatus driverStatus(const fl::string &name) const FL_NOEXCEPT
Look up a driver's registration status without logging on miss.
Unified channel manager with priority-based driver selection.
Definition manager.h:50
static void removeListener(Listener *listener) FL_NOEXCEPT
static void addListener(Listener *listener, int priority=0) FL_NOEXCEPT
static T & instance() FL_NOEXCEPT
Definition singleton.h:41
bool empty() const FL_NOEXCEPT
const char * c_str() const FL_NOEXCEPT
static bool isAnyNetworkActive() FL_NOEXCEPT
T * get() const FL_NOEXCEPT
Definition shared_ptr.h:334
Task executor — runs registered task runners and manages the run loop.
#define FL_WARN(X)
Definition log.h:276
#define FL_ERROR(X)
Definition log.h:219
#define FL_DBG
Definition log.h:388
#define FL_WARN_ONCE(X)
Definition log.h:278
Centralized logging categories for FastLED hardware interfaces and subsystems.
Unified manager for channel drivers with priority-based fallback.
fl::u32 getWaitSpinBudgetUs() FL_NOEXCEPT
Get the current tiered-wait spin budget (microseconds).
void run(fl::u32 microseconds, ExecFlags flags)
Run selected task subsystems.
fl::u32 millis()
Universal millisecond timer - returns milliseconds since system startup.
const char * busName(Bus b) FL_NOEXCEPT
Canonical driver-name string for a Bus value.
Definition bus.h:167
Bus
Driver identifier for compile-time bus selection.
Definition bus.h:60
ChannelManager & channelManager()
Get the global ChannelManager singleton instance.
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
fl::u32 micros()
Universal microsecond timer - returns microseconds since system startup.
void sort_small(Iterator first, Iterator last, Compare comp) FL_NOEXCEPT
Definition algorithm.h:612
Base definition for an LED controller.
Definition crgb.hpp:179
Cross-platform facade for runtime network activity detection.
#define FL_NOEXCEPT
bool supportsClockless
Supports clockless protocols (WS2812, SK6812, etc.)
Definition driver.h:152
bool supportsSpi
Supports SPI protocols (APA102, SK9822, etc.)
Definition driver.h:153
Driver capabilities.
Definition driver.h:151
Value state
Current driver state.
Definition driver.h:173
@ READY
Hardware idle; ready to accept new transmissions.
Definition driver.h:167
@ DRAINING
All channels submitted; still transmitting.
Definition driver.h:169
@ ERROR
Driver encountered an error.
Definition driver.h:170
@ BUSY
Active: channels transmitting or queued.
Definition driver.h:168
Driver state with optional error message.
Definition driver.h:165
ISR-safe callback handle invoked when the manager should poll again.
Definition driver.h:58
Runtime-tunable microsecond spin budget for the channel-manager and driver wait loops (Phase 1 of #28...