FoxSDR 0.99.75 lists the aircraft the ADS-B receiver has heard but not yet placed, below the placed ones and counted apart (the plugin must be ADS-B 1.9.0 or newer to publish them), and remembers each radio's manual gain and puts it back the next time the radio is opened. Before this every SoapySDR radio opened at 30 dB whatever the hand had set, so a B200 run at 76 dB began each session 46 dB down. The plugin ABI is still 3 and gains two definitions, CASCADE_TRACK_FLAG_NO_POSITION and CASCADE_STATE_LISTS_UNPLACED_TRACKS; the host API level is unchanged. The four new interface words are in all 33 languages. This release's notes also say what was measured about receiving ADS-B on a B200: the antenna port, the gain, and a 1090 MHz filter.
Run once on the real thing before release, 2026-10-13: this build, installed, opened the owner's B200 with the remembered gain (source: manual gain restored - PGA 60.0 in the log), loaded ADS-B 1.9.0 and ignored the 1.8.1 copy beside it, and decoded 6,819 Mode S frames from 16 addresses in twelve minutes on TX/RX at 60 dB. Still not fixed, and known: the heard-without-a-position rows themselves were not seen by eye in that run (the display had gone to sleep and every window capture came back blank; the rows are proved by test_unplaced_list_app against the real application and a fixture that keeps 1.9.0's contract); the scope face's GAIN knob, one of the four ways a gain reaches the memory, is drawn by ImGui and no test presses it; the new words in thirty-three languages are machine translations that nobody has read; and everything the 0.99.74 notes list as not fixed or not verified is still so.
SHA-256 of foxsdr-setup-0.99.75.exe:
b6fe9e3e85e2c7c115be01286d654a38c83a875d5936587e6b9a5710635df27a
SHA-256 of FoxSDR-0.99.75-x86_64.AppImage:
91d7ccc8387aed08d93f7892279cf6bfb2e206a8d8dfef18b0202c628505ae46
SHA-256 of foxsdr-0.99.75-linux-x64.tar.gz:
74e9045199f657e0ca28451825aaabcb8135acc614691660ba3cfadf0cfadc19
SHA-256 of FoxSDR-0.99.75-aarch64.AppImage:
e0a10a6902cc61333768eeb017fb9e1017d4df5d1b84cd5d4b3030cd7e745d02
SHA-256 of foxsdr-0.99.75-linux-arm64.tar.gz:
d817be866ad6da81250a1c9046ff3fb3c3de4d4bb644b07b382ef10eb2f70a90
Features and fixes, 2026-10-12
Two changes were made on 2026-10-12, after 0.99.74 was published: the list of aircraft heard without a position, and the remembered manual gain. The section after them, on receiving ADS-B on a B200, is a note and not a change.
The target list now lists the aircraft the receiver has heard but cannot place yet, below the placed ones and counted apart, and never draws them. An ADS-B decoder hears an aircraft's identity, altitude or callsign before its position message arrives, and until now it had to hide the aircraft until it could place it. The plugin ABI gains two definitions, and the plugin repository's copy of plugin_abi.h must receive the same two lines: CASCADE_TRACK_FLAG_NO_POSITION (0x00000004u, in CascadeTrack::flags: the coordinates are NaN, the source hears this target and cannot place it yet, a host lists it and must not draw it) and CASCADE_STATE_LISTS_UNPLACED_TRACKS (0x00008000u, the next free CASCADE_STATE_* bit after _TRANSMITTING at 0x4000: the host understands the flag, and a plugin may publish such tracks only when it sees this bit). The host sets the bit in every CascadeReceiverState it fills, for every plugin, with no grant behind it. The ABI is still 3 and the host API level is unchanged. A track that carries the flag, or whose latitude or longitude is not finite (so an old or careless plugin that sends NaN without the flag is covered too), is kept in a list of its own (PluginUi::unplacedTracks()), out of the vector the map, the scope, the coverage map, the auto-fit, the follow, the web snapshot and the patch map's map part are all fed from, so none of them can draw it, fit to it, follow it, count it in the coverage map or call it "on screen"; the map also checks the position itself, and MapView::goTo refuses a place that is not a number. Its trail is dropped too (a path whose id is a no-position track's), and no altitude is recorded for it. In the target list it is a row after the placed rows, dimmed (drawn at 60% of the strength a placed row of the same age has), with its label and id, a second line with the altitude where it is known (35000 ft - no position yet; kilometres for a satellite) or just no position yet, a hover that shows the detail block (whose position line says no position yet and which has no range or bearing), and a Details button; it fades and drops on the same ageMs rule as a placed target of its kind. The heading reads 12 targets, 3 heard without a position when the second number is not zero (and 1 target, 1 heard ... and 3 of 41 targets, 2 heard ... in the singular and the On-screen-only forms), and the three plain lines are exactly as before when it is zero; the rail chip keeps counting the placed targets. With On screen only ticked the heard rows are gone, because they have no place on the map and so are never on screen, and the heading still counts them. The details window and the rail's Target details section find such a target by id, and the registry lookup works for it as for any other; their Go to on map and Follow keys are dead for it until it has a position.
One behaviour changed that is not new surface: a track with a latitude or longitude that is NaN and no flag used to be dropped without a word, and is now listed as heard without a position; one with finite coordinates outside the globe (91 degrees, 999 degrees) is still dropped. The existing test that named the old behaviour was changed to say so.
Verified on Windows. test_plugin_ui (15,131 checks) polls a fake source that sends one placed track, one flagged with NaN, one flagged with real-looking numbers (a careless plugin: the flag wins and the numbers are not kept), one NaN with no flag, one infinite and one out of range: only the placed one reaches the vector the map is fed from, the four unplaced come back in the order sent, tagged with their plugin, flag set and both coordinates NaN, with their altitude and label; the placed and unplaced counts are separate over either vector and over the two joined; the map is not opened for nothing but unplaced targets; an aircraft at 0, 45 s, 59.999 s and 60 s fades and drops as a placed one does and a vessel at 90 s is kept; a path whose owner is unplaced is not stored and the pure trail rule hides it; stopping the plugin empties the list at once. test_map_view (2,052) draws the same map with and without four unplaced targets beside a placed one (one flagged NaN, one flagged with numbers inside the view, one NaN without the flag, one flagged with numbers far away): the same vertex count, every vertex finite, the auto-fit centre and span of the placed one alone, a follow of an unplaced id leaves the view where it was, goTo(NaN) is ignored, and listedTrackCount lists two placed rows and one with the switch ticked whatever else is in the vector. test_track_metrics (4,295) pins the heading in its six forms with the English the catalogues are keyed on, the row's second line, the detail block, and that a NaN position never reaches the coverage map; test_track_detail_kind (26) runs the real adapter with a fake track-info plugin; test_plugin_settings_ui (325) pins CASCADE_STATE_LISTS_UNPLACED_TRACKS == 0x8000, that it is a new bit, and that it is set for the plugin that declared nothing, the one that declared the receiver locator and one whose struct stops before it. test_unplaced_list_app (34), new, starts the real cascade.exe four times against a fixture track source that keeps the plugin's contract (it reads the bit with get_state and publishes its unplaced aircraft only if it is set): with its map page opened, the census reads 1/1 placed and 2 heard (a third, last heard 61 s ago, is dropped), a row for the placed aircraft and, below it, one for each heard one; an unflagged NaN is listed too; with On screen only ticked there are no heard rows and the heading still says 2; with nothing placed the two heard rows are still drawn. Each of 23 deliberate breakages made the test named after it fail with the failure it names (one of them, the list returning to its empty state with rows to draw, first passed and was run again against a stronger test): the flag ignored; non-finite coordinates ignored; the placed count counting unplaced ones; the unplaced count ignoring age or counting placed ones; the poll dropping unplaced tracks, not setting the flag, not blanking the coordinates, not clearing last frame's, or keeping their trail; the stop leaving them listed; the trail rule ignoring an unplaced owner; the state bit not set (in the unit test and through the real application); the map's fit, follow and marker guards, goTo's guard and the list count's guard each removed; the list not drawing the rows, drawing them with On screen only, or returning to the empty state with rows to draw. Not verified: the rows with ADS-B plugin 1.9.0 and real aircraft on screen (the plugin and this build ran together on the B200 for twelve minutes, but the display was asleep and the captures were blank, so the fixture is the only thing that has been seen to draw them); the picture of the rows, which was read as rectangles from the census and not looked at; and one fixed choice, the dimming at 60%, which was not tried against a real map.
A radio's manual gain is now remembered and put back at its next open, so a B200 set to 76 dB no longer starts at 30 dB. Every open of a SoapySDR radio pushed a mid-dial 30 dB into every stage (the B200 here has been opened that way for months) and nothing remembered what the hand had set, so the owner's B200, run at 76 dB, began each session 46 dB down. AppConfig::manualGainByRadio ("manualGainByRadio": { "<radio key>": { "<stage>": dB, ... } } in config.json) keeps, under the same radio key as the bias tee and the auto gain switch, each stage the user has set and what the radio read back for it (the radio quantises). It is written when the user changes a stage and the radio accepts it - the Source section's sliders, the top bar's GAIN dial, the scope face's GAIN knob and a browser's gain control - and never by the open's own 30 dB push, by the restore itself, by a step the radio refuses, or while automatic gain is on. At an ordinary open (not the reopen after a driver fault, which restores the gains the user had a moment ago, and not an Airspy, whose gains are part of its gain mode) each remembered stage the radio still lists is set again after the push and the first readback and before the remembered auto gain is put back, clamped into the stage's own range, the mirror is read back so the dial and the sliders show what the radio took, and one line is logged: source: manual gain restored - PGA 76.0, .... A stage the radio no longer lists is ignored and kept for a radio that has it; a stage the radio turns down is skipped without a word. The file is read element by element: a radio whose value is not an object or whose key is not <kind>|<args>, a stage with no name or whose value is not a finite number, and a radio left with no stage are skipped, at most 64 radios and 16 stages of each are kept (the first sixteen by name), and a stage that is not a finite number is not written. The save debounce sees a change (configsEqual), and the start-up state keeps it. docs/MANUAL.md and docs/DIAGNOSTICS.md say so.
Verified on Windows through the real AppWindow and fake radios (an LNA 0 to 40 and a PGA 0 to 76 that round to whole decibels, and a twenty-stage one for the cap), test_autogain_app (389 checks; it was 227): nothing is written by an open; a stage set through the shared slider/dial path is remembered as the readback (33.4 becomes 33), sorted by name, only for the stage touched; a refusal, an unlisted stage and auto gain being on write nothing; a browser's gain control writes it and one for a stage the radio does not have does not; a Soapy open shows 30, 30 then LNA 33, PGA 76 in the radio's own order of calls, a native open only the second, with one log line naming them, the memory unchanged and the mirror equal to the radio; a remembered -5 and 200 reach the radio as 0 and 76, a stage the radio does not list is never asked for, and a PGA that refuses is skipped with no error line while the LNA comes back; a reopen after a driver fault gives the radio the 25 the user had and not the remembered 70 and logs nothing; with auto gain remembered on, the manual stages are set before the switch goes on, and switching it off brings them back after the radio has fallen to the bottom; a save, a restart through the file and a restore bring the radio back at 33 and 70; at 64 radios a known radio is updated and a new one is not added, and at 16 stages a seventeenth is not remembered while one already there still is. test_config (3,426) round-trips the map, checks the tolerance cases and both caps, configsEqual against a radio appearing, a stage appearing and a number moving, and startupState; test_airspy_app (55) checks that an Airspy's gains stay out of it, through the web path, in the mode where the request finds its stage by name. Each of 23 deliberate breakages made a test fail (the Airspy one first passed, because the Airspy test's first web request switched the radio's mode and so never reached the line, and was run again against a test that sends it twice): the loader not storing, the saver not writing, each cap, a key without <kind>|, a nameless stage, a stage that is not a number, a radio left empty, configsEqual ignoring it, the restore disabled, unclamped, asking for an unlisted stage, run on a recovery reopen, run after the auto gain, or not reading the mirror back, an Airspy remembered, a write under auto gain, either cap in the app, the shared path or the browser path not writing, and the memory not read from or put into the saved config. Not verified: the B200 itself; the scope face's GAIN knob writing the memory (it calls the same two lines as the others, but the knob is drawn by ImGui and no test presses it); that a stage that is not a finite number is not written (JSON has no NaN, and a NaN written would come back as a skipped null, so a check cannot tell the guard from its absence); and what a real SoapySDR driver reports for a stage's range, which a driver that reports none gets no clamp for.
The four new interface texts (%d targets, %d heard without a position, its singular and its On-screen-only form, and no position yet) are in all 33 catalogues (py -3.14 tools/i18n_keys.py: 1968 translated, 0 empty, 0 missing, 0 stale in each), src/core/lang_assets.hpp was regenerated once with tools/embed-lang.py, and test_i18n (195,395 checks) and test_i18n_glyphs pass. They are machine translations that nobody has read; where the verb would have to agree with a number, the count follows a colon (heard without a position: N) in the languages that need it. The two changes together build in Release without an error, and the whole suite, 364 tests one at a time, passed 364 of 364 in 2,199 s.
Receiving ADS-B on a B200
On a B200, set the antenna port to the one the antenna is on, run the gain well below the top, and put a 1090 MHz band-pass filter in front of the radio. These are measurements from one B200, not rules that FoxSDR enforces. The antenna port must be the one the antenna is plugged into: TX/RX against RX2 was 58 decoded lines against 2 for the same aircraft. At 60 to 65 dB the radio decoded four to eight times as many frames as at 76 dB on this front end. An unfiltered B200 is compressed by strong signals between 700 and 900 MHz, so a 1090 MHz band-pass filter helps.
ADS-B plugin 1.9.0 places an aircraft from a single frame once the aircraft is known, and from one frame against the receiver position for an aircraft below about 32,000 ft within 100 nm.