Implement CPS export for Anytone

This commit is contained in:
Marcus Kida 2026-02-19 16:04:19 +01:00
parent c8c9267d6a
commit 14f76ab30e
9 changed files with 220 additions and 23 deletions

View file

@ -73,6 +73,12 @@
var cpsDownloadBtn = document.getElementById("cps-download-btn"); var cpsDownloadBtn = document.getElementById("cps-download-btn");
var cpsCopyBtn = document.getElementById("cps-copy-btn"); var cpsCopyBtn = document.getElementById("cps-copy-btn");
var cpsChannelCount = document.getElementById("cps-channel-count"); var cpsChannelCount = document.getElementById("cps-channel-count");
var cpsType = document.getElementById("cps-type");
var cpsExportMotorola = document.getElementById("cps-export-motorola");
var cpsExportAnytone = document.getElementById("cps-export-anytone");
var cpsMotorolaSettings = document.getElementById("cps-motorola-settings");
var cpsAtTgBtn = document.getElementById("cps-at-tg-btn");
var cpsAtChBtn = document.getElementById("cps-at-ch-btn");
var cpsRepeaters = []; var cpsRepeaters = [];
var cpsActiveRepeaters = {}; var cpsActiveRepeaters = {};
@ -367,7 +373,8 @@
renderRepeaterTags(); renderRepeaterTags();
renderTgTable(); renderTgTable();
updateChannelCount(); updateChannelCount();
updateCpsButtons(); cpsType.value = "";
onCpsTypeChange();
cpsLanguage.value = (getLocale() === "de") ? "de" : "en"; cpsLanguage.value = (getLocale() === "de") ? "de" : "en";
translateDOM(); translateDOM();
} }
@ -400,9 +407,21 @@
function updateCpsButtons() { function updateCpsButtons() {
var hasData = getActiveRepeaters().length > 0 && cpsTalkgroups.length > 0; var hasData = getActiveRepeaters().length > 0 && cpsTalkgroups.length > 0;
var hasTgs = cpsTalkgroups.length > 0; var hasTgs = cpsTalkgroups.length > 0;
// Motorola
cpsContactsBtn.disabled = !hasTgs; cpsContactsBtn.disabled = !hasTgs;
cpsDownloadBtn.disabled = !hasData; cpsDownloadBtn.disabled = !hasData;
cpsCopyBtn.disabled = !hasData; cpsCopyBtn.disabled = !hasData;
// Anytone
cpsAtTgBtn.disabled = !hasTgs;
cpsAtChBtn.disabled = !hasData;
}
function onCpsTypeChange() {
var type = cpsType.value;
cpsMotorolaSettings.style.display = (type === "motorola") ? "" : "none";
cpsExportMotorola.style.display = (type === "motorola") ? "flex" : "none";
cpsExportAnytone.style.display = (type === "anytone") ? "flex" : "none";
updateCpsButtons();
} }
function closeCpsModal() { function closeCpsModal() {
@ -631,6 +650,89 @@
}); });
} }
// === Anytone CPS Export ===
function generateAnytoneContactsCsv(talkgroups) {
var rows = ["No.,Radio ID,Name,Call Type,Call Alert"];
talkgroups.forEach(function (tg, idx) {
var name = (tg.name || "TG" + tg.id).trim().substring(0, 16);
rows.push((idx + 1) + "," + tg.id + "," + name + ",Group Call,None");
});
return rows.join("\r\n");
}
var ANYTONE_CH_HEADER = "No.,Channel Name,Receive Frequency,Transmit Frequency," +
"Channel Type,Transmit Power,Band Width,CTCSS/DCS Decode,CTCSS/DCS Encode," +
"Contact,Contact Call Type,Contact TG/DMR ID,Radio ID," +
"Busy Lock/TX Permit,Squelch Mode,Optional Signal,DTMF ID,2Tone ID,5Tone ID,PTT ID," +
"Color Code,Slot,Scan List,Receive Group List,PTT Prohibit,Reverse," +
"Simplex TDMA,Slot Suit,AES Digital Encryption,Digital Encryption," +
"Call Confirmation,Talk Around(Simplex),Work Alone,Custom CTCSS," +
"2TONE Decode,Ranging,Through Mode,APRS RX,Analog APRS PTT Mode," +
"Digital APRS PTT Mode,APRS Report Type,Digital APRS Report Channel," +
"Correct Frequency[Hz],SMS Confirmation,Exclude channel from roaming," +
"DMR MODE,DataACK Disable,R5toneBot,R5ToneEot,Auto Scan," +
"Ana APRS TX Path,APRS TX Tone,APRS Signal Path,Digi APRS TX CH,Alert Tone";
function generateAnytoneChannelsCsv(repeaters, talkgroups) {
var rows = [ANYTONE_CH_HEADER];
var num = 0;
repeaters.forEach(function (r) {
var rxFreq = formatFreq(r.freq_tx);
var txFreq = formatFreq(r.freq_rx);
var cc = r.color_code || 1;
talkgroups.forEach(function (tg) {
num++;
var chName = (r.callsign + " " + tg.name).substring(0, 16);
var contactName = (tg.name || "TG" + tg.id).trim().substring(0, 16);
rows.push(
num + "," + chName + "," + rxFreq + "," + txFreq + "," +
"D-Digital,High,12.5K,Off,Off," +
contactName + ",Group Call," + tg.id + ",1," +
"Same Color Code,Carrier,Off,1,1,1,Off," +
cc + "," + tg.slot + ",None,None,Off,Off," +
"Off,Off,Normal Encryption,Off," +
"Off,Off,Off,251.1," +
"0,Off,Off,Off,Off," +
"Off,Off,1," +
"0,Off,0," +
"0,0,0,0,Off," +
"Off,Off,Off,1,Off,Off"
);
});
});
return rows.join("\r\n");
}
function downloadAnytoneContactsCsv() {
if (!cpsTalkgroups.length) return;
var csv = generateAnytoneContactsCsv(cpsTalkgroups);
var blob = new Blob([csv], { type: "text/csv" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "dmrmap-anytone-talkgroups.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function downloadAnytoneChannelsCsv() {
var active = getActiveRepeaters();
if (!cpsTalkgroups.length || !active.length) return;
var csv = generateAnytoneChannelsCsv(active, cpsTalkgroups);
var blob = new Blob([csv], { type: "text/csv" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "dmrmap-anytone-channels.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function showCount(count) { function showCount(count) {
countEl.textContent = t("repeater_count", { count: count }); countEl.textContent = t("repeater_count", { count: count });
} }
@ -1393,9 +1495,12 @@
} }
}); });
cpsAliasFormat.addEventListener("change", reformatAllAliases); cpsAliasFormat.addEventListener("change", reformatAllAliases);
cpsType.addEventListener("change", onCpsTypeChange);
cpsContactsBtn.addEventListener("click", downloadContactsCsv); cpsContactsBtn.addEventListener("click", downloadContactsCsv);
cpsDownloadBtn.addEventListener("click", downloadCpsXml); cpsDownloadBtn.addEventListener("click", downloadCpsXml);
cpsCopyBtn.addEventListener("click", copyCpsXml); cpsCopyBtn.addEventListener("click", copyCpsXml);
cpsAtTgBtn.addEventListener("click", downloadAnytoneContactsCsv);
cpsAtChBtn.addEventListener("click", downloadAnytoneChannelsCsv);
// TG autocomplete // TG autocomplete
var cpsAcIdx = -1; var cpsAcIdx = -1;

View file

@ -94,14 +94,16 @@
<option value="tg-ts">TG262-TS2</option> <option value="tg-ts">TG262-TS2</option>
</select> </select>
</div> </div>
<div class="cps-alias-format"> <div id="cps-motorola-settings">
<label data-i18n="cps_language">CPS language</label> <div class="cps-alias-format">
<select id="cps-language"> <label data-i18n="cps_language">CPS language</label>
<option value="de">Deutsch</option> <select id="cps-language">
<option value="en">English</option> <option value="de">Deutsch</option>
</select> <option value="en">English</option>
</select>
</div>
<p class="cps-exp-note" data-i18n="cps_language_note">The selected language must match your CPS installation language, otherwise the import will fail.</p>
</div> </div>
<p class="cps-exp-note" data-i18n="cps_language_note">The selected language must match your CPS installation language, otherwise the import will fail.</p>
<table class="cps-tg-table"> <table class="cps-tg-table">
<thead> <thead>
<tr> <tr>
@ -121,17 +123,41 @@
</div> </div>
</div> </div>
<div class="cps-modal-footer"> <div class="cps-modal-footer">
<div class="cps-export-step"> <div class="cps-type-row">
<span class="cps-step-label" data-i18n="cps_step1_label">Step 1: Import contacts</span> <label data-i18n="cps_type_label">CPS type</label>
<div class="cps-step-buttons"> <select id="cps-type">
<button type="button" id="cps-contacts-btn" class="cps-btn-primary" disabled data-i18n="cps_download_contacts">Download Contacts CSV</button> <option value="" selected data-i18n="cps_type_select">Select CPS type...</option>
<option value="motorola">Motorola</option>
<option value="anytone">Anytone</option>
</select>
</div>
<div id="cps-export-motorola" style="display:none">
<div class="cps-export-step">
<span class="cps-step-label" data-i18n="cps_step1_label">Step 1: Import contacts</span>
<div class="cps-step-buttons">
<button type="button" id="cps-contacts-btn" class="cps-btn-primary" disabled data-i18n="cps_download_contacts">Download Contacts CSV</button>
</div>
</div>
<div class="cps-export-step">
<span class="cps-step-label" data-i18n="cps_step2_label">Step 2: Paste zone</span>
<div class="cps-step-buttons">
<button type="button" id="cps-download-btn" class="cps-btn-primary" disabled data-i18n="cps_download_xml">Download Zone XML</button>
<button type="button" id="cps-copy-btn" class="cps-btn-secondary" disabled data-i18n="cps_copy_xml">Copy to Clipboard</button>
</div>
</div> </div>
</div> </div>
<div class="cps-export-step"> <div id="cps-export-anytone" style="display:none">
<span class="cps-step-label" data-i18n="cps_step2_label">Step 2: Paste zone</span> <div class="cps-export-step">
<div class="cps-step-buttons"> <span class="cps-step-label" data-i18n="cps_at_step1_label">Step 1: Import talk groups</span>
<button type="button" id="cps-download-btn" class="cps-btn-primary" disabled data-i18n="cps_download_xml">Download Zone XML</button> <div class="cps-step-buttons">
<button type="button" id="cps-copy-btn" class="cps-btn-secondary" disabled data-i18n="cps_copy_xml">Copy to Clipboard</button> <button type="button" id="cps-at-tg-btn" class="cps-btn-primary" disabled data-i18n="cps_at_download_tg">Download Talkgroups CSV</button>
</div>
</div>
<div class="cps-export-step">
<span class="cps-step-label" data-i18n="cps_at_step2_label">Step 2: Import channels</span>
<div class="cps-step-buttons">
<button type="button" id="cps-at-ch-btn" class="cps-btn-primary" disabled data-i18n="cps_at_download_ch">Download Channels CSV</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -56,5 +56,11 @@
"cps_copy_xml": "In Zwischenablage", "cps_copy_xml": "In Zwischenablage",
"cps_copied": "Kopiert!", "cps_copied": "Kopiert!",
"cps_unknown_tg": "Unbekannt", "cps_unknown_tg": "Unbekannt",
"cps_channel_count": "{{channels}} Kanäle ({{repeaters}} Relais × {{tgs}} Talkgroups)" "cps_channel_count": "{{channels}} Kanäle ({{repeaters}} Relais × {{tgs}} Talkgroups)",
"cps_type_label": "CPS-Typ",
"cps_type_select": "CPS-Typ auswählen...",
"cps_at_step1_label": "Schritt 1: Talkgroups importieren",
"cps_at_download_tg": "Talkgroups CSV herunterladen",
"cps_at_step2_label": "Schritt 2: Kanäle importieren",
"cps_at_download_ch": "Kanäle CSV herunterladen"
} }

View file

@ -56,5 +56,11 @@
"cps_copy_xml": "Copy to Clipboard", "cps_copy_xml": "Copy to Clipboard",
"cps_copied": "Copied!", "cps_copied": "Copied!",
"cps_unknown_tg": "Unknown", "cps_unknown_tg": "Unknown",
"cps_channel_count": "{{channels}} channels ({{repeaters}} repeaters × {{tgs}} talk groups)" "cps_channel_count": "{{channels}} channels ({{repeaters}} repeaters × {{tgs}} talk groups)",
"cps_type_label": "CPS type",
"cps_type_select": "Select CPS type...",
"cps_at_step1_label": "Step 1: Import talk groups",
"cps_at_download_tg": "Download Talkgroups CSV",
"cps_at_step2_label": "Step 2: Import channels",
"cps_at_download_ch": "Download Channels CSV"
} }

View file

@ -56,5 +56,11 @@
"cps_copy_xml": "Copiar al portapapeles", "cps_copy_xml": "Copiar al portapapeles",
"cps_copied": "¡Copiado!", "cps_copied": "¡Copiado!",
"cps_unknown_tg": "Desconocido", "cps_unknown_tg": "Desconocido",
"cps_channel_count": "{{channels}} canales ({{repeaters}} repetidores × {{tgs}} grupos)" "cps_channel_count": "{{channels}} canales ({{repeaters}} repetidores × {{tgs}} grupos)",
"cps_type_label": "Tipo de CPS",
"cps_type_select": "Seleccionar tipo de CPS...",
"cps_at_step1_label": "Paso 1: Importar grupos",
"cps_at_download_tg": "Descargar grupos CSV",
"cps_at_step2_label": "Paso 2: Importar canales",
"cps_at_download_ch": "Descargar canales CSV"
} }

View file

@ -56,5 +56,11 @@
"cps_copy_xml": "Copier dans le presse-papiers", "cps_copy_xml": "Copier dans le presse-papiers",
"cps_copied": "Copié !", "cps_copied": "Copié !",
"cps_unknown_tg": "Inconnu", "cps_unknown_tg": "Inconnu",
"cps_channel_count": "{{channels}} canaux ({{repeaters}} relais × {{tgs}} groupes)" "cps_channel_count": "{{channels}} canaux ({{repeaters}} relais × {{tgs}} groupes)",
"cps_type_label": "Type CPS",
"cps_type_select": "S\u00e9lectionner le type CPS...",
"cps_at_step1_label": "\u00c9tape 1 : Importer les groupes",
"cps_at_download_tg": "T\u00e9l\u00e9charger groupes CSV",
"cps_at_step2_label": "\u00c9tape 2 : Importer les canaux",
"cps_at_download_ch": "T\u00e9l\u00e9charger canaux CSV"
} }

View file

@ -56,5 +56,11 @@
"cps_copy_xml": "Copia negli appunti", "cps_copy_xml": "Copia negli appunti",
"cps_copied": "Copiato!", "cps_copied": "Copiato!",
"cps_unknown_tg": "Sconosciuto", "cps_unknown_tg": "Sconosciuto",
"cps_channel_count": "{{channels}} canali ({{repeaters}} ripetitori × {{tgs}} gruppi)" "cps_channel_count": "{{channels}} canali ({{repeaters}} ripetitori × {{tgs}} gruppi)",
"cps_type_label": "Tipo CPS",
"cps_type_select": "Seleziona tipo CPS...",
"cps_at_step1_label": "Passo 1: Importa gruppi",
"cps_at_download_tg": "Scarica gruppi CSV",
"cps_at_step2_label": "Passo 2: Importa canali",
"cps_at_download_ch": "Scarica canali CSV"
} }

View file

@ -58,5 +58,11 @@
"cps_copy_xml": "Kopiuj do schowka", "cps_copy_xml": "Kopiuj do schowka",
"cps_copied": "Skopiowano!", "cps_copied": "Skopiowano!",
"cps_unknown_tg": "Nieznany", "cps_unknown_tg": "Nieznany",
"cps_channel_count": "{{channels}} kanałów ({{repeaters}} przemienników × {{tgs}} grup)" "cps_channel_count": "{{channels}} kanałów ({{repeaters}} przemienników × {{tgs}} grup)",
"cps_type_label": "Typ CPS",
"cps_type_select": "Wybierz typ CPS...",
"cps_at_step1_label": "Krok 1: Importuj grupy",
"cps_at_download_tg": "Pobierz grupy CSV",
"cps_at_step2_label": "Krok 2: Importuj kana\u0142y",
"cps_at_download_ch": "Pobierz kana\u0142y CSV"
} }

View file

@ -946,6 +946,36 @@ html, body {
color: var(--text-fainter); color: var(--text-fainter);
} }
.cps-type-row {
display: flex;
align-items: center;
gap: 8px;
}
.cps-type-row label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
white-space: nowrap;
}
.cps-type-row select {
flex: 1;
padding: 4px 8px;
border: 1px solid var(--border);
border-radius: 4px;
font-size: 11px;
background: var(--input-bg);
color: var(--text);
outline: none;
}
#cps-export-motorola,
#cps-export-anytone {
flex-direction: column;
gap: 10px;
}
.cps-modal-footer { .cps-modal-footer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;