@springfield/ham-radio-api 17.8.0 → 17.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/antenna/type.d.ts +89 -0
- package/dist/antenna/type.d.ts.map +1 -0
- package/dist/antenna/type.js +265 -0
- package/dist/antenna/type.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/station/antenna.d.ts +25 -0
- package/dist/station/antenna.d.ts.map +1 -0
- package/dist/station/antenna.js +3 -0
- package/dist/station/antenna.js.map +1 -0
- package/dist/station/radio-station.d.ts +32 -0
- package/dist/station/radio-station.d.ts.map +1 -0
- package/dist/station/radio-station.js +9 -0
- package/dist/station/radio-station.js.map +1 -0
- package/package.json +2 -1
- package/src/antenna/type.ts +326 -0
- package/src/index.ts +5 -0
- package/src/station/antenna.ts +26 -0
- package/src/station/radio-station.ts +34 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ham-radio-api
|
|
2
2
|
|
|
3
|
-
Shared TypeScript types and schemas for HamBench: protocol DSL, memory maps, serial config, branded frequencies,
|
|
3
|
+
Shared TypeScript types and schemas for HamBench: protocol DSL, memory maps, serial config, branded frequencies, license types, stations, and antennas.
|
|
4
4
|
|
|
5
5
|
Packages that talk to radios depend on this library. It does not open a serial port.
|
|
6
6
|
|
|
@@ -10,4 +10,5 @@ Docs: [HamBench developer docs](https://springfield-ham-radio.github.io/ham-radi
|
|
|
10
10
|
corepack enable
|
|
11
11
|
yarn install
|
|
12
12
|
yarn build
|
|
13
|
+
yarn test
|
|
13
14
|
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Wavelength } from "../branded-types/wavelength.js";
|
|
2
|
+
export type AntennaTypeId = "dipole" | "inverted-v" | "quarter-wave-vertical" | "yagi-3el" | "magloop" | "end-fed" | "dual-band-vertical" | "vhf-yagi" | "uhf-yagi";
|
|
3
|
+
/** How energy is distributed in azimuth. Heading is unused for omni. */
|
|
4
|
+
export type AntennaPattern = "omni" | "bidirectional" | "directive";
|
|
5
|
+
/** Coarse elevation character. Skip rings use type plus height, not this alone. */
|
|
6
|
+
export type AntennaTakeoff = "high" | "medium" | "low";
|
|
7
|
+
export interface AntennaType {
|
|
8
|
+
id: AntennaTypeId;
|
|
9
|
+
label: string;
|
|
10
|
+
description: string;
|
|
11
|
+
pattern: AntennaPattern;
|
|
12
|
+
/**
|
|
13
|
+
* Typical peak gain in dBi. Catalog estimate for a generic of this family,
|
|
14
|
+
* not a manufacturer measurement or NEC run.
|
|
15
|
+
*/
|
|
16
|
+
gainDbi: number;
|
|
17
|
+
/** Azimuth beamwidth of the main lobe in degrees. Omni is 360. */
|
|
18
|
+
beamwidthDeg: number;
|
|
19
|
+
/** Typical front-to-back ratio in dB. Zero for omni and bidirectional. */
|
|
20
|
+
frontToBackDb: number;
|
|
21
|
+
takeoff: AntennaTakeoff;
|
|
22
|
+
/** Ham bands this family typically covers, as SpectrumBand.wavelength. */
|
|
23
|
+
defaultBands: Wavelength[];
|
|
24
|
+
defaultHeightAglM: number;
|
|
25
|
+
/** True heading of maximum radiation when the type is not omni. */
|
|
26
|
+
defaultHeadingDeg: number;
|
|
27
|
+
/** Dipoles, inverted-Vs, and HF beams can use LC traps for extra bands. */
|
|
28
|
+
supportsTraps: boolean;
|
|
29
|
+
/** Tribanders start trapped; a single-band dipole does not. */
|
|
30
|
+
defaultTrapped: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Built-in generic antenna families. Commercial SKUs can parameterize these later.
|
|
34
|
+
*/
|
|
35
|
+
export declare const ANTENNA_TYPES: readonly AntennaType[];
|
|
36
|
+
/**
|
|
37
|
+
* Returns whether `value` is a built-in antenna type id.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isAntennaTypeId(value: unknown): value is AntennaTypeId;
|
|
40
|
+
/**
|
|
41
|
+
* Looks up a built-in antenna type, or undefined when the id is unknown.
|
|
42
|
+
*/
|
|
43
|
+
export declare function antennaTypeById(id: AntennaTypeId | string | undefined): AntennaType | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* True when the operator should enter a heading (not omnidirectional).
|
|
46
|
+
*/
|
|
47
|
+
export declare function antennaTypeUsesHeading(type: AntennaType): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* True for dipoles, inverted-Vs, and HF Yagis — not VHF/UHF beams or omni verticals.
|
|
50
|
+
*/
|
|
51
|
+
export declare function antennaTypeSupportsTraps(type: AntennaType): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* HF / 6 m. 2 m (2) and 70 cm (0.7) are VHF/UHF.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isHfAntennaBand(wavelength: Wavelength): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Catalog order: longest wavelength first.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sortAntennaBands(bands: readonly Wavelength[]): Wavelength[];
|
|
60
|
+
/**
|
|
61
|
+
* True when every tagged band is VHF/UHF FM (2 m / 70 cm), not HF skywave.
|
|
62
|
+
*/
|
|
63
|
+
export declare function antennaBandsAreLineOfSight(bands: readonly Wavelength[]): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Trap resonant frequencies: every tagged HF band except the lowest (the full-size fundamental).
|
|
66
|
+
*
|
|
67
|
+
* An 80/40 dipole traps at 40 m. A 20/15/10 tribander traps at 15 m and 10 m.
|
|
68
|
+
*/
|
|
69
|
+
export declare function trapResonantBands(bands: readonly Wavelength[]): Wavelength[];
|
|
70
|
+
/**
|
|
71
|
+
* True when this installation uses traps on two or more HF bands.
|
|
72
|
+
*/
|
|
73
|
+
export declare function antennaIsTrapped(type: AntennaType, trapped: boolean | undefined, bands: readonly Wavelength[]): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* When traps are turned on with only one HF band, add the next-lower ham band as the fundamental
|
|
76
|
+
* (40 m → 80/40). If the tag is already 160 m, add 80 m as the trap frequency.
|
|
77
|
+
*/
|
|
78
|
+
export declare function bandsForEnabledTraps(bands: readonly Wavelength[]): Wavelength[];
|
|
79
|
+
/**
|
|
80
|
+
* Peak gain with a small catalog derate when traps are in the elements.
|
|
81
|
+
*/
|
|
82
|
+
export declare function estimateGainDbi(type: AntennaType, trapped: boolean): number;
|
|
83
|
+
/**
|
|
84
|
+
* Heuristic takeoff of the main lobe in degrees, from type and height in wavelengths.
|
|
85
|
+
*
|
|
86
|
+
* This is a catalog rule of thumb for skip rings, not an EZNEC elevation plot.
|
|
87
|
+
*/
|
|
88
|
+
export declare function estimateTakeoffDeg(type: AntennaType, heightAglM: number, wavelength?: Wavelength): number;
|
|
89
|
+
//# sourceMappingURL=type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../src/antenna/type.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAE5D,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,YAAY,GAAG,uBAAuB,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,oBAAoB,GAAG,UAAU,GAAG,UAAU,CAAC;AAEpK,wEAAwE;AACxE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,eAAe,GAAG,WAAW,CAAC;AAEpE,mFAAmF;AACnF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,aAAa,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,cAAc,CAAC;IACxB,0EAA0E;IAC1E,YAAY,EAAE,UAAU,EAAE,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,aAAa,EAAE,OAAO,CAAC;IACvB,+DAA+D;IAC/D,cAAc,EAAE,OAAO,CAAC;CACzB;AAqBD;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAwI/C,CAAC;AAOF;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAEtE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAM/F;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAEjE;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAEnE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAE/D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,EAAE,CAE3E;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAEhF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,EAAE,CAI5E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAEvH;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,EAAE,CAe/E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAM3E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,GAAE,UAAkD,GAAG,MAAM,CAYhJ"}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { Wavelength } from "../branded-types/wavelength.js";
|
|
2
|
+
const WAVELENGTH_160 = Wavelength(160);
|
|
3
|
+
const WAVELENGTH_80 = Wavelength(80);
|
|
4
|
+
const WAVELENGTH_40 = Wavelength(40);
|
|
5
|
+
const WAVELENGTH_30 = Wavelength(30);
|
|
6
|
+
const WAVELENGTH_20 = Wavelength(20);
|
|
7
|
+
const WAVELENGTH_17 = Wavelength(17);
|
|
8
|
+
const WAVELENGTH_15 = Wavelength(15);
|
|
9
|
+
const WAVELENGTH_12 = Wavelength(12);
|
|
10
|
+
const WAVELENGTH_10 = Wavelength(10);
|
|
11
|
+
const WAVELENGTH_6 = Wavelength(6);
|
|
12
|
+
const WAVELENGTH_2 = Wavelength(2);
|
|
13
|
+
const WAVELENGTH_70_CM = Wavelength(0.7);
|
|
14
|
+
/** HF + 6 m wavelengths, longest first. Matches amateur SpectrumBand.wavelength values. */
|
|
15
|
+
const HF_WAVELENGTHS = [WAVELENGTH_160, WAVELENGTH_80, WAVELENGTH_40, WAVELENGTH_30, WAVELENGTH_20, WAVELENGTH_17, WAVELENGTH_15, WAVELENGTH_12, WAVELENGTH_10, WAVELENGTH_6];
|
|
16
|
+
const HF_40_TO_10 = [WAVELENGTH_40, WAVELENGTH_30, WAVELENGTH_20, WAVELENGTH_17, WAVELENGTH_15, WAVELENGTH_12, WAVELENGTH_10];
|
|
17
|
+
const VHF_UHF = [WAVELENGTH_2, WAVELENGTH_70_CM];
|
|
18
|
+
/**
|
|
19
|
+
* Built-in generic antenna families. Commercial SKUs can parameterize these later.
|
|
20
|
+
*/
|
|
21
|
+
export const ANTENNA_TYPES = [
|
|
22
|
+
{
|
|
23
|
+
id: "dipole",
|
|
24
|
+
label: "Dipole",
|
|
25
|
+
description: "Half-wave center-fed wire. Heading is the broadside direction of maximum radiation. Enable traps for a multi-band (for example 80/40) dipole.",
|
|
26
|
+
pattern: "bidirectional",
|
|
27
|
+
gainDbi: 2.15,
|
|
28
|
+
beamwidthDeg: 78,
|
|
29
|
+
frontToBackDb: 0,
|
|
30
|
+
takeoff: "medium",
|
|
31
|
+
defaultBands: [WAVELENGTH_40],
|
|
32
|
+
defaultHeightAglM: 10,
|
|
33
|
+
defaultHeadingDeg: 45,
|
|
34
|
+
supportsTraps: true,
|
|
35
|
+
defaultTrapped: false,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "inverted-v",
|
|
39
|
+
label: "Inverted-V",
|
|
40
|
+
description: "Center-supported dipole with sloping legs. Higher takeoff than a flat-top at the same apex. Enable traps for a multi-band wire.",
|
|
41
|
+
pattern: "bidirectional",
|
|
42
|
+
gainDbi: 1.5,
|
|
43
|
+
beamwidthDeg: 90,
|
|
44
|
+
frontToBackDb: 0,
|
|
45
|
+
takeoff: "high",
|
|
46
|
+
defaultBands: [WAVELENGTH_40],
|
|
47
|
+
defaultHeightAglM: 8,
|
|
48
|
+
defaultHeadingDeg: 45,
|
|
49
|
+
supportsTraps: true,
|
|
50
|
+
defaultTrapped: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "quarter-wave-vertical",
|
|
54
|
+
label: "Quarter-wave vertical",
|
|
55
|
+
description: "Ground-mounted or elevated vertical. Omnidirectional; heading is not used.",
|
|
56
|
+
pattern: "omni",
|
|
57
|
+
gainDbi: 1.5,
|
|
58
|
+
beamwidthDeg: 360,
|
|
59
|
+
frontToBackDb: 0,
|
|
60
|
+
takeoff: "medium",
|
|
61
|
+
defaultBands: [WAVELENGTH_40],
|
|
62
|
+
defaultHeightAglM: 2,
|
|
63
|
+
defaultHeadingDeg: 0,
|
|
64
|
+
supportsTraps: false,
|
|
65
|
+
defaultTrapped: false,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "yagi-3el",
|
|
69
|
+
label: "3-element Yagi",
|
|
70
|
+
description: "Directive trap tribander (typically 20/15/10). Heading is the boom direction of maximum gain.",
|
|
71
|
+
pattern: "directive",
|
|
72
|
+
gainDbi: 7,
|
|
73
|
+
beamwidthDeg: 66,
|
|
74
|
+
frontToBackDb: 20,
|
|
75
|
+
takeoff: "low",
|
|
76
|
+
defaultBands: [WAVELENGTH_20, WAVELENGTH_15, WAVELENGTH_10],
|
|
77
|
+
defaultHeightAglM: 15,
|
|
78
|
+
defaultHeadingDeg: 45,
|
|
79
|
+
supportsTraps: true,
|
|
80
|
+
defaultTrapped: true,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "magloop",
|
|
84
|
+
label: "Magnetic loop",
|
|
85
|
+
description: "Small transmitting loop. Roughly omnidirectional; efficiency is lower than a full-size wire.",
|
|
86
|
+
pattern: "omni",
|
|
87
|
+
gainDbi: 0,
|
|
88
|
+
beamwidthDeg: 360,
|
|
89
|
+
frontToBackDb: 0,
|
|
90
|
+
takeoff: "high",
|
|
91
|
+
defaultBands: HF_40_TO_10,
|
|
92
|
+
defaultHeightAglM: 2,
|
|
93
|
+
defaultHeadingDeg: 0,
|
|
94
|
+
supportsTraps: false,
|
|
95
|
+
defaultTrapped: false,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "end-fed",
|
|
99
|
+
label: "End-fed",
|
|
100
|
+
description: "End-fed half-wave or random wire. Heading is the broadside of the wire.",
|
|
101
|
+
pattern: "bidirectional",
|
|
102
|
+
gainDbi: 2,
|
|
103
|
+
beamwidthDeg: 90,
|
|
104
|
+
frontToBackDb: 0,
|
|
105
|
+
takeoff: "medium",
|
|
106
|
+
defaultBands: HF_40_TO_10,
|
|
107
|
+
defaultHeightAglM: 10,
|
|
108
|
+
defaultHeadingDeg: 45,
|
|
109
|
+
supportsTraps: false,
|
|
110
|
+
defaultTrapped: false,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "dual-band-vertical",
|
|
114
|
+
label: "Dual-band vertical",
|
|
115
|
+
description: "2 m / 70 cm omnidirectional (J-pole, collinear, or dual-band whip). Heading is not used.",
|
|
116
|
+
pattern: "omni",
|
|
117
|
+
gainDbi: 4.5,
|
|
118
|
+
beamwidthDeg: 360,
|
|
119
|
+
frontToBackDb: 0,
|
|
120
|
+
takeoff: "low",
|
|
121
|
+
defaultBands: VHF_UHF,
|
|
122
|
+
defaultHeightAglM: 8,
|
|
123
|
+
defaultHeadingDeg: 0,
|
|
124
|
+
supportsTraps: false,
|
|
125
|
+
defaultTrapped: false,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "vhf-yagi",
|
|
129
|
+
label: "2 m Yagi",
|
|
130
|
+
description: "Directive 2 m beam. Heading is the boom direction of maximum gain.",
|
|
131
|
+
pattern: "directive",
|
|
132
|
+
gainDbi: 10,
|
|
133
|
+
beamwidthDeg: 42,
|
|
134
|
+
frontToBackDb: 18,
|
|
135
|
+
takeoff: "low",
|
|
136
|
+
defaultBands: [WAVELENGTH_2],
|
|
137
|
+
defaultHeightAglM: 10,
|
|
138
|
+
defaultHeadingDeg: 45,
|
|
139
|
+
supportsTraps: false,
|
|
140
|
+
defaultTrapped: false,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "uhf-yagi",
|
|
144
|
+
label: "70 cm Yagi",
|
|
145
|
+
description: "Directive 70 cm beam. Heading is the boom direction of maximum gain.",
|
|
146
|
+
pattern: "directive",
|
|
147
|
+
gainDbi: 12,
|
|
148
|
+
beamwidthDeg: 36,
|
|
149
|
+
frontToBackDb: 18,
|
|
150
|
+
takeoff: "low",
|
|
151
|
+
defaultBands: [WAVELENGTH_70_CM],
|
|
152
|
+
defaultHeightAglM: 10,
|
|
153
|
+
defaultHeadingDeg: 45,
|
|
154
|
+
supportsTraps: false,
|
|
155
|
+
defaultTrapped: false,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
/** Catalog loss for LC traps. Not a manufacturer efficiency spec. */
|
|
159
|
+
const TRAP_GAIN_LOSS_DBI = 0.5;
|
|
160
|
+
const ANTENNA_TYPE_IDS = new Set(ANTENNA_TYPES.map((type) => type.id));
|
|
161
|
+
/**
|
|
162
|
+
* Returns whether `value` is a built-in antenna type id.
|
|
163
|
+
*/
|
|
164
|
+
export function isAntennaTypeId(value) {
|
|
165
|
+
return typeof value === "string" && ANTENNA_TYPE_IDS.has(value);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Looks up a built-in antenna type, or undefined when the id is unknown.
|
|
169
|
+
*/
|
|
170
|
+
export function antennaTypeById(id) {
|
|
171
|
+
if (!id) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
return ANTENNA_TYPES.find((type) => type.id === id);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* True when the operator should enter a heading (not omnidirectional).
|
|
178
|
+
*/
|
|
179
|
+
export function antennaTypeUsesHeading(type) {
|
|
180
|
+
return type.pattern !== "omni";
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* True for dipoles, inverted-Vs, and HF Yagis — not VHF/UHF beams or omni verticals.
|
|
184
|
+
*/
|
|
185
|
+
export function antennaTypeSupportsTraps(type) {
|
|
186
|
+
return type.supportsTraps;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* HF / 6 m. 2 m (2) and 70 cm (0.7) are VHF/UHF.
|
|
190
|
+
*/
|
|
191
|
+
export function isHfAntennaBand(wavelength) {
|
|
192
|
+
return wavelength >= 6;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Catalog order: longest wavelength first.
|
|
196
|
+
*/
|
|
197
|
+
export function sortAntennaBands(bands) {
|
|
198
|
+
return [...new Set(bands)].sort((left, right) => right - left);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* True when every tagged band is VHF/UHF FM (2 m / 70 cm), not HF skywave.
|
|
202
|
+
*/
|
|
203
|
+
export function antennaBandsAreLineOfSight(bands) {
|
|
204
|
+
return bands.length > 0 && bands.every((wavelength) => wavelength <= 2);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Trap resonant frequencies: every tagged HF band except the lowest (the full-size fundamental).
|
|
208
|
+
*
|
|
209
|
+
* An 80/40 dipole traps at 40 m. A 20/15/10 tribander traps at 15 m and 10 m.
|
|
210
|
+
*/
|
|
211
|
+
export function trapResonantBands(bands) {
|
|
212
|
+
const hf = sortAntennaBands(bands).filter(isHfAntennaBand);
|
|
213
|
+
return hf.slice(1);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* True when this installation uses traps on two or more HF bands.
|
|
217
|
+
*/
|
|
218
|
+
export function antennaIsTrapped(type, trapped, bands) {
|
|
219
|
+
return type.supportsTraps && trapped === true && trapResonantBands(bands).length > 0;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* When traps are turned on with only one HF band, add the next-lower ham band as the fundamental
|
|
223
|
+
* (40 m → 80/40). If the tag is already 160 m, add 80 m as the trap frequency.
|
|
224
|
+
*/
|
|
225
|
+
export function bandsForEnabledTraps(bands) {
|
|
226
|
+
if (trapResonantBands(bands).length > 0) {
|
|
227
|
+
return sortAntennaBands(bands);
|
|
228
|
+
}
|
|
229
|
+
const present = HF_WAVELENGTHS.filter((wavelength) => bands.includes(wavelength));
|
|
230
|
+
const current = present[0] ?? WAVELENGTH_40;
|
|
231
|
+
const index = HF_WAVELENGTHS.indexOf(current);
|
|
232
|
+
const added = index > 0 ? HF_WAVELENGTHS[index - 1] : HF_WAVELENGTHS[index + 1];
|
|
233
|
+
if (!added) {
|
|
234
|
+
return sortAntennaBands(bands.length > 0 ? bands : [current]);
|
|
235
|
+
}
|
|
236
|
+
return sortAntennaBands([...bands, current, added]);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Peak gain with a small catalog derate when traps are in the elements.
|
|
240
|
+
*/
|
|
241
|
+
export function estimateGainDbi(type, trapped) {
|
|
242
|
+
if (!trapped) {
|
|
243
|
+
return type.gainDbi;
|
|
244
|
+
}
|
|
245
|
+
return Math.round((type.gainDbi - TRAP_GAIN_LOSS_DBI) * 100) / 100;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Heuristic takeoff of the main lobe in degrees, from type and height in wavelengths.
|
|
249
|
+
*
|
|
250
|
+
* This is a catalog rule of thumb for skip rings, not an EZNEC elevation plot.
|
|
251
|
+
*/
|
|
252
|
+
export function estimateTakeoffDeg(type, heightAglM, wavelength = type.defaultBands[0] ?? WAVELENGTH_40) {
|
|
253
|
+
const heights = Math.max(0.1, heightAglM / wavelength);
|
|
254
|
+
if (type.takeoff === "high") {
|
|
255
|
+
return clamp(Math.round(52 - heights * 10), 25, 70);
|
|
256
|
+
}
|
|
257
|
+
if (type.takeoff === "low") {
|
|
258
|
+
return clamp(Math.round(24 - heights * 8), 6, 25);
|
|
259
|
+
}
|
|
260
|
+
return clamp(Math.round(36 - heights * 12), 10, 45);
|
|
261
|
+
}
|
|
262
|
+
function clamp(value, min, max) {
|
|
263
|
+
return Math.min(max, Math.max(min, value));
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type.js","sourceRoot":"","sources":["../../src/antenna/type.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -37,4 +37,7 @@ export * from "./spectrum/band.js";
|
|
|
37
37
|
export * from "./spectrum/channel.js";
|
|
38
38
|
export * from "./spectrum/mode.js";
|
|
39
39
|
export * from "./spectrum/privilege.js";
|
|
40
|
+
export * from "./antenna/type.js";
|
|
41
|
+
export * from "./station/radio-station.js";
|
|
42
|
+
export * from "./station/antenna.js";
|
|
40
43
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,0CAA0C,CAAC;AACzD,cAAc,+BAA+B,CAAC;AAE9C,cAAc,oBAAoB,CAAC;AAEnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AAEtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,0CAA0C,CAAC;AACzD,cAAc,+BAA+B,CAAC;AAE9C,cAAc,oBAAoB,CAAC;AAEnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AAEtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AAExC,cAAc,mBAAmB,CAAC;AAElC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -37,4 +37,7 @@ export * from "./spectrum/band.js";
|
|
|
37
37
|
export * from "./spectrum/channel.js";
|
|
38
38
|
export * from "./spectrum/mode.js";
|
|
39
39
|
export * from "./spectrum/privilege.js";
|
|
40
|
+
export * from "./antenna/type.js";
|
|
41
|
+
export * from "./station/radio-station.js";
|
|
42
|
+
export * from "./station/antenna.js";
|
|
40
43
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,0CAA0C,CAAC;AACzD,cAAc,+BAA+B,CAAC;AAE9C,cAAc,oBAAoB,CAAC;AAEnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AAEtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,0CAA0C,CAAC;AACzD,cAAc,+BAA+B,CAAC;AAE9C,cAAc,oBAAoB,CAAC;AAEnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AAEtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AAExC,cAAc,mBAAmB,CAAC;AAElC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Wavelength } from "../branded-types/wavelength.js";
|
|
2
|
+
import type { AntennaTypeId } from "../antenna/type.js";
|
|
3
|
+
export declare const MIN_ANTENNA_HEIGHT_AGL_M = 0.5;
|
|
4
|
+
export declare const MAX_ANTENNA_HEIGHT_AGL_M = 120;
|
|
5
|
+
/**
|
|
6
|
+
* One physical antenna at one station.
|
|
7
|
+
*
|
|
8
|
+
* The same radiator at two sites is two records. `bands` are ham
|
|
9
|
+
* {@link SpectrumBand.wavelength} values, not a second band catalog.
|
|
10
|
+
*/
|
|
11
|
+
export interface StationAntenna {
|
|
12
|
+
id: string;
|
|
13
|
+
stationId: string;
|
|
14
|
+
nickname: string;
|
|
15
|
+
typeId: AntennaTypeId;
|
|
16
|
+
heightAglM: number;
|
|
17
|
+
/** True heading of maximum radiation, 0–359. Omitted for omni types. */
|
|
18
|
+
headingDeg?: number;
|
|
19
|
+
bands: Wavelength[];
|
|
20
|
+
/** LC traps in the elements. Omitted when the type is a single-band or trapless. */
|
|
21
|
+
trapped?: boolean;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
updatedAt: number;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=antenna.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"antenna.d.ts","sourceRoot":"","sources":["../../src/station/antenna.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAC5C,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,oFAAoF;IACpF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"antenna.js","sourceRoot":"","sources":["../../src/station/antenna.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAC5C,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Home site id. HamBench always has at least this station.
|
|
3
|
+
*/
|
|
4
|
+
export declare const HOME_STATION_ID = "station-home";
|
|
5
|
+
export declare const MIN_LATITUDE = -90;
|
|
6
|
+
export declare const MAX_LATITUDE = 90;
|
|
7
|
+
export declare const MIN_LONGITUDE = -180;
|
|
8
|
+
export declare const MAX_LONGITUDE = 180;
|
|
9
|
+
/**
|
|
10
|
+
* Whether the operator last edited the Maidenhead grid or the coordinates.
|
|
11
|
+
*
|
|
12
|
+
* Editing the grid sets lat/lon to the cell center. Editing coordinates keeps
|
|
13
|
+
* that point and derives a 6-character grid.
|
|
14
|
+
*/
|
|
15
|
+
export type StationLocationSource = "grid" | "coordinates";
|
|
16
|
+
/**
|
|
17
|
+
* An operating site (Home, Cabin, Portable), not a log STATION_CALLSIGN.
|
|
18
|
+
*
|
|
19
|
+
* Latitude and longitude are signed decimal degrees (south / west negative).
|
|
20
|
+
* UIs may collect them as degrees, minutes, and decimal seconds.
|
|
21
|
+
*/
|
|
22
|
+
export interface RadioStation {
|
|
23
|
+
id: string;
|
|
24
|
+
nickname: string;
|
|
25
|
+
gridsquare?: string;
|
|
26
|
+
latitude?: number;
|
|
27
|
+
longitude?: number;
|
|
28
|
+
locationSource?: StationLocationSource;
|
|
29
|
+
createdAt: number;
|
|
30
|
+
updatedAt: number;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=radio-station.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"radio-station.d.ts","sourceRoot":"","sources":["../../src/station/radio-station.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAE9C,eAAO,MAAM,YAAY,MAAM,CAAC;AAChC,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,OAAO,CAAC;AAClC,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,aAAa,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Home site id. HamBench always has at least this station.
|
|
3
|
+
*/
|
|
4
|
+
export const HOME_STATION_ID = "station-home";
|
|
5
|
+
export const MIN_LATITUDE = -90;
|
|
6
|
+
export const MAX_LATITUDE = 90;
|
|
7
|
+
export const MIN_LONGITUDE = -180;
|
|
8
|
+
export const MAX_LONGITUDE = 180;
|
|
9
|
+
//# sourceMappingURL=radio-station.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"radio-station.js","sourceRoot":"","sources":["../../src/station/radio-station.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,cAAc,CAAC;AAE9C,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EAAE,CAAC;AAChC,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,CAAC;AAC/B,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAG,CAAC;AAClC,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springfield/ham-radio-api",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.9.0",
|
|
4
4
|
"author": "Bryan Hunt",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc",
|
|
21
|
+
"test": "node --experimental-strip-types --test test/unit/*.test.ts",
|
|
21
22
|
"commitlint": "commitlint --edit",
|
|
22
23
|
"format": "oxfmt",
|
|
23
24
|
"lint": "oxlint",
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { Wavelength } from "../branded-types/wavelength.js";
|
|
2
|
+
|
|
3
|
+
export type AntennaTypeId = "dipole" | "inverted-v" | "quarter-wave-vertical" | "yagi-3el" | "magloop" | "end-fed" | "dual-band-vertical" | "vhf-yagi" | "uhf-yagi";
|
|
4
|
+
|
|
5
|
+
/** How energy is distributed in azimuth. Heading is unused for omni. */
|
|
6
|
+
export type AntennaPattern = "omni" | "bidirectional" | "directive";
|
|
7
|
+
|
|
8
|
+
/** Coarse elevation character. Skip rings use type plus height, not this alone. */
|
|
9
|
+
export type AntennaTakeoff = "high" | "medium" | "low";
|
|
10
|
+
|
|
11
|
+
export interface AntennaType {
|
|
12
|
+
id: AntennaTypeId;
|
|
13
|
+
label: string;
|
|
14
|
+
description: string;
|
|
15
|
+
pattern: AntennaPattern;
|
|
16
|
+
/**
|
|
17
|
+
* Typical peak gain in dBi. Catalog estimate for a generic of this family,
|
|
18
|
+
* not a manufacturer measurement or NEC run.
|
|
19
|
+
*/
|
|
20
|
+
gainDbi: number;
|
|
21
|
+
/** Azimuth beamwidth of the main lobe in degrees. Omni is 360. */
|
|
22
|
+
beamwidthDeg: number;
|
|
23
|
+
/** Typical front-to-back ratio in dB. Zero for omni and bidirectional. */
|
|
24
|
+
frontToBackDb: number;
|
|
25
|
+
takeoff: AntennaTakeoff;
|
|
26
|
+
/** Ham bands this family typically covers, as SpectrumBand.wavelength. */
|
|
27
|
+
defaultBands: Wavelength[];
|
|
28
|
+
defaultHeightAglM: number;
|
|
29
|
+
/** True heading of maximum radiation when the type is not omni. */
|
|
30
|
+
defaultHeadingDeg: number;
|
|
31
|
+
/** Dipoles, inverted-Vs, and HF beams can use LC traps for extra bands. */
|
|
32
|
+
supportsTraps: boolean;
|
|
33
|
+
/** Tribanders start trapped; a single-band dipole does not. */
|
|
34
|
+
defaultTrapped: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const WAVELENGTH_160 = Wavelength(160);
|
|
38
|
+
const WAVELENGTH_80 = Wavelength(80);
|
|
39
|
+
const WAVELENGTH_40 = Wavelength(40);
|
|
40
|
+
const WAVELENGTH_30 = Wavelength(30);
|
|
41
|
+
const WAVELENGTH_20 = Wavelength(20);
|
|
42
|
+
const WAVELENGTH_17 = Wavelength(17);
|
|
43
|
+
const WAVELENGTH_15 = Wavelength(15);
|
|
44
|
+
const WAVELENGTH_12 = Wavelength(12);
|
|
45
|
+
const WAVELENGTH_10 = Wavelength(10);
|
|
46
|
+
const WAVELENGTH_6 = Wavelength(6);
|
|
47
|
+
const WAVELENGTH_2 = Wavelength(2);
|
|
48
|
+
const WAVELENGTH_70_CM = Wavelength(0.7);
|
|
49
|
+
|
|
50
|
+
/** HF + 6 m wavelengths, longest first. Matches amateur SpectrumBand.wavelength values. */
|
|
51
|
+
const HF_WAVELENGTHS: readonly Wavelength[] = [WAVELENGTH_160, WAVELENGTH_80, WAVELENGTH_40, WAVELENGTH_30, WAVELENGTH_20, WAVELENGTH_17, WAVELENGTH_15, WAVELENGTH_12, WAVELENGTH_10, WAVELENGTH_6];
|
|
52
|
+
|
|
53
|
+
const HF_40_TO_10: Wavelength[] = [WAVELENGTH_40, WAVELENGTH_30, WAVELENGTH_20, WAVELENGTH_17, WAVELENGTH_15, WAVELENGTH_12, WAVELENGTH_10];
|
|
54
|
+
const VHF_UHF: Wavelength[] = [WAVELENGTH_2, WAVELENGTH_70_CM];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Built-in generic antenna families. Commercial SKUs can parameterize these later.
|
|
58
|
+
*/
|
|
59
|
+
export const ANTENNA_TYPES: readonly AntennaType[] = [
|
|
60
|
+
{
|
|
61
|
+
id: "dipole",
|
|
62
|
+
label: "Dipole",
|
|
63
|
+
description: "Half-wave center-fed wire. Heading is the broadside direction of maximum radiation. Enable traps for a multi-band (for example 80/40) dipole.",
|
|
64
|
+
pattern: "bidirectional",
|
|
65
|
+
gainDbi: 2.15,
|
|
66
|
+
beamwidthDeg: 78,
|
|
67
|
+
frontToBackDb: 0,
|
|
68
|
+
takeoff: "medium",
|
|
69
|
+
defaultBands: [WAVELENGTH_40],
|
|
70
|
+
defaultHeightAglM: 10,
|
|
71
|
+
defaultHeadingDeg: 45,
|
|
72
|
+
supportsTraps: true,
|
|
73
|
+
defaultTrapped: false,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "inverted-v",
|
|
77
|
+
label: "Inverted-V",
|
|
78
|
+
description: "Center-supported dipole with sloping legs. Higher takeoff than a flat-top at the same apex. Enable traps for a multi-band wire.",
|
|
79
|
+
pattern: "bidirectional",
|
|
80
|
+
gainDbi: 1.5,
|
|
81
|
+
beamwidthDeg: 90,
|
|
82
|
+
frontToBackDb: 0,
|
|
83
|
+
takeoff: "high",
|
|
84
|
+
defaultBands: [WAVELENGTH_40],
|
|
85
|
+
defaultHeightAglM: 8,
|
|
86
|
+
defaultHeadingDeg: 45,
|
|
87
|
+
supportsTraps: true,
|
|
88
|
+
defaultTrapped: false,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "quarter-wave-vertical",
|
|
92
|
+
label: "Quarter-wave vertical",
|
|
93
|
+
description: "Ground-mounted or elevated vertical. Omnidirectional; heading is not used.",
|
|
94
|
+
pattern: "omni",
|
|
95
|
+
gainDbi: 1.5,
|
|
96
|
+
beamwidthDeg: 360,
|
|
97
|
+
frontToBackDb: 0,
|
|
98
|
+
takeoff: "medium",
|
|
99
|
+
defaultBands: [WAVELENGTH_40],
|
|
100
|
+
defaultHeightAglM: 2,
|
|
101
|
+
defaultHeadingDeg: 0,
|
|
102
|
+
supportsTraps: false,
|
|
103
|
+
defaultTrapped: false,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "yagi-3el",
|
|
107
|
+
label: "3-element Yagi",
|
|
108
|
+
description: "Directive trap tribander (typically 20/15/10). Heading is the boom direction of maximum gain.",
|
|
109
|
+
pattern: "directive",
|
|
110
|
+
gainDbi: 7,
|
|
111
|
+
beamwidthDeg: 66,
|
|
112
|
+
frontToBackDb: 20,
|
|
113
|
+
takeoff: "low",
|
|
114
|
+
defaultBands: [WAVELENGTH_20, WAVELENGTH_15, WAVELENGTH_10],
|
|
115
|
+
defaultHeightAglM: 15,
|
|
116
|
+
defaultHeadingDeg: 45,
|
|
117
|
+
supportsTraps: true,
|
|
118
|
+
defaultTrapped: true,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "magloop",
|
|
122
|
+
label: "Magnetic loop",
|
|
123
|
+
description: "Small transmitting loop. Roughly omnidirectional; efficiency is lower than a full-size wire.",
|
|
124
|
+
pattern: "omni",
|
|
125
|
+
gainDbi: 0,
|
|
126
|
+
beamwidthDeg: 360,
|
|
127
|
+
frontToBackDb: 0,
|
|
128
|
+
takeoff: "high",
|
|
129
|
+
defaultBands: HF_40_TO_10,
|
|
130
|
+
defaultHeightAglM: 2,
|
|
131
|
+
defaultHeadingDeg: 0,
|
|
132
|
+
supportsTraps: false,
|
|
133
|
+
defaultTrapped: false,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "end-fed",
|
|
137
|
+
label: "End-fed",
|
|
138
|
+
description: "End-fed half-wave or random wire. Heading is the broadside of the wire.",
|
|
139
|
+
pattern: "bidirectional",
|
|
140
|
+
gainDbi: 2,
|
|
141
|
+
beamwidthDeg: 90,
|
|
142
|
+
frontToBackDb: 0,
|
|
143
|
+
takeoff: "medium",
|
|
144
|
+
defaultBands: HF_40_TO_10,
|
|
145
|
+
defaultHeightAglM: 10,
|
|
146
|
+
defaultHeadingDeg: 45,
|
|
147
|
+
supportsTraps: false,
|
|
148
|
+
defaultTrapped: false,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "dual-band-vertical",
|
|
152
|
+
label: "Dual-band vertical",
|
|
153
|
+
description: "2 m / 70 cm omnidirectional (J-pole, collinear, or dual-band whip). Heading is not used.",
|
|
154
|
+
pattern: "omni",
|
|
155
|
+
gainDbi: 4.5,
|
|
156
|
+
beamwidthDeg: 360,
|
|
157
|
+
frontToBackDb: 0,
|
|
158
|
+
takeoff: "low",
|
|
159
|
+
defaultBands: VHF_UHF,
|
|
160
|
+
defaultHeightAglM: 8,
|
|
161
|
+
defaultHeadingDeg: 0,
|
|
162
|
+
supportsTraps: false,
|
|
163
|
+
defaultTrapped: false,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "vhf-yagi",
|
|
167
|
+
label: "2 m Yagi",
|
|
168
|
+
description: "Directive 2 m beam. Heading is the boom direction of maximum gain.",
|
|
169
|
+
pattern: "directive",
|
|
170
|
+
gainDbi: 10,
|
|
171
|
+
beamwidthDeg: 42,
|
|
172
|
+
frontToBackDb: 18,
|
|
173
|
+
takeoff: "low",
|
|
174
|
+
defaultBands: [WAVELENGTH_2],
|
|
175
|
+
defaultHeightAglM: 10,
|
|
176
|
+
defaultHeadingDeg: 45,
|
|
177
|
+
supportsTraps: false,
|
|
178
|
+
defaultTrapped: false,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "uhf-yagi",
|
|
182
|
+
label: "70 cm Yagi",
|
|
183
|
+
description: "Directive 70 cm beam. Heading is the boom direction of maximum gain.",
|
|
184
|
+
pattern: "directive",
|
|
185
|
+
gainDbi: 12,
|
|
186
|
+
beamwidthDeg: 36,
|
|
187
|
+
frontToBackDb: 18,
|
|
188
|
+
takeoff: "low",
|
|
189
|
+
defaultBands: [WAVELENGTH_70_CM],
|
|
190
|
+
defaultHeightAglM: 10,
|
|
191
|
+
defaultHeadingDeg: 45,
|
|
192
|
+
supportsTraps: false,
|
|
193
|
+
defaultTrapped: false,
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
/** Catalog loss for LC traps. Not a manufacturer efficiency spec. */
|
|
198
|
+
const TRAP_GAIN_LOSS_DBI = 0.5;
|
|
199
|
+
|
|
200
|
+
const ANTENNA_TYPE_IDS = new Set<string>(ANTENNA_TYPES.map((type) => type.id));
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Returns whether `value` is a built-in antenna type id.
|
|
204
|
+
*/
|
|
205
|
+
export function isAntennaTypeId(value: unknown): value is AntennaTypeId {
|
|
206
|
+
return typeof value === "string" && ANTENNA_TYPE_IDS.has(value);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Looks up a built-in antenna type, or undefined when the id is unknown.
|
|
211
|
+
*/
|
|
212
|
+
export function antennaTypeById(id: AntennaTypeId | string | undefined): AntennaType | undefined {
|
|
213
|
+
if (!id) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return ANTENNA_TYPES.find((type) => type.id === id);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* True when the operator should enter a heading (not omnidirectional).
|
|
222
|
+
*/
|
|
223
|
+
export function antennaTypeUsesHeading(type: AntennaType): boolean {
|
|
224
|
+
return type.pattern !== "omni";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* True for dipoles, inverted-Vs, and HF Yagis — not VHF/UHF beams or omni verticals.
|
|
229
|
+
*/
|
|
230
|
+
export function antennaTypeSupportsTraps(type: AntennaType): boolean {
|
|
231
|
+
return type.supportsTraps;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* HF / 6 m. 2 m (2) and 70 cm (0.7) are VHF/UHF.
|
|
236
|
+
*/
|
|
237
|
+
export function isHfAntennaBand(wavelength: Wavelength): boolean {
|
|
238
|
+
return wavelength >= 6;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Catalog order: longest wavelength first.
|
|
243
|
+
*/
|
|
244
|
+
export function sortAntennaBands(bands: readonly Wavelength[]): Wavelength[] {
|
|
245
|
+
return [...new Set(bands)].sort((left, right) => right - left);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* True when every tagged band is VHF/UHF FM (2 m / 70 cm), not HF skywave.
|
|
250
|
+
*/
|
|
251
|
+
export function antennaBandsAreLineOfSight(bands: readonly Wavelength[]): boolean {
|
|
252
|
+
return bands.length > 0 && bands.every((wavelength) => wavelength <= 2);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Trap resonant frequencies: every tagged HF band except the lowest (the full-size fundamental).
|
|
257
|
+
*
|
|
258
|
+
* An 80/40 dipole traps at 40 m. A 20/15/10 tribander traps at 15 m and 10 m.
|
|
259
|
+
*/
|
|
260
|
+
export function trapResonantBands(bands: readonly Wavelength[]): Wavelength[] {
|
|
261
|
+
const hf = sortAntennaBands(bands).filter(isHfAntennaBand);
|
|
262
|
+
|
|
263
|
+
return hf.slice(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* True when this installation uses traps on two or more HF bands.
|
|
268
|
+
*/
|
|
269
|
+
export function antennaIsTrapped(type: AntennaType, trapped: boolean | undefined, bands: readonly Wavelength[]): boolean {
|
|
270
|
+
return type.supportsTraps && trapped === true && trapResonantBands(bands).length > 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* When traps are turned on with only one HF band, add the next-lower ham band as the fundamental
|
|
275
|
+
* (40 m → 80/40). If the tag is already 160 m, add 80 m as the trap frequency.
|
|
276
|
+
*/
|
|
277
|
+
export function bandsForEnabledTraps(bands: readonly Wavelength[]): Wavelength[] {
|
|
278
|
+
if (trapResonantBands(bands).length > 0) {
|
|
279
|
+
return sortAntennaBands(bands);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const present = HF_WAVELENGTHS.filter((wavelength) => bands.includes(wavelength));
|
|
283
|
+
const current = present[0] ?? WAVELENGTH_40;
|
|
284
|
+
const index = HF_WAVELENGTHS.indexOf(current);
|
|
285
|
+
const added = index > 0 ? HF_WAVELENGTHS[index - 1] : HF_WAVELENGTHS[index + 1];
|
|
286
|
+
|
|
287
|
+
if (!added) {
|
|
288
|
+
return sortAntennaBands(bands.length > 0 ? bands : [current]);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return sortAntennaBands([...bands, current, added]);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Peak gain with a small catalog derate when traps are in the elements.
|
|
296
|
+
*/
|
|
297
|
+
export function estimateGainDbi(type: AntennaType, trapped: boolean): number {
|
|
298
|
+
if (!trapped) {
|
|
299
|
+
return type.gainDbi;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return Math.round((type.gainDbi - TRAP_GAIN_LOSS_DBI) * 100) / 100;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Heuristic takeoff of the main lobe in degrees, from type and height in wavelengths.
|
|
307
|
+
*
|
|
308
|
+
* This is a catalog rule of thumb for skip rings, not an EZNEC elevation plot.
|
|
309
|
+
*/
|
|
310
|
+
export function estimateTakeoffDeg(type: AntennaType, heightAglM: number, wavelength: Wavelength = type.defaultBands[0] ?? WAVELENGTH_40): number {
|
|
311
|
+
const heights = Math.max(0.1, heightAglM / wavelength);
|
|
312
|
+
|
|
313
|
+
if (type.takeoff === "high") {
|
|
314
|
+
return clamp(Math.round(52 - heights * 10), 25, 70);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (type.takeoff === "low") {
|
|
318
|
+
return clamp(Math.round(24 - heights * 8), 6, 25);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return clamp(Math.round(36 - heights * 12), 10, 45);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function clamp(value: number, min: number, max: number): number {
|
|
325
|
+
return Math.min(max, Math.max(min, value));
|
|
326
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -40,3 +40,8 @@ export * from "./spectrum/band.js";
|
|
|
40
40
|
export * from "./spectrum/channel.js";
|
|
41
41
|
export * from "./spectrum/mode.js";
|
|
42
42
|
export * from "./spectrum/privilege.js";
|
|
43
|
+
|
|
44
|
+
export * from "./antenna/type.js";
|
|
45
|
+
|
|
46
|
+
export * from "./station/radio-station.js";
|
|
47
|
+
export * from "./station/antenna.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Wavelength } from "../branded-types/wavelength.js";
|
|
2
|
+
import type { AntennaTypeId } from "../antenna/type.js";
|
|
3
|
+
|
|
4
|
+
export const MIN_ANTENNA_HEIGHT_AGL_M = 0.5;
|
|
5
|
+
export const MAX_ANTENNA_HEIGHT_AGL_M = 120;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One physical antenna at one station.
|
|
9
|
+
*
|
|
10
|
+
* The same radiator at two sites is two records. `bands` are ham
|
|
11
|
+
* {@link SpectrumBand.wavelength} values, not a second band catalog.
|
|
12
|
+
*/
|
|
13
|
+
export interface StationAntenna {
|
|
14
|
+
id: string;
|
|
15
|
+
stationId: string;
|
|
16
|
+
nickname: string;
|
|
17
|
+
typeId: AntennaTypeId;
|
|
18
|
+
heightAglM: number;
|
|
19
|
+
/** True heading of maximum radiation, 0–359. Omitted for omni types. */
|
|
20
|
+
headingDeg?: number;
|
|
21
|
+
bands: Wavelength[];
|
|
22
|
+
/** LC traps in the elements. Omitted when the type is a single-band or trapless. */
|
|
23
|
+
trapped?: boolean;
|
|
24
|
+
createdAt: number;
|
|
25
|
+
updatedAt: number;
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Home site id. HamBench always has at least this station.
|
|
3
|
+
*/
|
|
4
|
+
export const HOME_STATION_ID = "station-home";
|
|
5
|
+
|
|
6
|
+
export const MIN_LATITUDE = -90;
|
|
7
|
+
export const MAX_LATITUDE = 90;
|
|
8
|
+
export const MIN_LONGITUDE = -180;
|
|
9
|
+
export const MAX_LONGITUDE = 180;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether the operator last edited the Maidenhead grid or the coordinates.
|
|
13
|
+
*
|
|
14
|
+
* Editing the grid sets lat/lon to the cell center. Editing coordinates keeps
|
|
15
|
+
* that point and derives a 6-character grid.
|
|
16
|
+
*/
|
|
17
|
+
export type StationLocationSource = "grid" | "coordinates";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* An operating site (Home, Cabin, Portable), not a log STATION_CALLSIGN.
|
|
21
|
+
*
|
|
22
|
+
* Latitude and longitude are signed decimal degrees (south / west negative).
|
|
23
|
+
* UIs may collect them as degrees, minutes, and decimal seconds.
|
|
24
|
+
*/
|
|
25
|
+
export interface RadioStation {
|
|
26
|
+
id: string;
|
|
27
|
+
nickname: string;
|
|
28
|
+
gridsquare?: string;
|
|
29
|
+
latitude?: number;
|
|
30
|
+
longitude?: number;
|
|
31
|
+
locationSource?: StationLocationSource;
|
|
32
|
+
createdAt: number;
|
|
33
|
+
updatedAt: number;
|
|
34
|
+
}
|