Skip to content

Commit d7e22d3

Browse files
committed
Updates
1 parent 899eb4e commit d7e22d3

File tree

5 files changed

+161
-11
lines changed

5 files changed

+161
-11
lines changed

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,25 +185,24 @@ $pi->input1()->monitor(new DigitalMonitor, function($newValue) {
185185

186186
#### Monitor::debounce
187187

188-
Wraps a callback to fire **only after changes stop** for the given interval (in milliseconds).
188+
Configures the monitor to fire **only after changes stop** for the given interval (in milliseconds).
189189

190190
```php
191-
$pi->input1()->monitor(new DigitalMonitor, Monitor::debounce(function($value) {
191+
$pi->input1()->monitor((new DigitalMonitor)->debounce(200), function($value) {
192192
// Handle change after quiet period
193-
}, 200));
193+
});
194194
```
195195

196196
#### Monitor::throttle
197197

198-
Wraps a callback to fire **at most once per interval** (in milliseconds).
198+
Configures the monitor to fire **at most once per interval** (in milliseconds).
199199

200200
```php
201-
$pi->input1()->monitor(new DigitalMonitor, Monitor::throttle(function($value) {
201+
$pi->input1()->monitor((new DigitalMonitor)->throttle(200), function($value) {
202202
// Handle change no more than every 200ms
203-
}, 200));
203+
});
204204
```
205205

206-
207206
### Process Image (Low-Level)
208207

209208
Get the raw process image interface for advanced access:

src/Modules/Module.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,13 @@ public function cancel(): void
6262

6363
public function monitor(string $variable, Monitor $monitor, callable $callback): void
6464
{
65-
Event::listen(PollingEvent::class, function () use ($callback, $variable, $monitor) {
65+
$wrapped = $monitor->wrap($callback);
66+
67+
Event::listen(PollingEvent::class, function () use ($wrapped, $variable, $monitor) {
6668
$next = $this->getProcessImage()->readVariable($variable);
6769

6870
if ($monitor->evaluate($next)) {
69-
$callback($next);
71+
$wrapped($next);
7072
}
7173
});
7274
}

src/Monitors/Monitor.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,40 @@
88

99
abstract class Monitor
1010
{
11+
protected ?int $rateLimit = null;
12+
13+
protected RateLimit $rateLimitType = RateLimit::None;
14+
15+
public function debounce(int $milliseconds): static
16+
{
17+
$this->rateLimit = $milliseconds;
18+
$this->rateLimitType = RateLimit::Debounce;
19+
20+
return $this;
21+
}
22+
23+
public function throttle(int $milliseconds): static
24+
{
25+
$this->rateLimit = $milliseconds;
26+
$this->rateLimitType = RateLimit::Throttle;
27+
28+
return $this;
29+
}
30+
31+
public function wrap(callable $callback): callable
32+
{
33+
assert($this->rateLimitType === RateLimit::None || is_int($this->rateLimit));
34+
35+
return match ($this->rateLimitType) {
36+
RateLimit::None => $callback,
37+
RateLimit::Debounce => $this->_debouncer($callback, (int) $this->rateLimit),
38+
RateLimit::Throttle => $this->_throttler($callback, (int) $this->rateLimit),
39+
};
40+
}
41+
1142
abstract public function evaluate(int|bool $next): bool;
1243

13-
public static function debounce(callable $callback, int $milliseconds): callable
44+
protected function _debouncer(callable $callback, int $milliseconds): callable
1445
{
1546
$timerId = null;
1647

@@ -26,7 +57,7 @@ public static function debounce(callable $callback, int $milliseconds): callable
2657
};
2758
}
2859

29-
public static function throttle(callable $callback, int $milliseconds): callable
60+
protected function _throttler(callable $callback, int $milliseconds): callable
3061
{
3162
$throttling = false;
3263

src/Monitors/RateLimit.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flat3\RevPi\Monitors;
6+
7+
enum RateLimit
8+
{
9+
case None;
10+
case Debounce;
11+
case Throttle;
12+
}

tests/Monitors/MonitorTest.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
namespace Flat3\RevPi\Tests\Monitors;
4+
5+
use Flat3\RevPi\Monitors\DigitalMonitor;
6+
use Flat3\RevPi\Tests\TestCase;
7+
use Flat3\RevPi\Tests\UsesVirtualEnvironment;
8+
9+
class MonitorTest extends TestCase implements UsesVirtualEnvironment
10+
{
11+
/**
12+
* Helper to advance the event loop n times (simulate time passing).
13+
*/
14+
public function test_no_throttle_debounce(): void
15+
{
16+
$monitor = new DigitalMonitor;
17+
$results = [];
18+
19+
$callback = function ($val) use (&$results) {
20+
$results[] = $val;
21+
};
22+
23+
$wrapped = $monitor->wrap($callback);
24+
25+
// Simulate variable changes
26+
$inputs = [0, 0, 1, 1, 0, 1, 1, 0];
27+
foreach ($inputs as $i => $value) {
28+
if ($monitor->evaluate($value)) {
29+
$wrapped($value);
30+
}
31+
$this->loop(1, 2);
32+
}
33+
34+
// Only trigger callback on CHANGE (not same value twice),
35+
// DigitalMonitor returns true for a transition.
36+
self::assertSame([1, 0, 1, 0], $results);
37+
}
38+
39+
public function test_fast_debounce(): void
40+
{
41+
$monitor = (new DigitalMonitor)->debounce(1); // 1ms debounce
42+
$results = [];
43+
$callback = function ($val) use (&$results) {
44+
$results[] = $val;
45+
};
46+
$wrapped = $monitor->wrap($callback);
47+
48+
$inputs = [0, 1, 0, 1, 0];
49+
foreach ($inputs as $value) {
50+
if ($monitor->evaluate($value)) {
51+
$wrapped($value);
52+
}
53+
$this->loop(1, 2);
54+
}
55+
56+
// Advance loop to fire debounce once
57+
$this->loop(2, 2);
58+
59+
self::assertSame([0], $results);
60+
61+
// Debounce with enough time between events so each triggers
62+
$monitor = (new DigitalMonitor)->debounce(1);
63+
$results = [];
64+
$wrapped = $monitor->wrap(function ($v) use (&$results) {
65+
$results[] = $v;
66+
});
67+
68+
$transitions = [0, 1, 0, 1, 0];
69+
foreach ($transitions as $value) {
70+
if ($monitor->evaluate($value)) {
71+
$wrapped($value);
72+
}
73+
$this->loop(2, 2); // Let debounce interval expire
74+
}
75+
self::assertSame([1, 0, 1, 0], $results);
76+
}
77+
78+
public function test_fast_throttle(): void
79+
{
80+
$monitor = (new DigitalMonitor)->throttle(2); // 2ms throttle
81+
$results = [];
82+
$callback = function ($val) use (&$results) {
83+
$results[] = $val;
84+
};
85+
$wrapped = $monitor->wrap($callback);
86+
87+
$inputs = [0, 1, 0, 1, 0];
88+
89+
// Submit all transitions rapidly with 0ms gap, before throttle can expire
90+
foreach ($inputs as $value) {
91+
if ($monitor->evaluate($value)) {
92+
$wrapped($value);
93+
}
94+
}
95+
// Should only fire the first one!
96+
$this->loop(3, 2);
97+
98+
// Now let throttle window expire, and send another transition
99+
if ($monitor->evaluate(1)) {
100+
$wrapped(1);
101+
}
102+
$this->loop(3, 2);
103+
104+
self::assertSame([1, 1], $results);
105+
}
106+
}

0 commit comments

Comments
 (0)