Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions install.m
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,27 @@
first_run(root);
end

% --- Always: ensure the Concurrency lockfile_mex is compiled ---
% lockfile_mex is the only MEX NOT committed to the repo, so the
% needs_build() gate above (which probes the committed FastSense
% binary_search_mex) returns false on a clean checkout that ships
% prebuilt FastSense binaries — and first_run()/build_mex() never
% runs build_concurrency_mex(). Build it here, independent of that
% gate, so every consumer (tests, FileLock, the concurrency CI
% smoke) gets it. Best-effort: honour FASTSENSE_SKIP_BUILD (CI with
% cached/prebuilt binaries) and never block install() on a missing
% C compiler (FileLock falls back to pure-MATLAB sidecar mode).
if isempty(getenv('FASTSENSE_SKIP_BUILD')) ...
&& exist('lockfile_mex', 'file') ~= 3 ...
&& exist('build_concurrency_mex', 'file') == 2
try
build_concurrency_mex();
catch concErr
warning('install:concurrencyMexFailed', ...
'lockfile_mex compile skipped (non-fatal): %s', concErr.message);
end
end

% --- Once per session: JIT warmup ---
jit_warmup();
end
Expand Down
35 changes: 23 additions & 12 deletions libs/FastSense/FastSense.m
Original file line number Diff line number Diff line change
Expand Up @@ -1144,20 +1144,31 @@ function render(obj, progressBar)
S = obj.Shadings(i);
if numel(S.X) > shadingCacheSize * 2
% Build cache from raw, then render from cache
[cx, cy1] = minmax_downsample(S.X, S.Y1, shadingCacheSize);
[~, cy2] = minmax_downsample(S.X, S.Y2, shadingCacheSize);
[cx, cy1] = minmax_downsample(S.X, S.Y1, shadingCacheSize);
[~, cy2] = minmax_downsample(S.X, S.Y2, shadingCacheSize);
% minmax_downsample's tail-anchor makes the output length
% data-dependent, so Y1 and Y2 may differ in length. The
% cache is re-sliced by a single shared index range in
% updateShadings, so trim all three to a common length to
% keep that slice in bounds.
Lc = min([numel(cx), numel(cy1), numel(cy2)]);
cx = cx(1:Lc);
cy1 = cy1(1:Lc);
cy2 = cy2(1:Lc);
obj.Shadings(i).CacheX = cx;
obj.Shadings(i).CacheY1 = cy1;
obj.Shadings(i).CacheY2 = cy2;
% Downsample cache to screen resolution
[xd, y1d] = minmax_downsample(cx, cy1, obj.PixelWidth);
[~, y2d] = minmax_downsample(cx, cy2, obj.PixelWidth);
patchX = [xd, fliplr(xd)];
% Downsample cache to screen resolution. Each boundary
% keeps its own X (xd / xd2) so the closed patch polygon
% always has matching vertex counts.
[xd, y1d] = minmax_downsample(cx, cy1, obj.PixelWidth);
[xd2, y2d] = minmax_downsample(cx, cy2, obj.PixelWidth);
patchX = [xd, fliplr(xd2)];
patchY = [y1d, fliplr(y2d)];
elseif numel(S.X) > obj.MinPointsForDownsample
[xd, y1d] = minmax_downsample(S.X, S.Y1, obj.PixelWidth);
[~, y2d] = minmax_downsample(S.X, S.Y2, obj.PixelWidth);
patchX = [xd, fliplr(xd)];
[xd, y1d] = minmax_downsample(S.X, S.Y1, obj.PixelWidth);
[xd2, y2d] = minmax_downsample(S.X, S.Y2, obj.PixelWidth);
patchX = [xd, fliplr(xd2)];
patchY = [y1d, fliplr(y2d)];
else
patchX = [S.X, fliplr(S.X)];
Expand Down Expand Up @@ -4044,9 +4055,9 @@ function updateShadings(obj)
y2Vis = srcY2(idxStart:idxEnd);

if nVis > obj.MinPointsForDownsample
[xd, y1d] = minmax_downsample(xVis, y1Vis, pw);
[~, y2d] = minmax_downsample(xVis, y2Vis, pw);
patchX = [xd, fliplr(xd)];
[xd, y1d] = minmax_downsample(xVis, y1Vis, pw);
[xd2, y2d] = minmax_downsample(xVis, y2Vis, pw);
patchX = [xd, fliplr(xd2)];
patchY = [y1d, fliplr(y2d)];
else
patchX = [xVis, fliplr(xVis)];
Expand Down
50 changes: 50 additions & 0 deletions tests/suite/TestAddShaded.m
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,55 @@ function testAddFillRendered(testCase)
ud = get(fp.Shadings(1).hPatch, 'UserData');
testCase.verifyEqual(ud.FastSense.Type, 'shaded', 'testAddFillRendered: type is shaded');
end

function testShadedPatchEqualLengthCacheBranch(testCase)
% Regression (260512-c5x): with > 2*shadingCacheSize (20000) points
% and a constant fill baseline, the upper (varying) and lower
% (constant) boundaries downsample to DIFFERENT lengths via the
% minmax tail-anchor. render()'s cache branch must still emit an
% equal-length patch polygon — previously patch() threw "Vectors
% must be the same length." (e.g. example_dock's Power Systems
% tile). The subsequent zoom exercises the same fix in
% updateShadings(). A monotonic upper curve guarantees divergence:
% its last sample IS its last bucket extreme (no tail-anchor) while
% the constant baseline DOES anchor.
n = 25000;
x = linspace(0, 100, n);
y = linspace(0, 10, n);
fp = FastSense();
fp.addLine(x, y, 'DisplayName', 'ramp');
fp.addFill(x, y, 'Baseline', 0, 'FaceColor', [0 0.5 1]);
fp.render();
testCase.addTeardown(@close, fp.hFigure);

hP = fp.Shadings(1).hPatch;
testCase.verifyEqual(numel(get(hP, 'XData')), numel(get(hP, 'YData')), ...
'cache branch: shading patch XData/YData length mismatch after render');

% Zoom to a sub-range that stays above MinPointsForDownsample so
% updateShadings re-downsamples (the live zoom/pan path).
set(fp.hAxes, 'XLim', [20 60]);
drawnow; pause(0.2);
testCase.verifyEqual(numel(get(hP, 'XData')), numel(get(hP, 'YData')), ...
'updateShadings: shading patch XData/YData length mismatch after zoom');
end

function testShadedPatchEqualLengthMidBranch(testCase)
% MinPointsForDownsample (5000) < n <= 2*shadingCacheSize (20000)
% exercises render()'s mid-size (elseif) downsample branch with the
% same varying-vs-constant boundary divergence.
n = 10000;
x = linspace(0, 100, n);
y = linspace(0, 5, n);
fp = FastSense();
fp.addLine(x, y);
fp.addFill(x, y, 'Baseline', 0);
fp.render();
testCase.addTeardown(@close, fp.hFigure);

hP = fp.Shadings(1).hPatch;
testCase.verifyEqual(numel(get(hP, 'XData')), numel(get(hP, 'YData')), ...
'mid branch: shading patch XData/YData length mismatch after render');
end
end
end
7 changes: 7 additions & 0 deletions tests/suite/TestMexParity.m
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,13 @@ function testBuildStoreNaNHandling(testCase)
yOut(odd(~minFirst)) = yMaxVals(~minFirst);
xOut(even(~minFirst)) = xMinVals(~minFirst);
yOut(even(~minFirst)) = yMinVals(~minFirst);
% Tail-anchor (260512-c5x): mirror minmax_core_mex.c /
% minmax_downsample.m. Append (segX(end), segY(end)) iff its X
% strictly exceeds the last emitted X. Length: 2*nb or 2*nb+1.
if segX(end) > xOut(end)
xOut(end + 1) = segX(end);
yOut(end + 1) = segY(end);
end
end

function [xOut, yOut] = lttb_core_matlab(x, y, numOut)
Expand Down
7 changes: 7 additions & 0 deletions tests/test_mex_parity.m
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ function test_mex_parity()
yOut(odd(~minFirst)) = yMaxVals(~minFirst);
xOut(even(~minFirst)) = xMinVals(~minFirst);
yOut(even(~minFirst)) = yMinVals(~minFirst);
% Tail-anchor (260512-c5x): mirror minmax_core_mex.c / minmax_downsample.m.
% Append (segX(end), segY(end)) iff its X strictly exceeds the last
% emitted X. Output length: 2*nb or 2*nb+1.
if segX(end) > xOut(end)
xOut(end + 1) = segX(end);
yOut(end + 1) = segY(end);
end
end

function [xOut, yOut] = lttb_core_matlab(x, y, numOut)
Expand Down
18 changes: 9 additions & 9 deletions wiki/Companion-Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The FastSense Companion is a three-pane `uifigure` control panel that browses the project's `TagRegistry`, opens dashboards and ad-hoc plots in their own MATLAB figures, and provides live status monitoring across the entire project. It is purely a navigator — every dashboard it opens runs in a standalone classical figure with its own live timer, theme, and toolbar.

Two parallel help systems live inside FastSense: **System 1** is the per-dashboard `Info` button driven by `DashboardEngine.InfoFile`, and **System 2** is this Wiki Browser. See [Dashboard Info vs Wiki](Dashboard-Info-vs-Wiki) for the full distinction.
Two parallel help systems live inside FastSense: **System 1** is the per-dashboard `Info` button driven by `DashboardEngine.InfoFile`, and **System 2** is this Wiki Browser. See [Dashboard Info vs Wiki](Dashboard-Info-vs-Wiki.md) for the full distinction.

## Three-pane layout

Expand All @@ -16,9 +16,9 @@ The right pane is *adaptive*: when one tag is selected it shows metadata, thresh

## Top toolbar (left to right)

- **Events** — opens the [Event Viewer](Event-Viewer)
- **Events** — opens the [Event Viewer](Event-Viewer.md)
- **Live: ON/OFF** — toggles the companion-driven inspector refresh and the live log
- **Tags** — opens the [Tag Status Table](Tag-Status-Table)
- **Tags** — opens the [Tag Status Table](Tag-Status-Table.md)
- **Tile / Close all** — manages the windows the Companion has opened (dashboards, ad-hoc plots, detached panes)
- **Wiki** — opens this Wiki Browser (you are reading it now)
- **Gear** — opens Companion settings (theme, live period)
Expand All @@ -30,7 +30,7 @@ The bottom of the window hosts two compact log panes:
- **Events log** — rolling list of recent threshold violations from `EventStore`
- **Live log** — per-tag `Δ samples` and latest value as new data lands

Each pane has a pop-out icon in its header that detaches the pane into its own figure window. See [Event Viewer](Event-Viewer) for the events pane and [Live Log](Live-Log) for the live updates pane.
Each pane has a pop-out icon in its header that detaches the pane into its own figure window. See [Event Viewer](Event-Viewer.md) for the events pane and [Live Log](Live-Log.md) for the live updates pane.

## Opening a dashboard

Expand All @@ -44,8 +44,8 @@ The **Live: ON/OFF** toggle controls a Companion-owned `timer` that drives the i

## See also

- [Tag Status Table](Tag-Status-Table)
- [Event Viewer](Event-Viewer)
- [Live Log](Live-Log)
- [Dashboard Info vs Wiki](Dashboard-Info-vs-Wiki)
- [Home](Home)
- [Tag Status Table](Tag-Status-Table.md)
- [Event Viewer](Event-Viewer.md)
- [Live Log](Live-Log.md)
- [Dashboard Info vs Wiki](Dashboard-Info-vs-Wiki.md)
- [Home](Home.md)
4 changes: 2 additions & 2 deletions wiki/Dashboard-Info-vs-Wiki.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@ The two systems never collide on the same button. They can both exist on the sam

## See also

- [Companion Overview](Companion-Overview)
- [Home](Home)
- [Companion Overview](Companion-Overview.md)
- [Home](Home.md)
8 changes: 4 additions & 4 deletions wiki/Event-Viewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ The Companion's bottom strip hosts two compact log panes:
- **Events log** — rolling list of recent detected events (this surface's compact form)
- **Live log** — per-tag sample-delta counts as new data arrives (a different surface)

See [Live Log](Live-Log) for the live updates pane.
See [Live Log](Live-Log.md) for the live updates pane.

## See also

- [Live Log](Live-Log)
- [Tag Status Table](Tag-Status-Table)
- [Companion Overview](Companion-Overview)
- [Live Log](Live-Log.md)
- [Tag Status Table](Tag-Status-Table.md)
- [Companion Overview](Companion-Overview.md)
10 changes: 5 additions & 5 deletions wiki/Live-Log.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Buffer is capped at 500 rows, newest first. When the cap is reached the oldest r

## Tracking source

The Live Log does **not** track per-tag sample cursors itself — `FastSenseCompanion.scanLiveTagUpdates_` owns the `LiveSampleCount_` map and calls `addLiveLogEntry(tagKey, delta, latestY)` whenever a positive delta is detected. This boundary is fixed by Phase 1027 CONTEXT and is the same separation the [Event Viewer](Event-Viewer)'s events log uses — pipeline state lives in the Companion, panes only render rows.
The Live Log does **not** track per-tag sample cursors itself — `FastSenseCompanion.scanLiveTagUpdates_` owns the `LiveSampleCount_` map and calls `addLiveLogEntry(tagKey, delta, latestY)` whenever a positive delta is detected. This boundary is fixed by Phase 1027 CONTEXT and is the same separation the [Event Viewer](Event-Viewer.md)'s events log uses — pipeline state lives in the Companion, panes only render rows.

## Filter

Expand All @@ -29,14 +29,14 @@ The **Clear** button next to the filter wipes the buffer entirely.

Only while the Companion is in **Live mode** (top toolbar's "Live: ON"). When Live is OFF the live pipeline is idle and no new rows arrive. Existing rows stay visible.

The [Tag Status Table](Tag-Status-Table) is the exception — it polls under its own window-owned timer and stays current even when Live is OFF.
The [Tag Status Table](Tag-Status-Table.md) is the exception — it polls under its own window-owned timer and stays current even when Live is OFF.

## Detached vs inline

When detached, the pane re-parents itself into a standalone `uifigure` and keeps its full buffer history. Closing the detached figure re-attaches the pane inline. The buffer is preserved across the round-trip.

## See also

- [Event Viewer](Event-Viewer)
- [Tag Status Table](Tag-Status-Table)
- [Companion Overview](Companion-Overview)
- [Event Viewer](Event-Viewer.md)
- [Tag Status Table](Tag-Status-Table.md)
- [Companion Overview](Companion-Overview.md)
6 changes: 3 additions & 3 deletions wiki/Tag-Status-Table.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ Click again to **Resume polling**. Useful when you want a stable snapshot of the

## See also

- [Companion Overview](Companion-Overview)
- [Live Log](Live-Log)
- [Event Viewer](Event-Viewer)
- [Companion Overview](Companion-Overview.md)
- [Live Log](Live-Log.md)
- [Event Viewer](Event-Viewer.md)
Loading