Skip to content

Commit 6a6f6ed

Browse files
committed
fix: make footswitch brightness gate hardware-only, like blackout
The gate (g_gate_level) was previously baked into make_frame()'s bri computation, so it darkened both the WS preview and the real laser output together. Since the gate defaults closed until the pedal or brightness fader is touched, this meant the preview stayed black after a restart - e.g. right after recalling a cue - until the operator manually touched the brightness UI. Now make_frame() always renders at full s.intensity; the gate is applied only to the hardware-bound frame copy in laser_thread(), exactly the same way blackout already was. Preview always shows the true look immediately; only the real laser output can be gated dark by the footswitch or blacked out. Also updates README's /brightness/gate REST doc to reflect the hardware-only behavior.
1 parent 115a64e commit 6a6f6ed

2 files changed

Lines changed: 44 additions & 36 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ All parameter changes take effect on the next frame — nothing restarts.
169169
| `POST /move/speed/<v>` / `/move/size/<v>` | Movement cycle speed / amplitude |
170170
| `POST /laser/rainbow/amount/<v>` / `/speed/<v>` | Rainbow color blend / hue cycle speed |
171171
| `POST /blackout/<0\|1>` | Force *hardware* output dark (never affects the browser preview) |
172-
| `POST /brightness/gate/<0\|1>` | Footswitch-style master brightness gate (affects preview + hardware): 1 = open instantly (renders the current brightness fader value), 0 = closed (fades to dark over `flash_release_ms`, or instantly if it's 0) — defaults closed until set. Never touches the `intensity` field itself |
172+
| `POST /brightness/gate/<0\|1>` | Footswitch-style master brightness gate, *hardware-only* (never affects the browser preview - same treatment as blackout above): 1 = open instantly (renders the current brightness fader value), 0 = closed (fades to dark over `flash_release_ms`, or instantly if it's 0) — defaults closed until set. Never touches the `intensity` field itself |
173173
| `POST /flash/<0\|1>` | Momentary flash: 1 = press (forces color to white + full brightness, remembering the prior values), 0 = release (always restores them instantly - `flash_release_ms` does not apply here, only to the footswitch gate above) |
174174
| `POST /flash/release_ms/<v>` | Footswitch gate release fade time (0-2000ms), despite the name this only affects `/brightness/gate` closing, not flash's own release: `0` = instant cut, `>0` = fade brightness to 0 over this many ms instead of snapping to dark (default `200`) |
175175
| `POST /mirror/x/<0\|1>` | Flip the output horizontally (mirror around center); 1 = mirrored, 0 = normal |

‎backend/laser_daemon.cpp‎

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -271,18 +271,20 @@ static std::atomic<bool> g_flash_active{false};
271271

272272
// Master-brightness gate for a footswitch wired as a "hold to show light"
273273
// pedal (CC64, the standard MIDI sustain-pedal number): while held/open,
274-
// output (both the browser preview *and* hardware) renders normally; while
275-
// released/closed (including at startup, before the pedal has ever been
276-
// touched), output is forced dark - baked directly into make_frame()'s
277-
// `bri` computation so preview and hardware always show it together (per
278-
// explicit request, unlike `blackout` which stays hardware-only so the
279-
// preview keeps working while blacked out on the real beam). Deliberately
280-
// does NOT touch LaserState.intensity itself - the fader/knob/UI/REST
281-
// setting it is LTP (last value counts) and keeps updating independently
282-
// of the gate, so whatever the fader was last set to (even while the gate
283-
// was closed) takes effect immediately the next time the pedal is pressed.
284-
// LTP goes both ways: touching the intensity fader directly (MIDI CC,
285-
// REST /api/state, /laser/brightness) also opens the gate immediately, so
274+
// hardware output renders normally; while released/closed (including at
275+
// startup, before the pedal has ever been touched), *hardware* output is
276+
// forced dark. This is hardware-only, exactly like `blackout` below - the
277+
// browser preview always renders at full s.intensity regardless of the
278+
// gate (see laser_thread()'s hardware-send step, which applies g_gate_level
279+
// only to the copy sent to the DAC), so recalling a cue or just opening the
280+
// UI after a restart shows the correct preview immediately, without first
281+
// having to touch the pedal or the brightness fader. Deliberately does NOT
282+
// touch LaserState.intensity itself - the fader/knob/UI/REST setting it is
283+
// LTP (last value counts) and keeps updating independently of the gate, so
284+
// whatever the fader was last set to (even while the gate was closed)
285+
// takes effect immediately the next time the pedal is pressed. LTP goes
286+
// both ways: touching the intensity fader directly (MIDI CC, REST
287+
// /api/state, /laser/brightness) also opens the gate immediately, so
286288
// brightness shows right away even if the pedal was never pressed - the
287289
// footswitch only needs to be used at all if you actually want it to be
288290
// able to force things dark on release. Read from laser_thread without
@@ -295,13 +297,13 @@ static std::atomic<bool> g_brightness_gate_open{false};
295297
// original design - pressing the pedal shows light right away), but
296298
// closing it starts a decay from 1.0 to 0.0 over flash_release_ms instead
297299
// of an instant cut (0ms = instant, same as before this existed). Applied
298-
// as a multiplier on top of s.intensity in make_frame() - never touches
299-
// LaserState.intensity itself, so the fader's LTP value is untouched by
300-
// how quickly the gate happens to be closing. g_prev_gate_open detects the
301-
// open->closed edge (the atomic bool alone carries no timestamp). Not
302-
// atomic themselves, but always touched under G_mtx - both by
303-
// laser_thread's per-tick update and by do_flash_press() forcing the gate
304-
// open (see its comment).
300+
// as a multiplier only to the hardware-bound frame copy in laser_thread()
301+
// (never to the preview, and never to LaserState.intensity itself, so the
302+
// fader's LTP value is untouched by how quickly the gate happens to be
303+
// closing). g_prev_gate_open detects the open->closed edge (the atomic
304+
// bool alone carries no timestamp). Not atomic themselves, but always
305+
// touched under G_mtx - both by laser_thread's per-tick update and by
306+
// do_flash_press() forcing the gate open (see its comment).
305307
static bool g_prev_gate_open = false;
306308
static bool g_gate_decaying = false;
307309
static std::chrono::steady_clock::time_point g_gate_decay_start;
@@ -534,13 +536,11 @@ static core::Frame make_frame(const LaserState& s) {
534536
hsv_to_rgb(hue, 1.0f, 1.0f, fr, fg, fb);
535537
}
536538

537-
// Footswitch master-brightness gate: applied to the shared frame
538-
// (so the preview shows it too, per explicit request - unlike
539-
// `blackout`, which stays hardware-only) via g_gate_level, a 0..1
540-
// multiplier that's 1.0 while the gate is open and fades to 0.0
541-
// over flash_release_ms once it closes (see g_gate_level's
542-
// comment). Doesn't touch s.intensity itself, so the fader keeps
543-
// whatever it was last set to (LTP) regardless of the gate.
539+
// Master brightness. The footswitch gate (g_gate_level) is
540+
// deliberately *not* applied here - it's hardware-only, applied to
541+
// a separate copy of the frame in laser_thread()'s hardware-send
542+
// step (same treatment as `blackout`), so the WS preview always
543+
// shows the true, ungated brightness.
544544
float px = mirror_sign * (base[i].x + ox);
545545
float py = base[i].y + oy;
546546
// Screen-edge safety: whatever we send downstream, the DAC encoder
@@ -557,7 +557,7 @@ static core::Frame make_frame(const LaserState& s) {
557557
// axis, so the figure simply fades out before reaching the edge
558558
// rather than being disturbed/squashed against it.
559559
bool offScreen = std::fabs(px) > 1.0f || std::fabs(py) > 1.0f;
560-
float bri = (frame_blank || offScreen) ? 0.0f : s.intensity * g_gate_level;
560+
float bri = (frame_blank || offScreen) ? 0.0f : s.intensity;
561561

562562
if (dotted) {
563563
// Arrive blanked and hold for a distance-scaled number of
@@ -794,18 +794,26 @@ static void laser_thread() {
794794

795795
if (hardwareReady) {
796796
last_hw_send = now_tp;
797-
// Blackout only ever suppresses the *real* laser output, never
798-
// the browser preview (which already got the true-color `frame`
799-
// above) - so a blanked copy is what actually goes to hardware,
800-
// keeping the same point positions (safety-relevant for some
801-
// controllers) but zeroing every channel's color. The
802-
// footswitch brightness gate is *not* handled here - it's
803-
// baked into `frame` itself (see make_frame()'s `bri`
804-
// computation) so both preview and hardware see it together.
797+
// Blackout and the footswitch brightness gate only ever
798+
// suppress/dim the *real* laser output, never the browser
799+
// preview (which already got the true, full-brightness `frame`
800+
// above) - so a modified copy is what actually goes to
801+
// hardware, keeping the same point positions (safety-relevant
802+
// for some controllers) but scaling/zeroing every channel's
803+
// color. Blackout wins outright (full zero); otherwise the
804+
// gate's current fade level (1.0 = fully open, decaying to 0.0
805+
// while closed/closing - see g_gate_level's comment) is applied
806+
// as a multiplier.
805807
if (snap.blackout) {
806808
core::Frame blanked = frame;
807809
for (auto& p : blanked.points) { p.r = 0.0f; p.g = 0.0f; p.b = 0.0f; }
808810
ctrl->sendFrame(std::move(blanked));
811+
} else if (g_gate_level < 1.0f) {
812+
core::Frame gated = frame;
813+
for (auto& p : gated.points) {
814+
p.r *= g_gate_level; p.g *= g_gate_level; p.b *= g_gate_level;
815+
}
816+
ctrl->sendFrame(std::move(gated));
809817
} else {
810818
ctrl->sendFrame(std::move(frame));
811819
}

0 commit comments

Comments
 (0)