fix(sat): the per-band antennas reach the satellite slices
Settings ▸ FlexRadio stores the band→antenna map keyed by the band name
in capitals — that is what the panel writes and what the entry form reads
back. applySatRadio looked its two bands up through bandForHz, which
returns the band plan's own spelling ("70cm"), so both lookups missed,
both antennas came back empty, and the early return left the pass on
whichever antenna the radio was last used on. An operator with XVTA on
2 m and XVTB on 70 cm had configured exactly the thing being ignored, and
nothing said so: the downlink ran through the wrong transverter in
silence.
The key is now computed by flexBandAntKey, which exists so the next
caller cannot make the same mistake, and a test pins the case in the
direction that broke. Both outcomes are logged — the resolved antennas,
or the bands nothing was configured for — because from the outside a
setting never made and a lookup that missed look identical.
Alongside, three things the same pass made obvious:
- Tracking shows the satellite's own azimuth, elevation, distance and
altitude beside the frequencies. The strip held where the ANTENNA was
pointing but not where the bird was, which is what says whether a pass
is worth calling on. Elevation dims below the horizon so a satellite
followed before its lever cannot be read as workable.
- Both satellite lists in the settings are sorted by name with the
numbers taken as numbers. The available column followed the order
birds.json happens to be written in and the followed column the order
of the clicks, so finding one bird among sixteen meant reading all
sixteen.
- A followed satellite that has since been renamed is resolved through
the plan's aliases. LILACSAT-2 became LO-90, and the followed list is
stored as plain text, so the bird the operator had chosen appeared as
having no elements while the same satellite sat in the available
column under its new name. Resolved in GetSatSettings rather than
satSettings, which is read at startup before the plan is loaded.
This commit is contained in:
+43
-6
@@ -314,7 +314,12 @@ func (a *App) GetSatSettings() (SatSettings, error) {
|
||||
if a.settings == nil {
|
||||
return SatSettings{}, fmt.Errorf("db not initialized")
|
||||
}
|
||||
return a.satSettings(), nil
|
||||
out := a.satSettings()
|
||||
// Resolved HERE and not in satSettings, which is read during startup before
|
||||
// the frequency plan is loaded. Saving the panel writes the resolved list
|
||||
// back, so the rename settles itself the first time anything is changed.
|
||||
out.Favorites = a.satFavorites()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SaveSatSettings stores them.
|
||||
@@ -535,9 +540,8 @@ func (a *App) AddSatelliteElements(text string) (int, error) {
|
||||
// configuration problem into a satellite that "does not exist".
|
||||
func (a *App) GetSatelliteBirds() []SatBird {
|
||||
store, birds, _ := a.satParts()
|
||||
set := a.satSettings()
|
||||
fav := map[string]bool{}
|
||||
for _, n := range set.Favorites {
|
||||
for _, n := range a.satFavorites() {
|
||||
fav[strings.ToUpper(n)] = true
|
||||
}
|
||||
|
||||
@@ -665,15 +669,48 @@ func satElement(store *sat.Store, b sat.Bird) (sat.Element, bool) {
|
||||
|
||||
// ── Tracking ────────────────────────────────────────────────────────────────
|
||||
|
||||
// satFavorites is the followed list, with every name resolved to the one the
|
||||
// frequency plan uses now.
|
||||
//
|
||||
// A satellite is renamed when it is granted an OSCAR number — LILACSAT-2
|
||||
// became LO-90 — and the plan carries the old name as an alias. The followed
|
||||
// list, though, is stored as the plain text the operator picked: after such a
|
||||
// rename his own choice was listed as having no elements while the same bird
|
||||
// sat under its new name in the available column, so the satellite he had
|
||||
// chosen had quietly become a stranger.
|
||||
//
|
||||
// Deduplicated on the way out, because an operator who followed both spellings
|
||||
// must not now see the same bird twice.
|
||||
func (a *App) satFavorites() []string {
|
||||
names := a.satSettings().Favorites
|
||||
_, birds, _ := a.satParts()
|
||||
if birds == nil {
|
||||
return names
|
||||
}
|
||||
out := make([]string, 0, len(names))
|
||||
seen := map[string]bool{}
|
||||
for _, n := range names {
|
||||
if b, ok := birds.Find(n); ok {
|
||||
n = b.Name
|
||||
}
|
||||
k := strings.ToUpper(n)
|
||||
if seen[k] {
|
||||
continue
|
||||
}
|
||||
seen[k] = true
|
||||
out = append(out, n)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// satNames resolves the names the UI asked for, falling back to the favourites
|
||||
// and then to every planned bird we hold elements for.
|
||||
func (a *App) satNames(names []string) []string {
|
||||
if len(names) > 0 {
|
||||
return names
|
||||
}
|
||||
set := a.satSettings()
|
||||
if len(set.Favorites) > 0 {
|
||||
return set.Favorites
|
||||
if favs := a.satFavorites(); len(favs) > 0 {
|
||||
return favs
|
||||
}
|
||||
var out []string
|
||||
for _, b := range a.GetSatelliteBirds() {
|
||||
|
||||
+30
-6
@@ -96,8 +96,13 @@ type SatTrackStatus struct {
|
||||
Az float64 `json:"az"`
|
||||
El float64 `json:"el"`
|
||||
Visible bool `json:"visible"`
|
||||
Radio string `json:"radio"` // what the rig is doing: "sat", "downlink-only", ""
|
||||
Error string `json:"error"`
|
||||
// Where the satellite is, as opposed to where to point: an operator reads
|
||||
// the distance to know whether a pass is worth calling on, and the altitude
|
||||
// to know how long it will last.
|
||||
RangeKm float64 `json:"range_km"`
|
||||
AltKm float64 `json:"alt_km"`
|
||||
Radio string `json:"radio"` // what the rig is doing: "sat", "downlink-only", ""
|
||||
Error string `json:"error"`
|
||||
|
||||
// Where the antenna is. RotLive distinguishes a reading from the controller
|
||||
// from the last position it was TOLD to go to — a stuck rotator must not be
|
||||
@@ -350,6 +355,7 @@ func (a *App) satTrackStep(t *satTracker) {
|
||||
st.NominalDown, st.NominalUp = nominal, nomUp
|
||||
st.DownHz, st.UpHz = down, up
|
||||
st.Az, st.El, st.Visible = pos.Az, pos.El, visible
|
||||
st.RangeKm, st.AltKm = pos.RangeKm, pos.AltKm
|
||||
t.status = st
|
||||
t.mu.Unlock()
|
||||
|
||||
@@ -649,8 +655,20 @@ func satBandLetter(hz int64) string {
|
||||
return "K" // 24 GHz and above
|
||||
}
|
||||
|
||||
// applySatAntennas puts each satellite slice on the antenna configured for ITS
|
||||
// band.
|
||||
// flexBandAntKey is the key a frequency has in the per-band antenna and power
|
||||
// maps.
|
||||
//
|
||||
// Those maps are keyed by the band name UPPERCASED ("70CM"), because that is
|
||||
// how the settings panel writes them; bandForHz returns the band plan's own
|
||||
// spelling ("70cm"). Every other caller happened to uppercase on the way in,
|
||||
// the satellite tracker did not, and so it read an empty antenna out of a map
|
||||
// the operator had filled in — which is not a mistake worth making twice.
|
||||
func flexBandAntKey(hz int64) string {
|
||||
return strings.ToUpper(bandForHz(hz))
|
||||
}
|
||||
|
||||
// applySatRadio puts each satellite slice on the antenna configured for ITS
|
||||
// band, and sets the uplink tone.
|
||||
//
|
||||
// Settings ▸ FlexRadio already holds a per-band RX/TX antenna map, and it was
|
||||
// only ever applied by the entry form on a band change — to the active slice.
|
||||
@@ -681,11 +699,17 @@ func (a *App) applySatRadio(tp sat.Transponder) {
|
||||
}
|
||||
// The downlink is received, so it takes that band's RX antenna; the uplink
|
||||
// is transmitted, so it takes that band's TX antenna.
|
||||
rxAnt := m[bandForHz(tp.DownLo)].RX
|
||||
txAnt := m[bandForHz(tp.UpLo)].TX
|
||||
downBand, upBand := flexBandAntKey(tp.DownLo), flexBandAntKey(tp.UpLo)
|
||||
rxAnt := m[downBand].RX
|
||||
txAnt := m[upBand].TX
|
||||
if strings.TrimSpace(rxAnt) == "" && strings.TrimSpace(txAnt) == "" {
|
||||
// Worth a line: an operator who HAS configured the pair and still sees
|
||||
// the wrong antenna has no other way to tell a setting he never made
|
||||
// from a lookup that missed.
|
||||
applog.Printf("sat: no antenna configured for this pass (down %s, up %s)", downBand, upBand)
|
||||
return
|
||||
}
|
||||
applog.Printf("sat: antennas rx=%q (%s) tx=%q (%s)", rxAnt, downBand, txAnt, upBand)
|
||||
if err := a.cat.FlexDo(func(fc cat.FlexController) error {
|
||||
return fc.SatAntennas(rxAnt, txAnt)
|
||||
}); err != nil {
|
||||
|
||||
@@ -135,3 +135,26 @@ func TestSatSidebands(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The per-band antenna map is keyed by the UPPERCASED band name, because that
|
||||
// is what the settings panel writes. The satellite tracker looked its two bands
|
||||
// up with the band plan's own spelling, matched nothing, and ran the pass on
|
||||
// whichever antenna the radio was last left on — a 70 cm downlink through a 2 m
|
||||
// transverter, with no error anywhere. This pins the contract in the direction
|
||||
// that broke.
|
||||
func TestFlexBandAntKeyIsUppercased(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
hz int64
|
||||
want string
|
||||
}{
|
||||
{435_400_000, "70CM"}, // an FM bird's downlink
|
||||
{145_950_000, "2M"}, // its uplink
|
||||
{1_269_000_000, "23CM"}, // AO-92's L band
|
||||
{29_450_000, "10M"}, // AO-7 mode A
|
||||
{9_000_000_000, ""}, // nothing in the plan: no key, and no antenna
|
||||
} {
|
||||
if got := flexBandAntKey(c.hz); got != c.want {
|
||||
t.Errorf("flexBandAntKey(%d) = %q, want %q", c.hz, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -7,14 +7,20 @@
|
||||
"On the rotor dial, the beam no longer whips a full turn round the compass when the antenna crosses north. A rotator at 020° turned anticlockwise reports 020, 010, 000, 359, 358 … and the animation travelled from 20° to 359° the long way — a complete revolution on screen while the mast moved forty degrees the other way. The beam now follows an accumulated angle, so what is drawn is the way the antenna is actually turning. Both lobes of a bidirectional antenna are handled separately, since they cross north at different moments.",
|
||||
"The rotor widget stops claiming the antenna is turning every time the wind moves it. A threshold alone could not tell the difference — a gust pushes a beam well past four degrees and back, and each excursion counted, so Stop lit and went out all evening on an antenna that had not turned. What separates a rotation from the weather is not how far the reading moves but which way: a rotor under power advances, gust after gust reverses. A step now counts only when the previous one went the same way, so movement is announced one poll later on a mast that takes half a minute to cross a pass, and never at all on a windy afternoon.",
|
||||
"LilacSat-2 gets its FM transponder — 144.350 up, 437.200 down — and its proper name, LO-90. It was shipped with nothing but an APRS digipeater, because the transponder database marks that transponder inactive: it runs to an announced schedule rather than continuously, which from a database looks exactly like a dead one. The label says \"(scheduled)\" so nobody wonders why it is quiet. The generator now also lists the satellites it refused on that ground and kept nothing else for, so the next regeneration is not silent about them.",
|
||||
"A frequency OpsLog got wrong can now be corrected on a station that already has the satellite file. Until now the file was copied out on the first run and was the operator's from then on, so a mistake we shipped — LilacSat-2 with no FM transponder — had become their data and could never be mended. The plan now adds what is missing, brings up to date what was never edited, and leaves alone what was: an entry you corrected by hand outranks anything shipped, and the log names the ones it stood down on. The first run after this cannot tell the two apart, so it takes the shipped plan and copies your file to satellites.json.bak first — after that your edits are recognised precisely and survive every release."
|
||||
"A frequency OpsLog got wrong can now be corrected on a station that already has the satellite file. Until now the file was copied out on the first run and was the operator's from then on, so a mistake we shipped — LilacSat-2 with no FM transponder — had become their data and could never be mended. The plan now adds what is missing, brings up to date what was never edited, and leaves alone what was: an entry you corrected by hand outranks anything shipped, and the log names the ones it stood down on. The first run after this cannot tell the two apart, so it takes the shipped plan and copies your file to satellites.json.bak first — after that your edits are recognised precisely and survive every release.",
|
||||
"The per-band antennas are applied to the satellite slices at last. The map is keyed by the band name in capitals, the tracker looked its two bands up in lower case, and so it read no antenna at all out of a table the operator had filled in — a 70 cm downlink stayed on the 2 m transverter. The log now names the band and the antenna it resolved, so a setting never made can be told apart from a lookup that missed.",
|
||||
"Tracking now shows where the satellite is beside where the antenna points: azimuth, elevation, distance and altitude, in the same strip as the two frequencies. The elevation goes dim below the horizon, so a bird still being followed on its way up cannot be read as workable.",
|
||||
"The two satellite lists in the settings are sorted by name, numerically — AO-27, AO-91, AO-123. The left one came in the order the frequency file happens to be written and the right one in the order they were clicked, so finding one bird among sixteen meant reading all sixteen. A satellite renamed on getting its OSCAR number, as LILACSAT-2 became LO-90, is also recognised in a followed list that still holds the old name instead of being listed as having no elements."
|
||||
],
|
||||
"fr": [
|
||||
"SteppIR : un bouton Calibrer, à côté de Rétracter. Il amène chaque élément en butée pour que le contrôleur retrouve son zéro — le remède à une antenne qui s’accorde à la mauvaise longueur après une coupure en pleine course, après avoir poussé les éléments à la main, ou après un moteur qui a glissé. Cela prend plusieurs minutes et l’antenne est inutilisable jusqu’à la fin : une confirmation est donc demandée. Les contrôleurs Ultrabeam n’ont pas cette commande et le disent, au lieu de faire semblant. Rétracter les éléments existait déjà et s’explique maintenant : c’est la position de rangement, et le prochain accord les fait ressortir tout seuls.",
|
||||
"Sur le cadran du rotor, le faisceau ne fait plus un tour complet de la boussole quand l’antenne franchit le nord. Un rotor à 020° tourné dans le sens antihoraire annonce 020, 010, 000, 359, 358… et l’animation allait de 20° à 359° par le chemin long — une révolution complète à l’écran pendant que le pylône bougeait de quarante degrés dans l’autre sens. Le faisceau suit désormais un angle cumulé : ce qui est dessiné est le mouvement réel de l’antenne. Les deux lobes d’une antenne bidirectionnelle sont traités séparément, puisqu’ils ne franchissent pas le nord au même moment.",
|
||||
"Le widget rotor n’annonce plus une antenne en rotation chaque fois que le vent la bouge. Un simple seuil ne pouvait pas faire la différence — une rafale pousse une beam bien au-delà de quatre degrés puis la ramène, et chaque écart comptait : Stop s’allumait et s’éteignait toute la soirée sur une antenne qui n’avait pas tourné. Ce qui distingue une rotation de la météo n’est pas l’amplitude mais le sens : un rotor sous tension avance, une rafale revient. Un écart ne compte donc que si le précédent allait dans le même sens — la rotation est annoncée un relevé plus tard sur un pylône qui met trente secondes à traverser, et plus du tout par vent fort.",
|
||||
"LilacSat-2 récupère son transpondeur FM — 144,350 en montée, 437,200 en descente — et son vrai nom, LO-90. Il était livré avec un simple digipeater APRS, parce que la base de transpondeurs marque ce transpondeur comme inactif : il fonctionne selon un calendrier annoncé plutôt qu’en continu, ce qui, vu d’une base de données, ressemble exactement à un transpondeur mort. Le libellé indique « (scheduled) » pour que personne ne se demande pourquoi il est muet. Le générateur liste désormais aussi les satellites qu’il a écartés pour cette raison sans rien garder d’autre, afin que la prochaine régénération ne les passe plus sous silence.",
|
||||
"Une fréquence qu’OpsLog avait fausse peut désormais être corrigée sur une station qui possède déjà le fichier satellites. Jusqu’ici ce fichier était copié au premier lancement puis appartenait à l’opérateur, si bien qu’une erreur de notre part — LilacSat-2 sans transpondeur FM — devenait sa donnée et ne pouvait plus être réparée. Le plan ajoute maintenant ce qui manque, met à jour ce qui n’a jamais été modifié, et laisse intact ce qui l’a été : une entrée corrigée à la main prime sur tout ce qui est livré, et le journal nomme celles devant lesquelles il s’est effacé. Le premier lancement après ce changement ne peut pas distinguer les deux : il prend donc le plan livré et copie d’abord votre fichier en satellites.json.bak — ensuite vos modifications sont reconnues précisément et survivent à chaque version."
|
||||
"Une fréquence qu’OpsLog avait fausse peut désormais être corrigée sur une station qui possède déjà le fichier satellites. Jusqu’ici ce fichier était copié au premier lancement puis appartenait à l’opérateur, si bien qu’une erreur de notre part — LilacSat-2 sans transpondeur FM — devenait sa donnée et ne pouvait plus être réparée. Le plan ajoute maintenant ce qui manque, met à jour ce qui n’a jamais été modifié, et laisse intact ce qui l’a été : une entrée corrigée à la main prime sur tout ce qui est livré, et le journal nomme celles devant lesquelles il s’est effacé. Le premier lancement après ce changement ne peut pas distinguer les deux : il prend donc le plan livré et copie d’abord votre fichier en satellites.json.bak — ensuite vos modifications sont reconnues précisément et survivent à chaque version.",
|
||||
"Les antennes par bande sont enfin appliquées aux slices satellite. La table est indexée par le nom de bande en majuscules, le suivi cherchait ses deux bandes en minuscules, et il ne lisait donc aucune antenne dans un tableau que l’opérateur avait rempli — une descente 70 cm restait sur le transverter 2 m. Le journal indique désormais la bande et l’antenne retenues, pour distinguer un réglage jamais fait d’une recherche qui a échoué.",
|
||||
"Le suivi affiche désormais où est le satellite à côté de là où pointe l’antenne : azimut, élévation, distance et altitude, dans le même bandeau que les deux fréquences. L’élévation s’estompe sous l’horizon, pour qu’un satellite encore suivi avant son lever ne soit pas pris pour un satellite travaillable.",
|
||||
"Les deux listes de satellites des réglages sont triées par nom, en tenant compte des nombres — AO-27, AO-91, AO-123. Celle de gauche suivait l’ordre du fichier de fréquences et celle de droite l’ordre des clics : retrouver un satellite parmi seize obligeait à lire les seize. Un satellite renommé lors de l’attribution de son numéro OSCAR, comme LILACSAT-2 devenu LO-90, est aussi reconnu dans une liste de suivis qui porte encore l’ancien nom, au lieu d’y apparaître sans éléments."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -57,7 +57,7 @@ type Tuning = {
|
||||
type Track = {
|
||||
on: boolean; name: string; transponder: string; mode: string;
|
||||
nominal_down: number; nominal_up: number; down_hz: number; up_hz: number;
|
||||
az: number; el: number; visible: boolean;
|
||||
az: number; el: number; visible: boolean; range_km: number; alt_km: number;
|
||||
radio: string; // "sat" | "downlink-only" | ""
|
||||
error: string;
|
||||
rot_on: boolean; rot_az: number; rot_el: number; rot_live: boolean; rot_az_only: boolean;
|
||||
@@ -719,7 +719,28 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
first thing they hide to get the map full width. */}
|
||||
{tracking?.on && (
|
||||
<div className="flex items-center gap-2.5 rounded-md border border-border bg-card/60 px-2 py-0.5 text-xs tabular-nums">
|
||||
<span className="flex items-center gap-1" title={t('sat.down')}>
|
||||
{/* Where the bird IS, which is not where the antenna is pointing:
|
||||
these three say whether the pass is worth calling on, and the
|
||||
rotator group further along says whether the mast has caught up
|
||||
with them. Elevation goes dim below the horizon, so a satellite
|
||||
still being tracked on its way up cannot be read as workable. */}
|
||||
<span className="flex items-center gap-1.5" title={`${t('sat.tipAz')} / ${t('sat.tipEl')}`}>
|
||||
<Radar className="size-3 text-muted-foreground" />
|
||||
<span className={cn('font-medium', !tracking.visible && 'text-muted-foreground')}>
|
||||
{Math.round(tracking.az)}° / {tracking.el.toFixed(1)}°
|
||||
</span>
|
||||
</span>
|
||||
{tracking.range_km > 0 && (
|
||||
<span className="text-muted-foreground" title={t('sat.range')}>
|
||||
{Math.round(tracking.range_km).toLocaleString()} km
|
||||
</span>
|
||||
)}
|
||||
{tracking.alt_km > 0 && (
|
||||
<span className="text-muted-foreground" title={t('sat.altitude')}>
|
||||
↑{Math.round(tracking.alt_km).toLocaleString()} km
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1 border-l border-border pl-2.5" title={t('sat.down')}>
|
||||
<ArrowDown className="size-3 text-muted-foreground" />
|
||||
<span className="font-medium">{fmtHz(tracking.down_hz)}</span>
|
||||
</span>
|
||||
|
||||
@@ -1468,6 +1468,13 @@ function SatelliteElementsBlock({ autoTle, onAutoTle }: { autoTle: boolean; onAu
|
||||
// Following none means following every satellite that has both elements and a
|
||||
// frequency plan, which is the sensible thing for somebody who has not chosen
|
||||
// yet and the reason the list does not start out empty-handed.
|
||||
// byCallsign orders satellite names the way an operator reads them: alphabetical,
|
||||
// but with the number taken as a number. Plain string order files AO-123 between
|
||||
// AO-1 and AO-27, which is not where anybody looks for it.
|
||||
function byCallsign(a: { name?: string }, b: { name?: string }) {
|
||||
return String(a?.name ?? '').localeCompare(String(b?.name ?? ''), undefined, { numeric: true, sensitivity: 'base' });
|
||||
}
|
||||
|
||||
function SatelliteFollowList({ followed, onChange }: { followed: string[]; onChange: (next: string[]) => void }) {
|
||||
const { t } = useI18n();
|
||||
const [all, setAll] = useState<any[]>([]);
|
||||
@@ -1485,8 +1492,13 @@ function SatelliteFollowList({ followed, onChange }: { followed: string[]; onCha
|
||||
const needle = q.trim().toLowerCase();
|
||||
const available = all.filter((b) => !followedSet.has(String(b.name).toUpperCase())
|
||||
&& (!withPlanOnly || (b.transponders?.length ?? 0) > 0)
|
||||
&& (needle === '' || String(b.name).toLowerCase().includes(needle)));
|
||||
const chosen = followed.map((n) => byName.get(n) ?? { name: n, has_elements: false, transponders: [] });
|
||||
&& (needle === '' || String(b.name).toLowerCase().includes(needle))).sort(byCallsign);
|
||||
// Sorted, both columns: the left one came in the order the frequency file
|
||||
// happens to be written and the right one in the order the operator clicked,
|
||||
// so finding AO-91 among sixteen followed birds meant reading all sixteen.
|
||||
const chosen = followed
|
||||
.map((n) => byName.get(n) ?? { name: n, has_elements: false, transponders: [] })
|
||||
.sort(byCallsign);
|
||||
|
||||
const label = (b: any) => {
|
||||
const bits: string[] = [];
|
||||
|
||||
@@ -4282,6 +4282,8 @@ export namespace main {
|
||||
az: number;
|
||||
el: number;
|
||||
visible: boolean;
|
||||
range_km: number;
|
||||
alt_km: number;
|
||||
radio: string;
|
||||
error: string;
|
||||
rot_on: boolean;
|
||||
@@ -4307,6 +4309,8 @@ export namespace main {
|
||||
this.az = source["az"];
|
||||
this.el = source["el"];
|
||||
this.visible = source["visible"];
|
||||
this.range_km = source["range_km"];
|
||||
this.alt_km = source["alt_km"];
|
||||
this.radio = source["radio"];
|
||||
this.error = source["error"];
|
||||
this.rot_on = source["rot_on"];
|
||||
|
||||
Reference in New Issue
Block a user