Files
updated-radar/DESIGN.md

246 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Radar Simulation — Class Design and File Layout
Author: Mark Allyn
---
## Class Hierarchy
```
Scope (abstract base)
├── ExhibitIntro
├── AScope (abstract)
│ ├── MarineAScope
│ └── ChainHomeAScope
├── PPIScope (abstract)
│ ├── MarinePPIScope
│ └── ATCPPIScope
└── PARScope
```
---
## Class Descriptions
### Scope (abstract base)
Everything all scopes share:
- Left panel text rendering
- `s` / `S` key handling (scope advance / reverse)
- Auto-advance timer reset on any key or control input
- Pure virtual methods: `render()`, `handleKey()`, `getDescription()`
### ExhibitIntro : public Scope
- Text-only rendering, no radar display
- Header: "WELCOME TO MUSEUM VINTAGE RADAR EXHIBIT" (all caps)
- Emphasizes s/S keys and 120-second auto-advance
### AScope : public Scope (abstract)
Shared A-scope behavior:
- Horizontal range axis, vertical amplitude axis
- Noise floor rendering (rain/wave clutter)
- Incandescent graticule (three horizontal amplitude lines + vertical range lines)
- Bearing control with key-hold acceleration
- Phosphor type as parameter (P1 or P7)
### MarineAScope : public AScope
- P1 phosphor (green)
- Range settings: 2, 4, 6 miles
- Max 2: one interim range at 1
- Max 4: one interim range at 2
- Max 6: one interim range at 4
- Keys: c (bearing CW), v (bearing CCW), u (range up), d (range down)
- u and d are ignored during graticule swap animation
**Graticule swap animation:**
In the period, changing max range required the operator to physically slide the glass
graticule panel upward and out from in front of the CRT, then slide the replacement
graticule (calibrated for the new range) downward into position. This is simulated.
State machine triggered by u or d key:
NORMAL graticule in place, scope operating normally
|
| u or d pressed
v
SLIDING_OUT old graticule translates upward off screen (~0.5 seconds)
|
v
BARE_CRT no graticule rendered; CRT trace and noise floor still running
|
v
SLIDING_IN new graticule slides down into position (~0.5 seconds)
|
v
NORMAL new range now active
- u and d keys ignored while state != NORMAL
- Graticule remains incandescent color throughout (edge-lit glass, not CRT-dependent)
### ChainHomeAScope : public AScope
- P7 phosphor (early implementation, slow decay for slow PRF)
- Goniometer state: H/V mode toggle, azimuth angle, elevation angle
- PRF toggle: 25 Hz / 12.5 Hz
- Calibrator stretch/shrink scale factor
- Fixed 100-mile range (no range change)
- Keys: [ (goniometer H/V toggle), 9 (tune left), 0 (tune right),
. (PRF toggle), n (calibrator shrink), m (calibrator stretch)
### PPIScope : public Scope (abstract)
Shared PPI behavior:
- Clockwise sweep with P7 phosphor persistence
- Immediate beam strike: blue
- Persistence: green/yellow, faded by next sweep pass
- Incandescent bearing graticule:
- Inner ring with 1-degree tick marks (0359, True North = 0)
- Text labels every 15 degrees
- Outer ring
- Yellow cursor: 10-degree arc section + bearing crossline
- Cursor range/bearing readout displayed under scope (white text)
- Bearing offset for boat mode (k = right, j = left)
- Keys: r (cursor bearing right), l (cursor bearing left),
t (cursor range increase), y (cursor range decrease),
k (antenna bearing offset right), j (antenna bearing offset left)
- Cursor range clamped to max range if exceeded
### MarinePPIScope : public PPIScope
- Sweep time: 4 seconds
- Max range settings: 2, 4, 6 miles
- Max 2: rings at 1, 2
- Max 4: rings at 2, 4
- Max 6: rings at 4, 6
- Keys: u (range up), d (range down) — affects only this scope
### ATCPPIScope : public PPIScope
- Sweep time: 5 seconds
- Max range settings: 5, 10, 15, 20 miles
- Max 5: rings at 2.5, 5
- Max 10: rings at 2, 4, 6, 8, 10
- Max 15: rings at 4, 8, 12, 15
- Max 20: rings at 5, 10, 15, 20
- Keys: u (range up), d (range down) — affects only this scope
### PARScope : public Scope
- Two vertically stacked sub-scopes (right panel):
- Top: azimuth (lateral deviation vs. range) — ~1/3 larger
- Bottom: elevation (vertical deviation vs. range)
- P7 phosphor; graticules are incandescent etched glass
- 30 Hz alternating scan between azimuth and elevation planes
(each plane scans at ~15 Hz, i.e., 1/15 second per plane)
- Fixed 10-mile range — no range change control
- Non-linear horizontal scale: inner 5 miles occupies 70% of width
- All targets simulated; no cursor or bearing controls
- Located at south end of Runway 16/34, BLI — active runway 34 (northbound)
---
## Supporting Classes
| Class | Thread | Purpose |
|---|---|---|
| `ScopeManager` | 1 | Owns scope list; handles s/S switching and 120s auto-advance timer |
| `PhosphorRenderer` | 1 | P1 and P7 decay/persistence simulation; shared dependency |
| `Graticule` | 1 | Draws incandescent graticule lines and text; parameterized per scope |
| `LeftPanel` | 1 | Renders scope description text panel (left side of window) |
| `SharedRenderState` | 1,2,3 | Mutex A — state variables Thread 1 reads each frame to push as shader uniforms |
| `TargetBuffer` | 2,4 | Mutex B — target data handoff between Thread 2 (traffic cop) and Thread 4 (simulator) |
| `TrafficCop` | 2 | Polls Simulator and RPi receivers each beam update; writes targets to SharedRenderState |
| `Simulator` | 4 | Runs fake targets independently; returns data to TrafficCop when polled |
| `KnobPanel` | 3 | Future hardware stub — writes to SharedRenderState under Mutex A |
| `RPiReceiver` | 2 | Stub — one instance per Raspberry Pi; called by TrafficCop |
---
## Thread Summary
| Thread | Class(es) | Mutex Access |
|---|---|---|
| Thread 1 | ScopeManager, all Scope subclasses, PhosphorRenderer, Graticule, LeftPanel | Reads SharedRenderState under Mutex A |
| Thread 2 | TrafficCop, RPiReceiver | Writes SharedRenderState under Mutex A; reads TargetBuffer under Mutex B |
| Thread 3 | KnobPanel | Writes SharedRenderState under Mutex A |
| Thread 4 | Simulator | Writes TargetBuffer under Mutex B |
Keyboard input arrives via GLFW callback (glfwSetKeyCallback) in Thread 1.
Thread 1 dispatches s/S to ScopeManager and all other keys to the active Scope.
---
## Proposed File Layout
```
src/
main.cpp — GLFW init, thread launch, main loop
scope_manager.h / scope_manager.cpp
scope.h / scope.cpp — abstract Scope base class
scope_intro.h / scope_intro.cpp — ExhibitIntro
scope_ascope.h / scope_ascope.cpp — abstract AScope
scope_marine_a.h / scope_marine_a.cpp
scope_chain_home.h / scope_chain_home.cpp
scope_ppi.h / scope_ppi.cpp — abstract PPIScope
scope_marine_ppi.h / scope_marine_ppi.cpp
scope_atc_ppi.h / scope_atc_ppi.cpp
scope_par.h / scope_par.cpp
phosphor.h / phosphor.cpp
graticule.h / graticule.cpp
left_panel.h / left_panel.cpp
shared_render_state.h / shared_render_state.cpp
target_buffer.h / target_buffer.cpp
traffic_cop.h / traffic_cop.cpp
simulator.h / simulator.cpp
knob_panel.h / knob_panel.cpp
rpi_receiver.h / rpi_receiver.cpp
settings.h — all tunable constants (no .cpp needed)
shaders/
phosphor.vert / phosphor.frag — parameterized for P1 and P7 via uniforms
graticule.vert / graticule.frag
text.vert / text.frag
sweep.vert / sweep.frag
```
---
## Key Design Notes
1. **ScopeManager** sits in Thread 1 and holds the active scope pointer. The GLFW
key callback calls ScopeManager::handleKey(), which dispatches s/S to itself
and all other keys to the active scope. The auto-advance timer resets on any
key event or control input.
2. **SharedRenderState** holds two categories: control state (bearing, range,
cursor — written by Thread 1 keyboard callbacks and Thread 3 knob panel) and
target state (written by Thread 2). Thread 1 reads the whole struct once per
frame under Mutex A to push uniforms to the shaders.
3. **TargetBuffer** is separate from SharedRenderState — it is the handoff point
between Thread 2 (traffic cop) and Thread 4 (simulator) under Mutex B.
4. **PhosphorRenderer** is a shared utility. AScope and PPIScope subclasses
receive it as a constructor dependency rather than each reimplementing decay
logic. Pass phosphor type (P1/P7) and decay constant as parameters.
5. **Shaders** are parameterized via uniforms rather than duplicated. A single
phosphor shader pair handles both P1 (green, no persistence) and P7 (blue
strike, yellow-green decay) by passing color and decay time as uniforms.
6. **Shoreline and terrain geometry** is loaded once at Thread 1 startup as a
static VBO. It is read-only after load — no mutex needed.
7. **Each scope's max range and cursor state are independent.** Changing range
on the Marine PPI does not affect the ATC PPI, and vice versa. State is
owned by each scope instance.
8. **PAR sub-scopes** (azimuth and elevation) can be implemented as private
member objects within PARScope rather than as separate Scope subclasses,
since they are never displayed independently.
9. **settings.h** is a header-only file (no .cpp). It will contain only
`constexpr` constants organized into named sections. Every other source
file that needs a tunable value includes this file. No magic numbers
anywhere else in the codebase. Add candidate variables here before
coding begins; values can be refined during debugging and appearance work.
10. **MarineAScope graticule swap** is a state machine with four states:
NORMAL → SLIDING_OUT → BARE_CRT → SLIDING_IN → NORMAL. The u and d keys
are blocked during the animation. The new range value is latched when the
key is pressed but not applied to the scope state until SLIDING_IN completes.
Animation duration is approximately 0.5 seconds per slide (out and in).