- Resolve ISS station modules (ZARYA/UNITY/ZVEZDA/DESTINY/NAUKA) to the
primary 25544 instead of whichever module appears first in the TLE source
- Match OSCAR designators to local names spelled 'OSCAR N'/'AMSAT-OSCAR N'
(AO-7 -> OSCAR 7) so Linear lists keep AO-7 on SatNOGS-sourced data
- SSTV virtual type now uses the transceiver service class (Amateur only)
plus launcher-debris name exclusion, dropping retired weather birds
(TIROS) and rocket bodies (ARIANE 6 R/B) while keeping ISS/CSS/TEIKYOSAT
- Persist the AMSAT active-frequency whitelist (palewire mirror) on sync;
used only to disambiguate multi-match names, never as a hard filter
- matchesAmSatName: exact token match instead of substring contains,
so 'ISS' no longer matches AISSAT-1 and 'AO-7' no longer matches
AO-73 (FUNCUBE-1); resolves missing ISS in FM and missing AO-7 in
Linear lists
- resolveTypeIds: intersect SSTV catnums with current in-orbit TLE set,
excluding retired satellites (e.g. TIROS) whose radios are tagged SSTV
- add regression tests for token matching
Two correctness fixes for the FM/Linear/SSTV filters:
1. Satellite type picker: virtual transponder types (AMSAT Live FM,
AMSAT Live Linear, Live SSTV) are now mutually exclusive with the
regular TLE-source types - ticking a virtual type clears Amateur/
CubeSat/etc and vice versa, so selecting 'AMSAT Live FM' no longer
unions in hundreds of non-transponder satellites. Virtual types
still combine with each other. Also: an empty virtual-type resolve
now yields an empty list instead of falling back to 'show all'.
2. Mutual-match fallback: when the AMSAT lists are empty (first run /
sync failure) the DB mode filter used downlinkMode alone, so any
downlink-only beacon sharing a mode label was misclassified
(ISS/IO-86 CW beacons into Linear, FO-29's FM Digitalker into FM).
New getIdsWithModesAndUplink() requires uplinkLow IS NOT NULL,
matching real transponders only. SSTV filter keeps the old query
(SSTV sats are often downlink-only).
Shared virtual-type names moved to Sources.virtualTypeNames.
Mutual match page gains FM/Linear transponder-type filters. The
authoritative source is AMSAT's human-maintained Live FM/Linear
Satellites pages, fetched during data sync and stored as catnum sets;
filters resolve to those sets (falling back to DB mode filter when
empty), and unchecking both shows a prompt instead of querying.
- DataParser.parseAmSatLivePage: table rows to satellite names,
TEVEL2-1 thru TEVEL2-9 range expansion, bracket-alias matching
- SettingsRepo: AMSAT FM/Linear catnum sets persisted in prefs
- DatabaseRepo: fetch AMSAT pages in updateFromRemote, best-effort
(failure keeps previous list, never blocks the update)
- getIdsWithModes: AND isAlive = 1
- MutualScreen: FilterChip row inside MatchSearchCard
- Tests: parser (4) + viewmodel (3) + repo fakes
- MarkedStation storage upgraded to an ordered list per grid
(Map<String, List<MarkedStation>>); JSON array shape with legacy
v14.1 single-object migration on read
- MapAction: SetMarkedStation appends (duplicate callsign ignored),
RemoveMarkedStation deletes by callsign, PinMarkedStation moves a
mark to the top of the list
- MaidenheadGridOverlay: red fill for any marked grid unchanged;
first-call label mode shows the FIRST (pinned) marked callsign
- MapScreen: one row per mark (pale-yellow callsign), pin icon
(冖 + stroke ↑, drawn in-app) left of the Marked label — shown only
when the grid has 2+ marks; pinned row's icon grey, others pale
yellow; delete icon per row
- New ic_pin.xml vector drawable; strings grid_mark_pin
- New MarkedStation model + ISettingsRepo/SettingsRepo persistence
(single JSON key markedGridStations, org.json only)
- MapState/MapViewModel: markedGrids state, Set/RemoveMarkedStation
actions; marks auto-clear once the grid becomes worked
- MaidenheadGridOverlay: red fill (alpha 90, same as green worked
fill) for marked grids at both zoom levels; first-call label mode
shows the marked callsign in pale yellow on red cells
- MapScreen: unworked-grid dialog gains a 'mark a station you want
to contact' button + callsign input dialog; marked row shows red
callsign, 'Marked' in the count column, marking date, delete icon
- i18n: values/ + values-zh/ (grid_mark_*)
- Pass history (Iu-yang1 fork): PassesSettings.hoursBefore look-back window;
second 'Time before' slider in the pass filter; completed passes stay
listed (progress clamped to [0,1]) in the history window.
- Compass calibration (Iu-yang1 fork): OtherSettings.compassOffsetDegrees,
CompassAccuracy from the magnetic sensor, calibration dialog with
figure-eight accuracy meter, corrected heading and manual offset field;
RadarViewModel applies the offset.
- What's New dialog now shows the cumulative 4.4.6->4.4.7 changelog and
follows the system language (values/ + values-zh/).
- pass_filter_history_hours localized in es/ru/si/tr/uk.
Add a 'first call' pill toggle below the grid-mode switch (map top-right,
visible only in grid mode + VUCC award). When on, worked (green) cells are
labelled with the first callsign worked in that grid (earliest confirmed
QSO by time) instead of the Maidenhead code; non-worked cells get no label
at all. Persisted via OtherSettings.stateOfMapFirstCall (full MVI chain).
The feature is VUCC-only: All and other award overlays never draw it.
Day-cell numbers now count the most recent consecutive reports sharing the
newest status (counting back from the newest report, stopping at the first
different status), instead of the total report count of the day. Empty gaps
do not break the streak; statuses compare by exact report text; counting
does not cross day boundaries.
- SatDay gains streakCount (computed in AmSatRepository.buildStatuses)
- DayCell renders day.streakCount instead of slot count sum
- Add real org.json to core:data unit tests (android.jar stub cannot parse)
- Add 2 streak tests: mixed-status slot breaks, all-same-status counts all
The stored sync cursor was 'yyyyMMdd', which LoTW's qso_qslsince silently
ignores (measured live: it falls back to the system-supplied default).
Write a full UTC 'yyyy-MM-dd HH:mm:ss' cursor instead — the documented
format LoTW honors — so incremental pulls are exact (second precision)
and the settings page can show the last successful sync time on the LoTW
card, formatted like the ephemeris update time (prefs_updated_title).
Legacy yyyyMMdd cursors are normalized for the API, the gate and the
display (lotwCursorApi/lotwCursorDate/lotwCursorEpochMs).
Mirrors the ephemeris auto-update check: on every app start, if LoTW
credentials are configured, the new 'auto-sync LoTW grids' toggle is on,
at least one manual sync was done before, and today has not synced yet
(ARRL rate limit, ~once a day), pull confirmations since the last sync
and merge them. Same callsign -> incremental merge; callsign change ->
full replace. Manual dialog and automatic path share the orchestration
(mode resolution, gate, merge/persist) moved to core:domain
LoTWGridSync.kt; LoTWSyncMode moved to core:domain. 8 unit tests.
Two explicit sync buttons in the LoTW dialog:
- Full sync: qso_qslsince=2000-01-01, fresh report replaces all stored data.
- Incremental merge: since=last successful sync date, merged into the stored
set (grids union, QSOs deduped by call + time). First sync / callsign change
automatically falls back to full replace so no stale grids from another
account linger.
Progress & estimates:
- Report body is now streamed; the header's <APP_LoTW_NUMREC> gives the real
QSL count, total size is estimated as NUMREC * 720 B (measured), and the UI
shows a determinate bar with 'N QSOs, ~X s remaining' once known.
- Pre-sync hint shows the measured rate (~8 s per 100 QSOs).
- readTimeout raised to 600 s (stall timeout, not a total cap — huge accounts
legitimately take 30+ min at ARRL's ~10 KB/s stream rate); connectTimeout
raised to 60 s (a busy server took 36.5 s to accept a connection).
Also: ISettingsRepo gains last-sync date/callsign bookkeeping, and the mutual
test fake implements the new members.
GridQso gains myGrid (ADIF MY_GRIDSQUARE) captured per QSO during LoTW
parsing (handles alphabetical field order + EOR reset) and persisted as
JSON key 'mg' (absent in pre-myGrid data -> null). Legacy syncs keep the
global VUCC count; one resync unlocks per-grid counting.
- DB v2 migration: entries.ndot column so hasDecayed() actually works after
reload (parsed ndot was dropped on write, making the decay detection dead
code); radios.isCustom column to mark manually imported transceivers.
- updateTransceiversFromFile marks imported radios isCustom=true; remote
updates now deleteManagedRadios() then insert the fresh snapshot, keeping
manual imports (no source can refresh them, so nothing would bring them back).
- Wire MIGRATION_1_2 into MainContainer (fallbackToDestructiveMigration=false
would otherwise crash on upgrade).
- Grid mode now opens with the VUCC award chip selected by default (re-asserted
on every grid-mode entry; the user's in-mode chip choice is preserved).
- The station's own grid outline is now drawn at every zoom level: 2-char field
border below the sub-square zoom, 4-char square border above it (was hidden
at the default entry zoom), stroke widened to 6f.
- Grids the account operated from (LoTW <MY_GRIDSQUARE>, satellite QSOs only)
are covered with GridMaster-style blue 45° zebra stripes. The stripe paint is
fully opaque so a worked+roamed cell shows pure-blue stripes with green
between them — the two colors never alpha-blend into a teal/green mix.
- LoTW sync now parses and persists the roamed set (LoTWResult.Success +
SettingsRepo key 'roamedGrids'), plumbed through MapState/MapViewModel into
MaidenheadGridOverlay. Unit tests cover MY_GRIDSQUARE parsing (SAT-only,
GRIDSQUARE non-leak, 4-char truncation).
- projectionToY clamps latitude to Web Mercator max (±85.05) so raw -90
polar vertices (Antarctica ring, WAZ zones 29/30/32/39) no longer project
to infinity, removing vertical pole spikes
- traceSegment unwraps longitude deltas per-vertex (continuous with previous
point) instead of folding each vertex independently — a full-circle ring
(Antarctica, span ~358°) no longer jumps ±180° mid-ring to draw a line
across the whole map
- closing chord now folds to the nearest world copy (short-side seal at the
antimeridian) instead of a full-width chord
- AwardCalculator: DXCC chip now listed before VUCC
- wapc/dxcc assets: add Hong Kong & Macau labels (forceLabel) and MO boundary in dxcc
- AwardBoundaryOverlay: rings split at antimeridian no longer close() per segment — removes spurious Russia cross-continent chord
- AwardCalculator: Macau DXCC entity 330 -> 152 (+test)
- Settings LoTW dialog: add sync duration hint (zh/en)
- Grid-mode top bar replaced with award filter chips (All/DXCC/VUCC/
WAPC/WAJA/WAZ/WAS) showing live progress (count/target)
- New AwardBoundaryOverlay renders real administrative boundaries
(provinces/prefectures/CQ zones/states/countries) with the VUCC visual
language: yellow boundary lines, green fill for worked regions, labels
- Boundary assets packaged from public GeoJSON (DataV China, Japan land,
HB9HIL CQ zones, Natural Earth states/countries) with ADIF match keys:
WAPC province codes, WAJA ADIF prefecture numbers, US state abbrs,
ARRL DXCC entity codes
- GridQso extended with dxcc/country/cqz/state parsed from LoTW report
(qso_qsldetail); AwardCalculator derives six-award progress; STATE is
interpreted per DXCC entity (ADIF 'STATE depends on DXCC')
- Grid-mode toggle pill floated at map top-right (Grid mode / Satellite mode)
- New tests: AwardCalculatorTest (9 cases), LoTW award-field parsing (3 cases)
Rate limit classification per ARRL policy (one download in progress per user
id; HTTP 503 'Page request limit' observed on throttled pulls) and the HTML
login-error page returned for wrong credentials. Settings screen now shows a
specific message per cause instead of one generic 'check callsign/password/network'.
- LoTW sync now stores per-QSO detail (call/time/sat/mode/bands) grouped by
gridsquare (PROP_MODE=SAT only), single JSON blob in SharedPreferences
- GridQso model derives band label (V/U, U/V, V/A...) from BAND_RX/BAND
- Tap detection via osmdroid MapEventsOverlay; selected grid outlined green
- Centered dialog: calls sorted by first-contact time, first-QSO summary,
expandable call rows list subsequent QSOs in that grid; tap outside closes
Add a full-screen update checker accessible from a button at the
bottom of the settings page. It queries the fork's GitHub latest
release and can download + install the APK in place.
- UpdateRepository (core/data): fetch releases/latest via GitHub API,
parse tag/description/APK asset, stream APK download to cacheDir
- VersionComparator (core/domain): compare 4.4.6-ba7opf.N style tags
(base version first, then build number; v-prefix tolerant) + tests
- IMainContainer/MainContainer: inject updateRepo
- UpdateCheckerScreen (feature/settings): shows current version,
checks for updates, displays latest version + release notes, and a
Download & install button; FileProvider hands the APK to the
system installer
- SettingsScreen: bottom entry button, full-screen page toggle
- AndroidManifest: REQUEST_INSTALL_PACKAGES permission + FileProvider
- i18n strings (en/zh)
- Point the built-in SatNOGS source at db.satnogs.org/api/tle/?format=3le
(the CelesTrak satnogs group is a separate, slower subset); rename the
old CelesTrak source to 'CelesTrak SatNOGS'.
- Add a one-time migration in SettingsRepo: installed apps that already
persisted a satelliteUrls list containing the old CelesTrak SatNOGS URL
get the real SatNOGS TLE URL inserted right after it.
- Dedupe imported satellites by catnum and radios by uuid in
DatabaseRepo.updateFromRemote so multiple sources don't insert the
same primary key twice.
- Test: importing JAMX-0825b (NORAD 98248) from the real SatNOGS source.
Start AMSAT status prefetch when the app enters foreground and share the in-flight request with the status page fetch path to avoid duplicate network requests.
When adjusting RX in passband mode (via +/- buttons or slider), the old
code used TransponderMapper.mapDownlinkToUplink (pure passband mapping
without Doppler compensation), producing ~10kHz error at LEO velocities.
Fix: use DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset
for the full TX-from-RX round-trip, matching the RX→PASSBAND button
onClick logic. Also fixed frame mismatch: position now uses txLow/txRange
(Doppler-shifted frame) consistently.
Port upstream PR #234 to fork main:
- New feature:status module (SatStatusScreen, SatStatusViewModel)
- New AmSatRepository + IAmSatRepository + SatStatus models
- RemoteSource: AMSAT API methods (getAmSatCatalog, getAmSatReports)
- MainContainer/IMainContainer: DI injection
- MainTheme: AMSAT status colors (tertiary slots)
- Chinese translations added for all AMSAT strings
- 26 unit tests passing
Note: navigation entry not included — upstream PR #234 intentionally
left it out pending UI restructuring. Add AMSAT to your nav bar or
menu as desired.
- isNamedLinearTransponder: match ' Lin' prefix (AO-7 style) and
add fallback branch (hasLinearName && hasLinearMode) for entries
with 'Linear'/'Lin' but no 'transponder' word (AO-7, AO-73)
- add deduplicateTransponders: merge entries sharing the same
uplink/downlink frequency range, preferring non-CW entry
(AO-7 Mode A had separate SSB and CW entries; JO-97 had
separate CW and SSB Transponder entries)
- apply dedup in CalculatorPage filter chain
- 6 new tests, 23 total, all passing
- MutualViewModel is Activity-scoped, so returning from Radar keeps query results
- TrackSampleData gains time field for live cut-off
- Removed standalone mutual card from radar page
- RadarViewCompose draws optional dashed station-B track + current dot
- RadarScreen builds trackB from mutual data up to current time
- MutualPass now carries TrackSample (azimuth/elevation for both stations)
- New MutualRadarView: polar plot with elevation rings (30/60/90),
cardinal spokes, solid A-track vs dashed B-track, AOS/LOS markers
- Shown in expanded mutual pass card and radar page mutual card
- Fix cubic Bezier control points (proper Catmull-Rom to Bezier conversion)
- Refine AOS/LOS more robustly by walking from edge into the pass window
- Still 5s sampling for smooth curves
- Add newColumnCount tracking to CwSpectrogram
- CwDecoder now processes each new column individually for timing
- Each column = 8ms at 8000 Hz sample rate
- Proper per-column iteration through spectrogram history
Complete rewrite inspired by Morse Expert / CW Skimmer (VE3NEA):
- CwFFT: radix-2 FFT (256-point) for time-frequency analysis
- CwSpectrogram: sliding-window waterfall (40 cols x 33 bins, 8ms resolution)
- CwBayesianDecoder: Gaussian probability replaces hard dit/dash thresholds
- CwChannelTracker: multi-channel peak detection (up to 3 signals)
- CwDecoder: integrates all components, monitors 200-1200 Hz simultaneously
Key advantages over v2 (ggmorse):
- Frequency-agnostic: full spectrum monitored, not locked to one tone
- Multi-channel: tracks multiple signals in parallel
- Bayesian: probability-based decisions, not hard ratios
- Doppler tolerant: frequency drift just moves energy between bins
- After initial pitch lock, re-scan every ~8 seconds in a narrow
±100 Hz window around the current pitch estimate
- Only re-lock if pitch change > 20 Hz (avoids jitter)
- Helps maintain lock when Doppler tracking has residual error
- Add CwDsp with FIR bandpass filter, envelope detection, Goertzel tone detector
- Add CwDecoder with real-time Morse timing analysis and character lookup
- Add CW state/actions to RadarState, wire into RadarViewModel
- Add collapsible CW decoder panel to transceivers page
- 19 unit tests covering DSP, Morse table, and decoder state
- Shares IAudioCapture with SSTV, auto-stops SSTV when CW starts
- Add DopplerFrequencyCalculator utility with passband mapping + Doppler correction
- Add MHz input UI with real-time 1Hz refresh during pass
- Add kHz offset for downlink frequency correction
- TX/RX fields on same row for compact layout
- 40 unit tests covering linear/FM/inverted transponders