From 71e588aabafcd6762cb85d6cf86d555217ad57f0 Mon Sep 17 00:00:00 2001 From: David Snyder <37163053+AlwaysLearningTech@users.noreply.github.com> Date: Sun, 3 Aug 2025 19:31:51 -0700 Subject: [PATCH 1/3] Update gb3gf.py Updated for OpenGD77 R2025.03.23.01 --- src/dzcb/gb3gf.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/dzcb/gb3gf.py b/src/dzcb/gb3gf.py index dd97224..e8918c5 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,20 +54,29 @@ 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", # New entry + "TS1_TA_Tx", # New entry + "TS2_TA_Tx ID", # New entry "RX Tone", "TX Tone", - "Power", - "Bandwidth", "Squelch", + "Power", "Rx Only", "Zone Skip", "All Skip", "TOT", "VOX", + "No Beep", # New entry + "No Eco", # New entry + "APRS", # New entry + "Latitude", # New entry + "Longitude", # New entry + "Use Location" # New entry ] with open("{}/Channels.csv".format(output_dir), "w", newline="") as f: csvw = csv.DictWriter(f, channel_fields, delimiter=";") @@ -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,13 +114,19 @@ 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": "None", + "Longitude": "None", + "Use Location": "Yes" } ) csvw.writerow(d) From 497c879001ddbdb738a4a3072388846f7ef818ec Mon Sep 17 00:00:00 2001 From: David Snyder <37163053+AlwaysLearningTech@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:38:31 -0700 Subject: [PATCH 2/3] Updated --- src/dzcb/gb3gf.py | 30 +++++++++++++++--------------- src/dzcb/k7abd.py | 16 ++++++++++++++++ src/dzcb/model.py | 21 +++++++++++++++++++++ src/dzcb/repeaterbook.py | 2 ++ 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/dzcb/gb3gf.py b/src/dzcb/gb3gf.py index e8918c5..e01a4a8 100644 --- a/src/dzcb/gb3gf.py +++ b/src/dzcb/gb3gf.py @@ -59,9 +59,9 @@ def Codeplug_to_gb3gf_opengd77_csv(cp, output_dir): "Timeslot", "Contact", "TG List", - "DMR ID", # New entry - "TS1_TA_Tx", # New entry - "TS2_TA_Tx ID", # New entry + "DMR ID", + "TS1_TA_Tx", + "TS2_TA_Tx ID", "RX Tone", "TX Tone", "Squelch", @@ -71,15 +71,15 @@ def Codeplug_to_gb3gf_opengd77_csv(cp, output_dir): "All Skip", "TOT", "VOX", - "No Beep", # New entry - "No Eco", # New entry - "APRS", # New entry - "Latitude", # New entry - "Longitude", # New entry - "Use Location" # New entry + "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): @@ -124,15 +124,15 @@ def Codeplug_to_gb3gf_opengd77_csv(cp, output_dir): "No Beep": "Yes", "No Eco": "No", "APRS": "None", - "Latitude": "None", - "Longitude": "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: @@ -154,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 @@ -170,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"], } From 6ea35b6a437e45bf25cc82a5b9af52e92e9083f6 Mon Sep 17 00:00:00 2001 From: David Snyder Date: Wed, 7 Jan 2026 00:27:49 -0800 Subject: [PATCH 3/3] Update Anytone radio support: add 878_4_00 schema, update 890_1_03 with DMRZone support, add color code variants --- src/dzcb/anytone.py | 219 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 197 insertions(+), 22 deletions(-) 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