Add modulation to RadioFrequency

- This adds the information about the modulation of the RadioFrequency.
- Updated all Radios with the capabale modulation
- Show Modulation on Kneeboard
- Defaulting to AM Modulation as this is also the default used by pydcs.
- Force AM Modulation for JTAC tasking

We currently do not force the modulation in the code anywhere other than JTAC. Pydcs defaults to AM (modulation=0). So this change is more a preparation for upcoming features which allow to use more frequencies like VHF FM or similar.
This commit is contained in:
RndName 2022-04-10 23:13:36 +02:00
parent aae314ae1d
commit 69a5b4f227
3 changed files with 118 additions and 63 deletions

View File

@ -12,6 +12,7 @@ from typing import Any, ClassVar, Dict, Optional, TYPE_CHECKING, Tuple
import yaml import yaml
from dcs.terrain import Airport from dcs.terrain import Airport
from dcs.task import Modulation
from game.radio.radios import RadioFrequency from game.radio.radios import RadioFrequency
from game.radio.tacan import TacanChannel from game.radio.tacan import TacanChannel
@ -33,10 +34,10 @@ class AtcData:
if atc_data is None: if atc_data is None:
return None return None
return AtcData( return AtcData(
RadioFrequency.parse(atc_data["hf"]), RadioFrequency.parse(atc_data["hf"], Modulation.FM),
RadioFrequency.parse(atc_data["vhf_low"]), RadioFrequency.parse(atc_data["vhf_low"], Modulation.FM),
RadioFrequency.parse(atc_data["vhf_high"]), RadioFrequency.parse(atc_data["vhf_high"], Modulation.AM),
RadioFrequency.parse(atc_data["uhf"]), RadioFrequency.parse(atc_data["uhf"], Modulation.AM),
) )
@ -108,7 +109,10 @@ class AirfieldData:
vor = None vor = None
if (vor_data := data.get("vor")) is not None: if (vor_data := data.get("vor")) is not None:
vor = (vor_data["callsign"], RadioFrequency.parse(vor_data["frequency"])) vor = (
vor_data["callsign"],
RadioFrequency.parse(vor_data["frequency"], Modulation.FM),
)
rsbn = None rsbn = None
if (rsbn_data := data.get("rsbn")) is not None: if (rsbn_data := data.get("rsbn")) is not None:
@ -122,7 +126,7 @@ class AirfieldData:
if (ils_data := runway_data.get("ils")) is not None: if (ils_data := runway_data.get("ils")) is not None:
ils[name] = ( ils[name] = (
ils_data["callsign"], ils_data["callsign"],
RadioFrequency.parse(ils_data["frequency"]), RadioFrequency.parse(ils_data["frequency"], Modulation.FM),
) )
if (prmg_data := runway_data.get("prmg")) is not None: if (prmg_data := runway_data.get("prmg")) is not None:
@ -131,13 +135,13 @@ class AirfieldData:
if (outer_ndb_data := runway_data.get("outer_ndb")) is not None: if (outer_ndb_data := runway_data.get("outer_ndb")) is not None:
outer_ndb[name] = ( outer_ndb[name] = (
outer_ndb_data["callsign"], outer_ndb_data["callsign"],
RadioFrequency.parse(outer_ndb_data["frequency"]), RadioFrequency.parse(outer_ndb_data["frequency"], Modulation.AM),
) )
if (inner_ndb_data := runway_data.get("inner_ndb")) is not None: if (inner_ndb_data := runway_data.get("inner_ndb")) is not None:
inner_ndb[name] = ( inner_ndb[name] = (
inner_ndb_data["callsign"], inner_ndb_data["callsign"],
RadioFrequency.parse(inner_ndb_data["frequency"]), RadioFrequency.parse(inner_ndb_data["frequency"], Modulation.AM),
) )
return AirfieldData( return AirfieldData(

View File

@ -165,7 +165,11 @@ class FlotGenerator:
maintask=AFAC, maintask=AFAC,
) )
jtac.points[0].tasks.append( jtac.points[0].tasks.append(
FAC(callsign=len(self.air_support.jtacs) + 1, frequency=int(freq.mhz)) FAC(
callsign=len(self.air_support.jtacs) + 1,
frequency=int(freq.mhz),
modulation=freq.modulation,
)
) )
jtac.points[0].tasks.append(SetInvisibleCommand(True)) jtac.points[0].tasks.append(SetInvisibleCommand(True))
jtac.points[0].tasks.append(SetImmortalCommand(True)) jtac.points[0].tasks.append(SetImmortalCommand(True))

View File

@ -6,18 +6,24 @@ import logging
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, FrozenSet, Iterator, List, Set, Tuple from typing import Dict, FrozenSet, Iterator, List, Set, Tuple
from dcs.task import Modulation
@dataclass(frozen=True) @dataclass(frozen=True)
class RadioFrequency: class RadioFrequency:
"""A radio frequency. """A radio frequency and the modulation used"""
Not currently concerned with tracking modulation, just the frequency.
"""
#: The frequency in kilohertz. #: The frequency in kilohertz.
hertz: int hertz: int
#: The frequency modulation (AM or FM)
# Pydcs defaults currently to modultion=0 which is equal to AM
# Modulation is currently only used to tell the User the Modulation via the
# kneeboard. We do not force any modulation from pydcs. We just set the
# frequency with the set_frequency function which does not allow to set the
# modulation yet. It defaults to modulation=0 which is equal to forcing AM
modulation: Modulation = Modulation.AM
def __str__(self) -> str: def __str__(self) -> str:
if self.hertz >= 1000000: if self.hertz >= 1000000:
return self.format("MHz", 1000000) return self.format("MHz", 1000000)
@ -26,8 +32,8 @@ class RadioFrequency:
def format(self, units: str, divisor: int) -> str: def format(self, units: str, divisor: int) -> str:
converted = self.hertz / divisor converted = self.hertz / divisor
if converted.is_integer(): if converted.is_integer():
return f"{int(converted)} {units}" return f"{int(converted)} {units} {self.modulation.name}"
return f"{converted:0.3f} {units}" return f"{converted:0.3f} {units} {self.modulation.name}"
@property @property
def mhz(self) -> float: def mhz(self) -> float:
@ -39,7 +45,7 @@ class RadioFrequency:
return self.hertz / 1000000 return self.hertz / 1000000
@classmethod @classmethod
def parse(cls, text: str) -> RadioFrequency: def parse(cls, text: str, modulation: Modulation = Modulation.AM) -> RadioFrequency:
match = re.match(r"""^(\d+)(?:\.(\d{1,3}))? (MHz|kHz)$""", text) match = re.match(r"""^(\d+)(?:\.(\d{1,3}))? (MHz|kHz)$""", text)
if match is None: if match is None:
raise ValueError(f"Could not parse radio frequency from {text}") raise ValueError(f"Could not parse radio frequency from {text}")
@ -57,18 +63,22 @@ class RadioFrequency:
partial *= 10 partial *= 10
if units == "MHz": if units == "MHz":
return MHz(whole, partial) return MHz(whole, partial, modulation)
if units == "kHz": if units == "kHz":
return kHz(whole, partial) return kHz(whole, partial, modulation)
raise ValueError(f"Unexpected units in radio frequency: {units}") raise ValueError(f"Unexpected units in radio frequency: {units}")
def MHz(num: int, khz: int = 0) -> RadioFrequency: def MHz(
return RadioFrequency(num * 1000000 + khz * 1000) num: int, khz: int = 0, modulation: Modulation = Modulation.AM
) -> RadioFrequency:
return RadioFrequency(num * 1000000 + khz * 1000, modulation)
def kHz(num: int, hz: int = 0) -> RadioFrequency: def kHz(
return RadioFrequency(num * 1000 + hz) num: int, hz: int = 0, modulation: Modulation = Modulation.AM
) -> RadioFrequency:
return RadioFrequency(num * 1000 + hz, modulation)
@dataclass(frozen=True) @dataclass(frozen=True)
@ -84,25 +94,29 @@ class RadioRange:
#: The spacing between adjacent frequencies. #: The spacing between adjacent frequencies.
step: RadioFrequency step: RadioFrequency
#: Modulation, AM or FM. Defaulting to AM as it is more used for comms
# Overrides the modulation setting of the min and max frequency for the whole range
modulation: Modulation = Modulation.AM
#: Specific frequencies to exclude. (e.g. Guard channels) #: Specific frequencies to exclude. (e.g. Guard channels)
excludes: FrozenSet[RadioFrequency] = frozenset() excludes: FrozenSet[RadioFrequency] = frozenset()
def range(self) -> Iterator[RadioFrequency]: def range(self) -> Iterator[RadioFrequency]:
"""Returns an iterator over the usable frequencies of this radio.""" """Returns an iterator over the usable frequencies of this radio."""
return ( return (
RadioFrequency(x) RadioFrequency(x, self.modulation)
for x in range(self.minimum.hertz, self.maximum.hertz, self.step.hertz) for x in range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
if RadioFrequency(x) not in self.excludes if RadioFrequency(x, self.modulation) not in self.excludes
) )
@property @property
def last_channel(self) -> RadioFrequency: def last_channel(self) -> RadioFrequency:
return next( return next(
RadioFrequency(x) RadioFrequency(x, self.modulation)
for x in reversed( for x in reversed(
range(self.minimum.hertz, self.maximum.hertz, self.step.hertz) range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
) )
if RadioFrequency(x) not in self.excludes if RadioFrequency(x, self.modulation) not in self.excludes
) )
@ -141,58 +155,88 @@ class ChannelInUseError(RuntimeError):
# TODO: Figure out appropriate steps for each radio. These are just guesses. # TODO: Figure out appropriate steps for each radio. These are just guesses.
#: List of all known radios used by aircraft in the game. #: List of all known radios used by aircraft in the game.
RADIOS: List[Radio] = [ RADIOS: List[Radio] = [
Radio("AN/ARC-164", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)), Radio("AN/ARC-164", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
Radio("AN/ARC-186(V) AM", (RadioRange(MHz(116), MHz(152), step=MHz(1)),)), Radio("AN/ARC-186(V) AM", (RadioRange(MHz(116), MHz(152), MHz(1), Modulation.AM),)),
Radio("AN/ARC-186(V) FM", (RadioRange(MHz(30), MHz(76), step=MHz(1)),)), Radio("AN/ARC-186(V) FM", (RadioRange(MHz(30), MHz(76), MHz(1), Modulation.FM),)),
Radio( Radio(
"AN/ARC-210", "AN/ARC-210",
( (
RadioRange(MHz(225), MHz(400), MHz(1), frozenset((MHz(243),))), RadioRange(
RadioRange(MHz(136), MHz(155), MHz(1)), MHz(225),
RadioRange(MHz(156), MHz(174), MHz(1)), MHz(400),
RadioRange(MHz(118), MHz(136), MHz(1)), MHz(1),
RadioRange(MHz(30), MHz(88), MHz(1)), Modulation.AM,
frozenset((MHz(243),)),
),
RadioRange(MHz(136), MHz(155), MHz(1), Modulation.AM),
RadioRange(MHz(156), MHz(174), MHz(1), Modulation.FM),
RadioRange(MHz(118), MHz(136), MHz(1), Modulation.AM),
RadioRange(MHz(30), MHz(88), MHz(1), Modulation.FM),
# The AN/ARC-210 can also use 225-400 and 136-155 with FM Modulation
RadioRange(
MHz(225),
MHz(400),
MHz(1),
Modulation.FM,
frozenset((MHz(243),)),
),
RadioRange(MHz(136), MHz(155), MHz(1), Modulation.FM),
), ),
), ),
Radio("AN/ARC-222", (RadioRange(MHz(116), MHz(152), step=MHz(1)),)), Radio("AN/ARC-222", (RadioRange(MHz(116), MHz(152), MHz(1), Modulation.AM),)),
Radio("SCR-522", (RadioRange(MHz(100), MHz(156), step=MHz(1)),)), Radio("SCR-522", (RadioRange(MHz(100), MHz(156), MHz(1), Modulation.AM),)),
Radio("A.R.I. 1063", (RadioRange(MHz(100), MHz(156), step=MHz(1)),)), Radio("A.R.I. 1063", (RadioRange(MHz(100), MHz(156), MHz(1), Modulation.AM),)),
Radio("BC-1206", (RadioRange(kHz(200), kHz(400), step=kHz(10)),)), Radio("BC-1206", (RadioRange(kHz(200), kHz(400), kHz(10), Modulation.AM),)),
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between Radio(
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current "TRT ERA 7000 V/UHF",
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We (
# can model gaps later if needed. RadioRange(MHz(118), MHz(150), MHz(1), Modulation.AM),
Radio("TRT ERA 7000 V/UHF", (RadioRange(MHz(118), MHz(150), step=MHz(1)),)), RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),
Radio("TRT ERA 7200 UHF", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)), ),
),
Radio("TRT ERA 7200 UHF", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
# Tomcat radios # Tomcat radios
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio # # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
Radio("AN/ARC-159", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)), Radio("AN/ARC-159", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
# AN/ARC-182 can also operate from 30 MHz to 88 MHz, as well as from 225 MHz
# to 400 MHz range, but we can't model gaps with the current implementation.
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio # https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
Radio("AN/ARC-182", (RadioRange(MHz(108), MHz(174), step=MHz(1)),)), Radio(
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps. "AN/ARC-182",
Radio("FR 22", (RadioRange(MHz(225), MHz(400), step=kHz(50)),)), (
RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),
RadioRange(MHz(108), MHz(174), MHz(1), Modulation.AM),
# The Range from 30-88MHz should be FM but its modeled as AM in dcs
RadioRange(MHz(30), MHz(88), MHz(1), Modulation.AM),
),
),
Radio(
"FR 22",
(
RadioRange(MHz(225), MHz(400), kHz(50), Modulation.AM),
RadioRange(MHz(103), MHz(156), kHz(25), Modulation.AM),
),
),
# P-51 / P-47 Radio # P-51 / P-47 Radio
# 4 preset channels (A/B/C/D) # 4 preset channels (A/B/C/D)
Radio("SCR522", (RadioRange(MHz(100), MHz(156), step=kHz(25)),)), Radio("SCR522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), step=MHz(1)),)), # JF-17 Radios should use AM
Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)), Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), MHz(1), Modulation.AM),)),
Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
# MiG-15bis # MiG-15bis
Radio("RSI-6K HF", (RadioRange(MHz(3, 750), MHz(5), step=kHz(25)),)), Radio("RSI-6K HF", (RadioRange(MHz(3, 750), MHz(5), kHz(25), Modulation.AM),)),
# MiG-19P # MiG-19P
Radio("RSIU-4V", (RadioRange(MHz(100), MHz(150), step=MHz(1)),)), Radio("RSIU-4V", (RadioRange(MHz(100), MHz(150), MHz(1), Modulation.AM),)),
# MiG-21bis # MiG-21bis
Radio("RSIU-5V", (RadioRange(MHz(118), MHz(140), step=MHz(1)),)), Radio("RSIU-5V", (RadioRange(MHz(118), MHz(140), MHz(1), Modulation.AM),)),
# Ka-50 # Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps. # Note: Also capable of 100MHz-150MHz, but we can't model gaps.
Radio("R-800L1", (RadioRange(MHz(220), MHz(400), step=kHz(25)),)), Radio("R-800L1", (RadioRange(MHz(220), MHz(400), kHz(25), Modulation.AM),)),
Radio("R-828", (RadioRange(MHz(20), MHz(60), step=kHz(25)),)), Radio("R-828", (RadioRange(MHz(20), MHz(60), kHz(25), Modulation.FM),)),
# UH-1H # UH-1H
Radio("AN/ARC-51BX", (RadioRange(MHz(225), MHz(400), step=kHz(50)),)), Radio("AN/ARC-51BX", (RadioRange(MHz(225), MHz(400), kHz(50), Modulation.AM),)),
Radio("AN/ARC-131", (RadioRange(MHz(30), MHz(76), step=kHz(50)),)), Radio("AN/ARC-131", (RadioRange(MHz(30), MHz(76), kHz(50), Modulation.FM),)),
Radio("AN/ARC-134", (RadioRange(MHz(116), MHz(150), step=kHz(25)),)), Radio("AN/ARC-134", (RadioRange(MHz(116), MHz(150), kHz(25), Modulation.AM),)),
Radio("R&S Series 6000", (RadioRange(MHz(100), MHz(156), step=kHz(25)),)), # JAS39
Radio("R&S Series 6000", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
] ]
@ -233,7 +277,10 @@ class RadioRegistry:
# Not a real radio, but useful for allocating a channel usable for # Not a real radio, but useful for allocating a channel usable for
# inter-flight communications. # inter-flight communications.
BLUFOR_UHF = Radio("BLUFOR UHF", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)) # Uses AM as default Modulation
BLUFOR_UHF = Radio(
"BLUFOR UHF", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)
)
def __init__(self) -> None: def __init__(self) -> None:
self.allocated_channels: Set[RadioFrequency] = set() self.allocated_channels: Set[RadioFrequency] = set()