Fix AnyTone Channel export
This commit is contained in:
parent
e53b008679
commit
fade468ee0
2 changed files with 97 additions and 38 deletions
|
|
@ -14,6 +14,11 @@ const makeRepeater = (overrides: Partial<Repeater> = {}): Repeater => ({
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Parse a quoted CSV row into unquoted values. */
|
||||||
|
function parseCsvRow(row: string): string[] {
|
||||||
|
return row.split(",").map(v => v.replace(/^"|"$/g, ""));
|
||||||
|
}
|
||||||
|
|
||||||
describe("generateAnytoneContactsCsv", () => {
|
describe("generateAnytoneContactsCsv", () => {
|
||||||
const tgs: CpsTalkgroup[] = [
|
const tgs: CpsTalkgroup[] = [
|
||||||
{ id: 262, name: "TG262 Deutschlan", slot: "1" },
|
{ id: 262, name: "TG262 Deutschlan", slot: "1" },
|
||||||
|
|
@ -71,34 +76,77 @@ describe("generateAnytoneChannelsCsv", () => {
|
||||||
expect(lines).toHaveLength(3); // header + 2 channels
|
expect(lines).toHaveLength(3); // header + 2 channels
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has 55 columns per row (matching header)", () => {
|
it("has 56 columns per row (matching header)", () => {
|
||||||
const csv = generateAnytoneChannelsCsv(repeaters, tgs);
|
const csv = generateAnytoneChannelsCsv(repeaters, tgs);
|
||||||
const lines = csv.split("\r\n");
|
const lines = csv.split("\r\n");
|
||||||
const headerCols = lines[0].split(",").length;
|
const headerCols = lines[0].split(",").length;
|
||||||
expect(headerCols).toBe(55);
|
expect(headerCols).toBe(56);
|
||||||
for (let i = 1; i < lines.length; i++) {
|
for (let i = 1; i < lines.length; i++) {
|
||||||
expect(lines[i].split(",")).toHaveLength(headerCols);
|
expect(lines[i].split(",")).toHaveLength(headerCols);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes correct frequencies", () => {
|
it("all values are double-quoted", () => {
|
||||||
const csv = generateAnytoneChannelsCsv(repeaters, tgs);
|
const csv = generateAnytoneChannelsCsv(repeaters, tgs);
|
||||||
expect(csv).toContain("439.500000");
|
const lines = csv.split("\r\n");
|
||||||
expect(csv).toContain("431.900000");
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const fields = lines[i].split(",");
|
||||||
|
for (const field of fields) {
|
||||||
|
expect(field).toMatch(/^".*"$/);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes correct frequencies with 5 decimal places", () => {
|
||||||
|
const csv = generateAnytoneChannelsCsv(repeaters, tgs);
|
||||||
|
expect(csv).toContain("439.50000");
|
||||||
|
expect(csv).toContain("431.90000");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes color code and timeslot", () => {
|
it("includes color code and timeslot", () => {
|
||||||
const csv = generateAnytoneChannelsCsv(repeaters, tgs);
|
const csv = generateAnytoneChannelsCsv(repeaters, tgs);
|
||||||
const lines = csv.split("\r\n");
|
const lines = csv.split("\r\n");
|
||||||
const cols = lines[1].split(",");
|
const cols = parseCsvRow(lines[1]);
|
||||||
// Color code at index 20, slot at index 21
|
// RX Color Code at index 20, Slot at index 21
|
||||||
expect(cols[20]).toBe("1");
|
expect(cols[20]).toBe("1");
|
||||||
expect(cols[21]).toBe("1");
|
expect(cols[21]).toBe("1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("has empty Radio ID field", () => {
|
||||||
|
const csv = generateAnytoneChannelsCsv(repeaters, tgs);
|
||||||
|
const lines = csv.split("\r\n");
|
||||||
|
const cols = parseCsvRow(lines[1]);
|
||||||
|
expect(cols[12]).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has Through Mode set to On", () => {
|
||||||
|
const csv = generateAnytoneChannelsCsv(repeaters, tgs);
|
||||||
|
const lines = csv.split("\r\n");
|
||||||
|
const cols = parseCsvRow(lines[1]);
|
||||||
|
expect(cols[36]).toBe("On");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has TxCC as last column set to 1", () => {
|
||||||
|
const csv = generateAnytoneChannelsCsv(repeaters, tgs);
|
||||||
|
const lines = csv.split("\r\n");
|
||||||
|
const cols = parseCsvRow(lines[1]);
|
||||||
|
expect(cols[55]).toBe("1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ANYTONE_CH_HEADER", () => {
|
describe("ANYTONE_CH_HEADER", () => {
|
||||||
it("has 55 columns", () => {
|
it("has 56 columns", () => {
|
||||||
expect(ANYTONE_CH_HEADER.split(",")).toHaveLength(55);
|
expect(ANYTONE_CH_HEADER.split(",")).toHaveLength(56);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses RX Color Code column name", () => {
|
||||||
|
expect(ANYTONE_CH_HEADER).toContain('"RX Color Code"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has all values double-quoted", () => {
|
||||||
|
const fields = ANYTONE_CH_HEADER.split(",");
|
||||||
|
for (const field of fields) {
|
||||||
|
expect(field).toMatch(/^".*"$/);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
import type { Repeater, CpsTalkgroup } from "./types";
|
import type { Repeater, CpsTalkgroup } from "./types";
|
||||||
import { formatFreq } from "./format";
|
|
||||||
|
|
||||||
export const ANYTONE_CH_HEADER = "No.,Channel Name,Receive Frequency,Transmit Frequency," +
|
function q(v: string | number): string {
|
||||||
"Channel Type,Transmit Power,Band Width,CTCSS/DCS Decode,CTCSS/DCS Encode," +
|
return '"' + v + '"';
|
||||||
"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," +
|
export const ANYTONE_CH_HEADER = [
|
||||||
"Simplex TDMA,Slot Suit,AES Digital Encryption,Digital Encryption," +
|
"No.", "Channel Name", "Receive Frequency", "Transmit Frequency",
|
||||||
"Call Confirmation,Talk Around(Simplex),Work Alone,Custom CTCSS," +
|
"Channel Type", "Transmit Power", "Band Width", "CTCSS/DCS Decode",
|
||||||
"2TONE Decode,Ranging,Through Mode,APRS RX,Analog APRS PTT Mode," +
|
"CTCSS/DCS Encode", "Contact", "Contact Call Type", "Contact TG/DMR ID",
|
||||||
"Digital APRS PTT Mode,APRS Report Type,Digital APRS Report Channel," +
|
"Radio ID", "Busy Lock/TX Permit", "Squelch Mode", "Optional Signal",
|
||||||
"Correct Frequency[Hz],SMS Confirmation,Exclude channel from roaming," +
|
"DTMF ID", "2Tone ID", "5Tone ID", "PTT ID", "RX Color Code", "Slot",
|
||||||
"DMR MODE,DataACK Disable,R5toneBot,R5ToneEot,Auto Scan," +
|
"Scan List", "Receive Group List", "PTT Prohibit", "Reverse",
|
||||||
"Ana APRS TX Path,APRS TX Tone,APRS Signal Path,Digi APRS TX CH,Alert Tone";
|
"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 Mute",
|
||||||
|
"Send Talker Alias", "AnaAprsTxPath", "ARC4", "ex_emg_kind", "TxCC",
|
||||||
|
].map(q).join(",");
|
||||||
|
|
||||||
/** Generate an Anytone CPS talkgroups CSV. */
|
/** Generate an Anytone CPS talkgroups CSV. */
|
||||||
export function generateAnytoneContactsCsv(talkgroups: CpsTalkgroup[]): string {
|
export function generateAnytoneContactsCsv(talkgroups: CpsTalkgroup[]): string {
|
||||||
|
|
@ -24,32 +31,36 @@ export function generateAnytoneContactsCsv(talkgroups: CpsTalkgroup[]): string {
|
||||||
return rows.join("\r\n");
|
return rows.join("\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Format frequency to 5 decimal places for Anytone CPS. */
|
||||||
|
function formatFreq5(mhz: number | string): string {
|
||||||
|
return parseFloat(String(mhz)).toFixed(5);
|
||||||
|
}
|
||||||
|
|
||||||
/** Generate an Anytone CPS channels CSV. */
|
/** Generate an Anytone CPS channels CSV. */
|
||||||
export function generateAnytoneChannelsCsv(repeaters: Repeater[], talkgroups: CpsTalkgroup[]): string {
|
export function generateAnytoneChannelsCsv(repeaters: Repeater[], talkgroups: CpsTalkgroup[]): string {
|
||||||
const rows = [ANYTONE_CH_HEADER];
|
const rows = [ANYTONE_CH_HEADER];
|
||||||
let num = 0;
|
let num = 0;
|
||||||
repeaters.forEach(function (r) {
|
repeaters.forEach(function (r) {
|
||||||
const rxFreq = formatFreq(r.freq_tx);
|
const rxFreq = formatFreq5(r.freq_tx);
|
||||||
const txFreq = formatFreq(r.freq_rx);
|
const txFreq = formatFreq5(r.freq_rx);
|
||||||
const cc = r.color_code || 1;
|
const cc = r.color_code || 1;
|
||||||
talkgroups.forEach(function (tg) {
|
talkgroups.forEach(function (tg) {
|
||||||
num++;
|
num++;
|
||||||
const chName = (r.callsign + " " + tg.name).substring(0, 16);
|
const chName = (r.callsign + " " + tg.name).substring(0, 16);
|
||||||
const contactName = (tg.name || "TG" + tg.id).trim().substring(0, 16);
|
const contactName = (tg.name || "TG" + tg.id).trim().substring(0, 16);
|
||||||
rows.push(
|
rows.push([
|
||||||
num + "," + chName + "," + rxFreq + "," + txFreq + "," +
|
num, chName, rxFreq, txFreq,
|
||||||
"D-Digital,High,12.5K,Off,Off," +
|
"D-Digital", "High", "12.5K", "Off", "Off",
|
||||||
contactName + ",Group Call," + tg.id + ",1," +
|
contactName, "Group Call", tg.id, "",
|
||||||
"Same Color Code,Carrier,Off,1,1,1,Off," +
|
"Same Color Code", "Carrier", "Off", 1, 1, 1, "Off",
|
||||||
cc + "," + tg.slot + ",None,None,Off,Off," +
|
cc, tg.slot, "None", "None", "Off", "Off",
|
||||||
"Off,Off,Normal Encryption,Off," +
|
"Off", "Off", "Normal Encryption", "Off",
|
||||||
"Off,Off,Off,251.1," +
|
"Off", "Off", "Off", "251.1",
|
||||||
"0,Off,Off,Off,Off," +
|
0, "Off", "On", "Off", "Off",
|
||||||
"Off,Off,1," +
|
"Off", "Off", 1, 0, "Off",
|
||||||
"0,Off,0," +
|
0, 0, 0, 0, 0, 0,
|
||||||
"0,0,0,0,Off," +
|
0, 0, 0, 0, 0, 1,
|
||||||
"Off,Off,Off,1,Off"
|
].map(q).join(","));
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return rows.join("\r\n");
|
return rows.join("\r\n");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue