189 lines
7.4 KiB
TypeScript
189 lines
7.4 KiB
TypeScript
/**
|
|
* Oscilloscope trigger logic
|
|
*
|
|
* The trigger turns the otherwise free-running scope into something that
|
|
* actually behaves like a digital storage scope — windows pin around
|
|
* detected edges instead of scrolling away from sparse activity. These
|
|
* tests pin down the three modes (auto / normal / single), the three
|
|
* edge selectors (rising / falling / either), and the re-arm flow.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import {
|
|
useOscilloscopeStore,
|
|
matchesTriggerEdge,
|
|
} from '../store/useOscilloscopeStore';
|
|
|
|
const initialState = useOscilloscopeStore.getState();
|
|
|
|
beforeEach(() => {
|
|
useOscilloscopeStore.setState({
|
|
...initialState,
|
|
channels: [],
|
|
samples: {},
|
|
triggerMode: 'auto',
|
|
triggerChannelId: null,
|
|
triggerEdge: 'rising',
|
|
triggerPosition: 0.5,
|
|
triggeredAtMs: null,
|
|
triggerStatus: 'idle',
|
|
running: true,
|
|
});
|
|
});
|
|
|
|
describe('matchesTriggerEdge', () => {
|
|
it('detects rising edges only when prev=LOW → new=HIGH', () => {
|
|
expect(matchesTriggerEdge(false, true, 'rising')).toBe(true);
|
|
expect(matchesTriggerEdge(true, false, 'rising')).toBe(false);
|
|
expect(matchesTriggerEdge(false, false, 'rising')).toBe(false);
|
|
});
|
|
|
|
it('detects falling edges only when prev=HIGH → new=LOW', () => {
|
|
expect(matchesTriggerEdge(true, false, 'falling')).toBe(true);
|
|
expect(matchesTriggerEdge(false, true, 'falling')).toBe(false);
|
|
expect(matchesTriggerEdge(true, true, 'falling')).toBe(false);
|
|
});
|
|
|
|
it('detects both directions when edge is either', () => {
|
|
expect(matchesTriggerEdge(false, true, 'either')).toBe(true);
|
|
expect(matchesTriggerEdge(true, false, 'either')).toBe(true);
|
|
expect(matchesTriggerEdge(true, true, 'either')).toBe(false);
|
|
expect(matchesTriggerEdge(false, false, 'either')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('pushSample under each trigger mode', () => {
|
|
it('auto mode never sets triggeredAtMs', () => {
|
|
const s = useOscilloscopeStore.getState();
|
|
s.addChannel('uno-1', 1, 'D1');
|
|
const chId = useOscilloscopeStore.getState().channels[0].id;
|
|
|
|
s.pushSample(chId, 0, false);
|
|
s.pushSample(chId, 1, true); // rising edge — auto ignores
|
|
s.pushSample(chId, 2, false); // falling edge — auto ignores
|
|
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBeNull();
|
|
expect(useOscilloscopeStore.getState().samples[chId]).toHaveLength(3);
|
|
});
|
|
|
|
it('normal mode latches triggeredAtMs on the configured rising edge', () => {
|
|
const s = useOscilloscopeStore.getState();
|
|
s.addChannel('uno-1', 1, 'D1');
|
|
const chId = useOscilloscopeStore.getState().channels[0].id;
|
|
s.setTriggerMode('normal');
|
|
|
|
s.pushSample(chId, 0, false); // first sample — no prior state, no trigger
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBeNull();
|
|
s.pushSample(chId, 1, true); // ↑ rising — trigger fires
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBe(1);
|
|
expect(useOscilloscopeStore.getState().triggerStatus).toBe('triggered');
|
|
s.pushSample(chId, 2, false); // ↓ falling — not "rising", no re-trigger
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBe(1);
|
|
s.pushSample(chId, 3, true); // ↑ rising — re-trigger to new time
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBe(3);
|
|
});
|
|
|
|
it('single-shot mode stops capture after the first triggering edge', () => {
|
|
const s = useOscilloscopeStore.getState();
|
|
s.addChannel('uno-1', 1, 'D1');
|
|
const chId = useOscilloscopeStore.getState().channels[0].id;
|
|
s.setTriggerMode('single');
|
|
|
|
s.pushSample(chId, 0, false);
|
|
s.pushSample(chId, 1, true); // ↑ trigger → capture, freeze
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBe(1);
|
|
expect(useOscilloscopeStore.getState().running).toBe(false);
|
|
expect(useOscilloscopeStore.getState().triggerStatus).toBe('captured');
|
|
|
|
// Further pushes are ignored (`running === false` early-exit + capture lock)
|
|
s.pushSample(chId, 2, false);
|
|
s.pushSample(chId, 3, true);
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBe(1);
|
|
expect(useOscilloscopeStore.getState().samples[chId]).toHaveLength(2);
|
|
});
|
|
|
|
it('rearmTrigger resumes single-shot capture', () => {
|
|
const s = useOscilloscopeStore.getState();
|
|
s.addChannel('uno-1', 1, 'D1');
|
|
const chId = useOscilloscopeStore.getState().channels[0].id;
|
|
s.setTriggerMode('single');
|
|
|
|
s.pushSample(chId, 0, false);
|
|
s.pushSample(chId, 1, true); // capture
|
|
expect(useOscilloscopeStore.getState().running).toBe(false);
|
|
|
|
s.rearmTrigger();
|
|
expect(useOscilloscopeStore.getState().running).toBe(true);
|
|
expect(useOscilloscopeStore.getState().triggerStatus).toBe('armed');
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBeNull();
|
|
|
|
s.pushSample(chId, 2, false);
|
|
s.pushSample(chId, 3, true); // re-capture
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBe(3);
|
|
});
|
|
|
|
it('falling-edge selector ignores rising edges', () => {
|
|
const s = useOscilloscopeStore.getState();
|
|
s.addChannel('uno-1', 1, 'D1');
|
|
const chId = useOscilloscopeStore.getState().channels[0].id;
|
|
s.setTriggerMode('normal');
|
|
s.setTriggerEdge('falling');
|
|
|
|
s.pushSample(chId, 0, false);
|
|
s.pushSample(chId, 1, true); // rising — ignored
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBeNull();
|
|
s.pushSample(chId, 2, false); // falling — fires
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBe(2);
|
|
});
|
|
|
|
it('only the configured trigger channel fires the trigger', () => {
|
|
const s = useOscilloscopeStore.getState();
|
|
s.addChannel('uno-1', 1, 'D1');
|
|
s.addChannel('uno-1', 13, 'D13');
|
|
const [chD1, chD13] = useOscilloscopeStore.getState().channels.map((c) => c.id);
|
|
s.setTriggerMode('normal');
|
|
s.setTriggerChannel(chD13);
|
|
|
|
s.pushSample(chD1, 0, false);
|
|
s.pushSample(chD1, 1, true); // edge on D1 — should NOT trigger (channel mismatch)
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBeNull();
|
|
s.pushSample(chD13, 0, false);
|
|
s.pushSample(chD13, 1, true); // edge on D13 — triggers
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBe(1);
|
|
});
|
|
|
|
it('removing the trigger channel falls back to the first remaining channel', () => {
|
|
const s = useOscilloscopeStore.getState();
|
|
s.addChannel('uno-1', 1, 'D1');
|
|
s.addChannel('uno-1', 13, 'D13');
|
|
const [chD1, chD13] = useOscilloscopeStore.getState().channels.map((c) => c.id);
|
|
s.setTriggerMode('normal');
|
|
s.setTriggerChannel(chD13);
|
|
|
|
s.removeChannel(chD13);
|
|
expect(useOscilloscopeStore.getState().triggerChannelId).toBeNull();
|
|
|
|
// With triggerChannelId === null, the resolver falls back to the first
|
|
// remaining channel (chD1) so rising edges on D1 now trigger.
|
|
s.pushSample(chD1, 0, false);
|
|
s.pushSample(chD1, 1, true);
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBe(1);
|
|
});
|
|
|
|
it('clearSamples re-arms the trigger', () => {
|
|
const s = useOscilloscopeStore.getState();
|
|
s.addChannel('uno-1', 1, 'D1');
|
|
const chId = useOscilloscopeStore.getState().channels[0].id;
|
|
s.setTriggerMode('normal');
|
|
|
|
s.pushSample(chId, 0, false);
|
|
s.pushSample(chId, 1, true);
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBe(1);
|
|
|
|
s.clearSamples();
|
|
expect(useOscilloscopeStore.getState().triggeredAtMs).toBeNull();
|
|
expect(useOscilloscopeStore.getState().triggerStatus).toBe('armed');
|
|
expect(useOscilloscopeStore.getState().samples[chId]).toHaveLength(0);
|
|
});
|
|
});
|