feat(ftmap): a layer for who hears me

The map drew one direction of every path on it: what this receiver
decoded. The reverse — which stations are reporting our own
transmissions — is the half an operator cannot see from their own
radio, and on FT8 it is the half that decides whether calling is worth
the cycle.

It is the narrowest slice of the PSK Reporter feed there is. The v2
topic is

	pskr/filter/v2/<band>/<mode>/<tx call>/<rx call>/…

so putting the operator's callsign in the TRANSMIT level makes the
broker send nothing else. For scale, from internal/pskr's own measured
numbers: four bands unfiltered is 83 messages a second, filtered on the
receiver's square 0.2 to 1.2 a second — one callsign in the transmit
level is a handful per FT8 cycle however open the band is. Both grids
are in the payload, so the arc is arithmetic and there is no lookup.

internal/pskrme, with its own connection, for the same reason
internal/pskrtgt has its own: the three want slices of the feed that
cannot be filtered out of one another. It also means this keeps working
with the band-opening watch off — hanging it off that feed's lifecycle
would have made it fail silently for anyone not chasing openings.

Nothing is persisted. One entry per STATION inside a fifteen-minute
window, carrying its freshest report: PSK Reporter's uploaders batch,
many every five minutes, so a tighter window would show a fraction of
who actually heard the last few calls. Stop clears the window, or
switching the layer back on would redraw who heard us before it was on.

On the map the layer is dashed and single-coloured. Solid is what we
decoded, dashed is somebody decoding us; colour alone could not carry
that distinction next to fourteen band colours. The receivers are rings
rather than filled dots for the same reason. Per profile, since the
callsign IS the subscription — a switch resubscribes rather than going
on reporting who hears the previous station.
This commit is contained in:
2026-09-10 14:01:39 +02:00
parent f090e845ff
commit d25edd114c
10 changed files with 649 additions and 5 deletions
+9
View File
@@ -14,6 +14,7 @@ import {bandopen} from '../models';
import {cluster} from '../models';
import {dxped} from '../models';
import {extsvc} from '../models';
import {pskrme} from '../models';
import {powergenius} from '../models';
import {pskrtgt} from '../models';
import {pskr} from '../models';
@@ -517,6 +518,10 @@ export function GetGridCacheStatus():Promise<main.GridCacheStatus>;
export function GetGridScopeSettings():Promise<main.GridScopeSettings>;
export function GetHearMe():Promise<boolean>;
export function GetHearMeStatus():Promise<pskrme.Status>;
export function GetIcomState():Promise<cat.IcomTXState>;
export function GetKenwoodState():Promise<cat.KenwoodTXState>;
@@ -667,6 +672,8 @@ export function GetWebPublishStatus():Promise<main.WebPublishStatus>;
export function GetWhatsNew():Promise<Array<main.ChangelogEntry>>;
export function GetWhoHearsMe():Promise<Array<pskrme.Report>>;
export function GetWinkeyerSettings():Promise<main.WinkeyerSettings>;
export function GetWinkeyerStatus():Promise<winkeyer.Status>;
@@ -1235,6 +1242,8 @@ export function SetDVKLabel(arg1:number,arg2:string):Promise<void>;
export function SetFlexRSTChaseEnabled(arg1:boolean):Promise<void>;
export function SetHearMe(arg1:boolean):Promise<void>;
export function SetKenwoodAFGain(arg1:number):Promise<void>;
export function SetKenwoodAGC(arg1:string):Promise<void>;
+16
View File
@@ -966,6 +966,14 @@ export function GetGridScopeSettings() {
return window['go']['main']['App']['GetGridScopeSettings']();
}
export function GetHearMe() {
return window['go']['main']['App']['GetHearMe']();
}
export function GetHearMeStatus() {
return window['go']['main']['App']['GetHearMeStatus']();
}
export function GetIcomState() {
return window['go']['main']['App']['GetIcomState']();
}
@@ -1266,6 +1274,10 @@ export function GetWhatsNew() {
return window['go']['main']['App']['GetWhatsNew']();
}
export function GetWhoHearsMe() {
return window['go']['main']['App']['GetWhoHearsMe']();
}
export function GetWinkeyerSettings() {
return window['go']['main']['App']['GetWinkeyerSettings']();
}
@@ -2402,6 +2414,10 @@ export function SetFlexRSTChaseEnabled(arg1) {
return window['go']['main']['App']['SetFlexRSTChaseEnabled'](arg1);
}
export function SetHearMe(arg1) {
return window['go']['main']['App']['SetHearMe'](arg1);
}
export function SetKenwoodAFGain(arg1) {
return window['go']['main']['App']['SetKenwoodAFGain'](arg1);
}
+68
View File
@@ -5451,6 +5451,74 @@ export namespace pskr {
}
export namespace pskrme {
export class Report {
call: string;
grid: string;
band: string;
mode: string;
snr: number;
freq_hz: number;
// Go type: time
at: any;
static createFrom(source: any = {}) {
return new Report(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.call = source["call"];
this.grid = source["grid"];
this.band = source["band"];
this.mode = source["mode"];
this.snr = source["snr"];
this.freq_hz = source["freq_hz"];
this.at = this.convertValues(source["at"], null);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class Status {
enabled: boolean;
online: boolean;
reports: number;
watching: string;
error: string;
static createFrom(source: any = {}) {
return new Status(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.enabled = source["enabled"];
this.online = source["online"];
this.reports = source["reports"];
this.watching = source["watching"];
this.error = source["error"];
}
}
}
export namespace pskrtgt {
export class Bin {