diff --git a/src/dzcb/anytone.py b/src/dzcb/anytone.py index 4b96788..4f86880 100644 --- a/src/dzcb/anytone.py +++ b/src/dzcb/anytone.py @@ -3,9 +3,10 @@ Supported CPS versions - 578: 1.11 + 578: 1.21 868: 1.39 - 878: 1.21 + 878: 4.00 + 890: 1.03 """ import csv @@ -95,12 +96,26 @@ def value_from(cls, channel): return cls.ALWAYS.value -# 578/868/878 Common Talkgroups.CSV format +# 578/868/878/890 Common Talkgroups.CSV format talkgroup_fields = ("No.", "Radio ID", "Name", "Call Type", "Call Alert") talkgroup_filename = "TalkGroups.CSV" -talkgroup_filename_578_1_11 = "ContactTalkGroups.CSV" +talkgroup_filename_578_1_21 = "ContactTalkGroups.CSV" -channel_fields_578_1_11 = { +talkgroup_fields_890_1_03 = { + "No.": None, + "Radio ID": None, + "Callsign": None, + "Name": None, + "City": None, + "State": None, + "Country": None, + "Remarks": None, + "Call Type": None, + "Call Alert": None, +} +talkgroup_filename_890_1_03 = "DMRTalkGroups.CSV" + +channel_fields_578_1_21 = { "No.": None, "Channel Name": None, "Receive Frequency": None, @@ -138,7 +153,7 @@ def value_from(cls, channel): "2TONE Decode": "0", "Ranging": OFF, "Simplex": OFF, - "Digi APRS RX": OFF, + "APRS RX": OFF, "Analog APRS PTT Mode": OFF, "Digital APRS PTT Mode": OFF, "APRS Report Type": OFF, @@ -150,6 +165,10 @@ def value_from(cls, channel): "DataACK Disable": "0", "R5toneBot": "0", "R5ToneEot": "0", + "Auto Scan": OFF, + "Send Talker Alias": OFF, + "ARC4": OFF, + "ex_emg_kind": "0", } channel_fields_868_1_39 = { "No.": None, @@ -191,7 +210,7 @@ def value_from(cls, channel): "APRS Report": OFF, "APRS Report Channel": "1", } -channel_fields_878_1_21 = { +channel_fields_878_4_00 = { "No.": None, "Channel Name": None, "Receive Frequency": None, @@ -242,9 +261,146 @@ def value_from(cls, channel): "R5toneBot": "0", "R5ToneEot": "0", } +channel_fields_878_4_00 = { + "No.": None, + "Channel Name": None, + "Receive Frequency": None, + "Transmit Frequency": None, + "Channel Type": None, + "Transmit Power": "High", + "Band Width": "25K", + "CTCSS/DCS Decode": OFF, + "CTCSS/DCS Encode": OFF, + "Contact": "", + "Contact Call Type": "Group Call", + "Contact TG/DMR ID": "0", + "Radio ID": "", + "Busy Lock/TX Permit": "Always", + "Squelch Mode": "Carrier", + "Optional Signal": OFF, + "DTMF ID": "1", + "2Tone ID": "1", + "5Tone ID": "1", + "PTT ID": OFF, + "RX Color Code": "1", + "Slot": "1", + "Scan List": NONE, + "Receive Group List": NONE, + "PTT Prohibit": OFF, + "Reverse": OFF, + "Simplex TDMA": OFF, + "Slot Suit": OFF, + "AES Digital Encryption": "Normal Encryption", + "Digital Encryption": OFF, + "Call Confirmation": OFF, + "Talk Around(Simplex)": OFF, + "Work Alone": OFF, + "Custom CTCSS": "251.1", + "2TONE Decode": "0", + "Ranging": OFF, + "Through Mode": OFF, + "APRS RX": OFF, + "Analog APRS PTT Mode": OFF, + "Digital APRS PTT Mode": OFF, + "APRS Report Type": OFF, + "Digital APRS Report Channel": "1", + "Correct Frequency[Hz]": "0", + "SMS Confirmation": OFF, + "Exclude channel from roaming": "0", + "DMR MODE": "0", + "DataACK Disable": "0", + "R5toneBot": "0", + "R5ToneEot": "0", + "Auto Scan": OFF, + "Ana Aprs Mute": OFF, + "Send Talker Alias": OFF, + "AnaAprsTxPath": "0", + "ARC4": OFF, + "ex_emg_kind": "0", + "TxCC": "1", +} +channel_fields_890_1_03 = { + "No.": None, + "Channel Name": None, + "Receive Frequency": None, + "Transmit Frequency": None, + "Channel Type": None, + "Transmit Power": "High", + "Band Width": "25K", + "CTCSS/DCS Decode": OFF, + "CTCSS/DCS Encode": OFF, + "Contact/Talk Group": "", + "Contact/Talk Group Call Type": "Group Call", + "Contact/Talk Group TG/DMR ID": "0", + "Radio ID": "", + "Busy Lock/TX Permit": "Always", + "Squelch Mode": "Carrier", + "Optional Signal": OFF, + "DTMF ID": "1", + "2Tone ID": "1", + "5Tone ID": "1", + "PTT ID": OFF, + "RX Color Code": "1", + "Slot": "1", + "Scan List": NONE, + "Receive Group List": NONE, + "PTT Prohibit": OFF, + "Reverse": OFF, + "Digital Duplex": OFF, + "Slot Suit": OFF, + "AES Digital Encryption": "Normal Encryption", + "Digital Encryption": OFF, + "Call Confirmation": OFF, + "Talk Around(Simplex)": OFF, + "Work Alone": OFF, + "Custom CTCSS": "251.1", + "2TONE Decode": "0", + "Ranging": OFF, + "Idle TX": OFF, + "APRS RX": OFF, + "Analog APRS PTT Mode": OFF, + "Digital APRS PTT Mode": OFF, + "APRS Report Type": OFF, + "Digital APRS Report Channel": "1", + "Correct Frequency[Hz]": "0", + "SMS Confirmation": OFF, + "Exclude channel from roaming": "0", + "DMR MODE": "0", + "DataACK Disable": "0", + "R5toneBot": "0", + "R5ToneEot": "0", + "Auto Scan": OFF, + "Ana APRS Mute": OFF, + "Send Talker Alias DMR/NX": OFF, + "AnaAprsTxPath": "0", + "ARC4": OFF, + "ex_emg_kind": "0", + "Rpga_Mdc": "0", + "DisturEn": OFF, + "DisturFreq": "0", + "dmr_crc_ignore": OFF, + "compand": OFF, + "tx_talkalaes": OFF, + "dup_call": OFF, + "tx_int": "0", + "BtRxState": "0", + "idle_tx": OFF, + "nxdn_wn": "0", + "NxdnRpga": "0", + "nxdnSqCon": "0", + "NxdnTxBusy": OFF, + "NxDnPttId": OFF, + "EnRan": OFF, + "DeRan": OFF, + "NxdnEncry": "0", + "NxdnGroupId": "0", + "NxdnIdNum": "0", + "NxdnStateNum": "0", + "txcc": "1", +} channel_filename = "Channel.CSV" -scanlist_fields_578_1_11 = { +scanlist_fields_578_1_21 = { "No.": None, "Scan List Name": None, "Scan Channel Member": None, @@ -280,7 +436,7 @@ def value_from(cls, channel): } scanlist_filename = "ScanList.CSV" -zone_fields_578_1_11 = { +zone_fields_578_1_21 = { "No.": None, "Zone Name": None, "Zone Channel Member": None, @@ -289,9 +445,10 @@ def value_from(cls, channel): "A Channel": None, "A Channel RX Frequency": None, "A Channel TX Frequency": None, - "B Channel": None, + "": None, "B Channel RX Frequency": None, "B Channel TX Frequency": None, + "Zone Hide": 0, } zone_fields_868_1_39 = { "No.": None, @@ -303,18 +460,18 @@ def value_from(cls, channel): zone_filename = "Zone.CSV" SUPPORTED_RADIOS = { - "578_1_11": dict( - version="1.11", + "578_1_21": dict( + version="1.21", expand_members=True, frequency_range=(COMMERCIAL_VHF, AMATEUR_220, COMMERCIAL_UHF), - channel=channel_fields_578_1_11, + channel=channel_fields_578_1_21, channel_filename=channel_filename, - scanlist=scanlist_fields_578_1_11, + scanlist=scanlist_fields_578_1_21, scanlist_filename=scanlist_filename, - zone=zone_fields_578_1_11, + zone=zone_fields_578_1_21, zone_filename=zone_filename, talkgroup=talkgroup_fields, - talkgroup_filename=talkgroup_filename_578_1_11, + talkgroup_filename=talkgroup_filename_578_1_21, replace_field_names={ "Through Mode": "Simplex", }, @@ -338,24 +495,40 @@ def value_from(cls, channel): }, remove_fields=["Contact TG/DMR ID", "DMR MODE"], ), - "878_1_21": dict( - version="1.21", + "878_4_00": dict( + version="4.00", expand_members=True, frequency_range=(COMMERCIAL_VHF, COMMERCIAL_UHF), - channel=channel_fields_878_1_21, + channel=channel_fields_878_4_00, channel_filename=channel_filename, - scanlist=scanlist_fields_578_1_11, + scanlist=scanlist_fields_578_1_21, scanlist_filename=scanlist_filename, - zone=zone_fields_578_1_11, + zone=zone_fields_578_1_21, zone_filename=zone_filename, talkgroup=talkgroup_fields, talkgroup_filename=talkgroup_filename, replace_field_names={}, remove_fields=[], ), + # 878v2_4_00 produces identical output to 878_4_00, so omitted to avoid duplication + "890_1_03": dict( + version="1.03", + expand_members=True, + frequency_range=(COMMERCIAL_VHF, AMATEUR_220, COMMERCIAL_UHF), + channel=channel_fields_890_1_03, + channel_filename="Channel.CSV", + scanlist=scanlist_fields_578_1_21, + scanlist_filename=scanlist_filename, + zone=zone_fields_578_1_21, + zone_filename="DMRZone.CSV", + talkgroup=talkgroup_fields, + talkgroup_filename=talkgroup_filename, + replace_field_names={}, + remove_fields=[], + ), } -DEFAULT_SUPPORTED_RADIOS = ("578_1_11", "868_1_39", "878_1_21") +DEFAULT_SUPPORTED_RADIOS = ("578_1_21", "868_1_39", "878_4_00", "890_1_03") def Talkgroup_to_dict(index, talkgroup): @@ -389,6 +562,8 @@ def AnalogChannel_to_dict(channel): def DigitalChannel_to_dict(channel): d = { "Color Code": str(channel.color_code), + "RX Color Code": str(channel.color_code), + "txcc": str(channel.color_code), "Busy Lock/TX Permit": TXPermit.value_from(channel), "DMR MODE": DMR_MODE.value_from(channel), # On the 578 and 878, DMR MODE = "Simplex" (0) channels diff --git a/src/dzcb/gb3gf.py b/src/dzcb/gb3gf.py index dd97224..e01a4a8 100644 --- a/src/dzcb/gb3gf.py +++ b/src/dzcb/gb3gf.py @@ -1,5 +1,6 @@ """ -Write series of CSV files acceptable for import into gb3gf codeplug tool +Write series of CSV files acceptable for import into OpenGD77. This previously used the gb3gf codeplug tool, but the tool has been removed from the web due to integration with OpenGD77. + Ex: OpenGD77 """ @@ -53,23 +54,32 @@ def Codeplug_to_gb3gf_opengd77_csv(cp, output_dir): "Channel Type", "Rx Frequency", "Tx Frequency", + "Bandwidth (kHz)", "Colour Code", "Timeslot", "Contact", "TG List", + "DMR ID", + "TS1_TA_Tx", + "TS2_TA_Tx ID", "RX Tone", "TX Tone", - "Power", - "Bandwidth", "Squelch", + "Power", "Rx Only", "Zone Skip", "All Skip", "TOT", "VOX", + "No Beep", + "No Eco", + "APRS", + "Latitude", + "Longitude", + "Use Location" ] with open("{}/Channels.csv".format(output_dir), "w", newline="") as f: - csvw = csv.DictWriter(f, channel_fields, delimiter=";") + csvw = csv.DictWriter(f, channel_fields, delimiter=",") csvw.writeheader() for ix, channel in enumerate(cp.channels): if isinstance(channel, AnalogChannel): @@ -89,6 +99,9 @@ def Codeplug_to_gb3gf_opengd77_csv(cp, output_dir): "Colour Code": channel.color_code, "Contact": "N/A", "TG List": channel.grouplist_name(cp) if channel.grouplist else "None", + "DMR ID": "None", + "TS1_TA_Tx": "Off", + "TS2_TA_Tx ID": "Off" } if channel.talkgroup: d["Contact"] = channel.talkgroup.name_with_timeslot @@ -101,19 +114,25 @@ def Codeplug_to_gb3gf_opengd77_csv(cp, output_dir): "Tx Frequency": round(channel.frequency + channel.offset, 5), "Timeslot": 1, "Power": str(channel.power), - "Bandwidth": channel.bandwidth.flattened([Bandwidth._25, Bandwidth._125]).value + "KHz", + "Bandwidth (kHz)": channel.bandwidth.flattened([Bandwidth._25, Bandwidth._125]).value + "KHz", "Squelch": str(channel.squelch) if channel.squelch else "Disabled", "Rx Only": value_replacements[channel.rx_only], "Zone Skip": "No", "All Skip": "No", "TOT": 90, "VOX": "No", + "No Beep": "Yes", + "No Eco": "No", + "APRS": "None", + "Latitude": channel.latitude, + "Longitude": channel.longitude, + "Use Location": "Yes" } ) csvw.writerow(d) tg_fields = ["TG List Name"] + ["Contact {}".format(x) for x in range(1, 33)] with open("{}/TG_Lists.csv".format(output_dir), "w", newline="") as f: - csvw = csv.DictWriter(f, tg_fields, delimiter=";") + csvw = csv.DictWriter(f, tg_fields, delimiter=",") csvw.writeheader() n_grouplists = len(cp.grouplists) for gl in cp.grouplists: @@ -135,7 +154,7 @@ def Codeplug_to_gb3gf_opengd77_csv(cp, output_dir): csvw.writerow(tg_list) zone_fields = ["Zone Name"] + ["Channel {}".format(x) for x in range(1, 81)] with open("{}/Zones.csv".format(output_dir), "w", newline="") as f: - csvw = csv.DictWriter(f, zone_fields, delimiter=";") + csvw = csv.DictWriter(f, zone_fields, delimiter=",") csvw.writeheader() zone_names = [z.name for z in cp.zones] # OpenGD77 doesn't have scanlist, so simulate it with separate zones @@ -151,7 +170,7 @@ def Codeplug_to_gb3gf_opengd77_csv(cp, output_dir): csvw.writerow(row) with open("{}/Contacts.csv".format(output_dir), "w", newline="") as f: csvw = csv.DictWriter( - f, ["Contact Name", "ID", "ID Type", "TS Override"], delimiter=";" + f, ["Contact Name", "ID", "ID Type", "TS Override"], delimiter="," ) csvw.writeheader() for tg in sorted(contacts, key=lambda c: c.name_with_timeslot): diff --git a/src/dzcb/k7abd.py b/src/dzcb/k7abd.py index 0249459..ac6f099 100644 --- a/src/dzcb/k7abd.py +++ b/src/dzcb/k7abd.py @@ -54,6 +54,8 @@ CTCSS_DECODE = "CTCSS Decode" CTCSS_ENCODE = "CTCSS Encode" TX_PROHIBIT = "TX Prohibit" +LATITUDE = "Latitude" +LONGITUDE = "Longitude" ANALOG_CSV_FIELDS = [ ZONE, CHANNEL_NAME, @@ -64,6 +66,8 @@ CTCSS_DECODE, CTCSS_ENCODE, TX_PROHIBIT, + LATITUDE, + LONGITUDE, ] @@ -156,6 +160,8 @@ def Analog_from_csv(analog_repeaters_csv): frequency = float(r[RX_FREQ]) offset = round(float(r[TX_FREQ]) - frequency, 1) power = r[POWER] + latitude = float(r[LATITUDE]) + longitude = float(r[LONGITUDE]) bandwidth = r[BANDWIDTH].rstrip("K") tone_encode = ( r[CTCSS_ENCODE] if r[CTCSS_ENCODE].lower() not in ("off", "") else None @@ -173,6 +179,8 @@ def Analog_from_csv(analog_repeaters_csv): tone_decode=tone_decode, power=power, bandwidth=bandwidth, + latitude=latitude, + longitude=longitude, ) ) except ValueError as ve: @@ -204,6 +212,8 @@ def DigitalRepeaters_from_k7abd_csv(digital_repeaters_csv, talkgroups_by_name): offset = round(float(r.pop("TX Freq")) - frequency, 1) color_code = r.pop("Color Code") power = r.pop("Power") + latitude = float(r.pop("Latitude")) + longitude = float(r.pop("Longitude")) talkgroups = [] for tg_name, timeslot in r.items(): if timeslot.strip() == "-": @@ -235,6 +245,8 @@ def DigitalRepeaters_from_k7abd_csv(digital_repeaters_csv, talkgroups_by_name): offset=offset, color_code=color_code, power=power, + latitude=latitude, + longitude=longitude, static_talkgroups=sorted(talkgroups, key=lambda tg: tg.name), ) yield repeater @@ -258,6 +270,8 @@ def DigitalChannels_from_k7abd_csv(digital_others_csv, talkgroups_by_name): offset = round(float(r.pop("TX Freq")) - frequency, 1) color_code = r.pop("Color Code") power = r.pop("Power") + latitude = float(r.pop("Latitude")) + longitude = float(r.pop("Longitude")) tg_name = r.pop("Talk Group") try: talkgroup = Talkgroup.from_contact( @@ -280,6 +294,8 @@ def DigitalChannels_from_k7abd_csv(digital_others_csv, talkgroups_by_name): offset=offset, color_code=color_code, power=power, + latitude=latitude, + longitude=longitude, talkgroup=talkgroup, ) ) diff --git a/src/dzcb/model.py b/src/dzcb/model.py index e964a36..769668c 100644 --- a/src/dzcb/model.py +++ b/src/dzcb/model.py @@ -211,6 +211,24 @@ class Channel: rx_only = attr.ib( default=False, validator=attr.validators.instance_of(bool), converter=bool ) + no_beep = attr.ib( + default=True, validator=attr.validators.instance_of(bool), converter=bool + ) + no_eco = attr.ib( + default=False, validator=attr.validators.instance_of(bool), converter=bool + ) + arps = attr.ib( + default=False, validator=attr.validators.instance_of(bool), converter=bool + ) + latitude = attr.ib( + default=None, validator=attr.validators.instance_of(float), converter=float + ) + longitude = attr.ib( + default=None, validator=attr.validators.instance_of(float), converter=float + ) + use_location = attr.ib( + default=True, validator=attr.validators.instance_of(bool), converter=bool + ) scanlist = attr.ib( eq=False, default=None, @@ -293,6 +311,9 @@ class DigitalChannel(Channel): bandwidth = Bandwidth._125 squelch = 0 color_code = attr.ib(default=1) + dmr_id = attr.ib(default=None) + ts1_ta_tx = attr.ib(default=None) + ts2_ta_tx_id = attr.ib(default=None) grouplist = attr.ib( default=None, validator=attr.validators.optional(attr.validators.instance_of(uuid.UUID)), diff --git a/src/dzcb/repeaterbook.py b/src/dzcb/repeaterbook.py index 885b12b..7f4685d 100644 --- a/src/dzcb/repeaterbook.py +++ b/src/dzcb/repeaterbook.py @@ -182,6 +182,8 @@ def repeater_to_k7abd_row(repeater, zone_name, name_format=None): k7abd.CTCSS_DECODE: normalize_tone(repeater["TSQ"]), k7abd.CTCSS_ENCODE: normalize_tone(repeater["PL"]), k7abd.TX_PROHIBIT: k7abd.OFF, + k7abd.LATITUDE: repeater["Lat"], + k7abd.LONGITUDE: repeater["Long"], }