Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ card — or exported as CSV for tools that expect it.
| **AnyTone AT-D890UV** | DMR + Analog | VHF / UHF | Direct USB — channels, zones, scan lists, settings, call-sign DB | 4000 channels; full DMR: zones, talkgroups, 308k-entry caller-ID database |
| **Yaesu FT5D** | C4FM (System Fusion) + Analog | VHF / UHF TX, wideband RX | microSD — patches the radio's own backup file | 900 channels in 24 banks; channels, banks, and menu settings |
| **Icom ID-52** | D-STAR + Analog | VHF / UHF TX, 108–174 / 225–479 MHz RX | microSD — patches the radio's own `.icf` file | 1000 memories in 100 groups; memories and menu settings restore in one operation |
| **Icom ID-5100** | D-STAR + Analog | 2 m / 70 cm TX, 118–174 / 375–550 MHz RX | Data cable — read, write, settings | 1000 memories in 26 named banks, programmed by Icom's clone protocol over the cable; 292 menu settings including My Call Sign, TX messages and the D-PRS (APRS) position, object, item and weather setup, written in the same upload as the memories |
| **Kenwood TH-D72** | APRS + Analog | 2 m / 70 cm TX, 118–174 / 320–524 MHz RX | Direct USB — read, write, settings | 1000 memories; 113 menu settings over the radio's own `MU` command |
| **Kenwood TH-D75** | D-STAR + APRS + Analog | VHF / 1.25 m / UHF TX, 0.1–524 MHz RX | microSD — patches the radio's own `.d75` file | 1000 memories in 30 groups; memories and menu settings, including the APRS setup |
| **Kenwood TM-D710** | APRS + Analog | 2 m / 70 cm TX, 118–524 MHz RX | Serial cable — read, write, settings | 1000 memories; 95 settings across **two transports** — 35 over the radio's `MU` command and 60 more, including the APRS position, status texts and station icon, out of the settings image `MU` cannot reach. Programmed live, one memory at a time over the COM port on the rear of the **operation panel** (not the main unit), so there is no image file and a backup is a transcript of the radio's own lines |
Expand Down Expand Up @@ -60,7 +61,6 @@ settings together, then verify on the actual radio before shipping.
| **AnyTone AT-D578UV** | DMR + Analog mobile | [#47](https://github.com/ww8l/codeplug-magic/issues/47) |
| **AnyTone AT-D868UV** | DMR + Analog handheld | [#51](https://github.com/ww8l/codeplug-magic/issues/51) |
| **Icom ID-51** | D-STAR + Analog handheld | [#50](https://github.com/ww8l/codeplug-magic/issues/50) |
| **Icom ID-5100** | D-STAR + Analog mobile | [#49](https://github.com/ww8l/codeplug-magic/issues/49) |
| **Icom IC-9100** | HF / VHF / UHF base | [#45](https://github.com/ww8l/codeplug-magic/issues/45) |
| **Icom IC-7610** | HF / 6 m SDR base | [#46](https://github.com/ww8l/codeplug-magic/issues/46) |
| **Quansheng UV-K5** | Analog handheld | [#44](https://github.com/ww8l/codeplug-magic/issues/44) |
Expand Down
96 changes: 96 additions & 0 deletions src-tauri/src/commands/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,56 @@ pub(crate) async fn resolve_codeplug_zone_slots(
Ok((model, ZonedCodeplug { slots, zones, warnings, refused }))
}

/// One channel list as a bank of the memories it resolved to: the unit a clone
/// radio with NAMED banks of arbitrary memories writes (the ID-5100's A-Z, #49).
///
/// Contrast [`ProgrammedZone`]: there, position is membership and the resolver
/// decides where channels land. Here the channels are already placed by the flat
/// resolver and a bank only *names* some of them, so this is computed after
/// [`resolve_codeplug_slots`] and never moves a slot.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SlotBank {
pub name: String,
/// Memory slots in the list's own order.
pub slots: Vec<usize>,
}

/// Which slots each channel list owns, in codeplug list order.
///
/// A memory carries ONE bank on the radios this serves, so a channel in two
/// lists belongs to the first — the same rule [`codeplug_channels`] uses to
/// place it, and the reason the two agree. A list whose every channel was
/// claimed earlier, or excluded for this radio, comes back empty rather than
/// dropped, so the driver can still say which list got nothing.
pub(crate) fn banks_for_slots(groups: &[CodeplugGroup], slots: &[SlotChannel]) -> Vec<SlotBank> {
let mut by_id: HashMap<i64, Vec<usize>> = HashMap::new();
for s in slots {
by_id.entry(s.channel.id).or_default().push(s.slot);
}
let mut claimed: HashSet<i64> = HashSet::new();
groups
.iter()
.map(|g| SlotBank {
name: g.list_name.clone(),
slots: g
.channels
.iter()
.filter(|c| claimed.insert(c.id))
.flat_map(|c| by_id.get(&c.id).cloned().unwrap_or_default())
.collect(),
})
.collect()
}

pub(crate) async fn resolve_codeplug_banks(
pool: &sqlx::SqlitePool,
codeplug_id: i64,
slots: &[SlotChannel],
) -> Result<Vec<SlotBank>, String> {
let groups = resolve_codeplug_groups(pool, codeplug_id).await?;
Ok(banks_for_slots(&groups, slots))
}

/// A fixed-zone radio's memory geometry.
///
/// `memories` is here because the last zone can be SHORT: the BT-9000 holds 960
Expand Down Expand Up @@ -2190,6 +2240,52 @@ mod tests {
/// absolute TRANSMIT FREQUENCY, not a shift. Writing the magnitude there
/// gave a 33 cm pair (927.5 RX / 902.5 TX) the row `split, 25.000000` — a
/// memory that transmits on 25 MHz.
#[test]
fn banks_follow_list_order_and_a_shared_channel_goes_to_the_first() {
let ch = |id: i64| Channel {
id,
..Default::default()
};
let groups = vec![
CodeplugGroup {
list_id: 1,
list_name: "Local".into(),
channels: vec![ch(10), ch(11), ch(12)],
},
CodeplugGroup {
list_id: 2,
list_name: "Travel".into(),
// 11 is shared with Local; 99 was excluded for this radio.
channels: vec![ch(13), ch(11), ch(99)],
},
CodeplugGroup {
list_id: 3,
list_name: "Dup".into(),
channels: vec![ch(12)],
},
];
// What the flat resolver produces for these lists: list order, first
// occurrence wins, excluded channel absent.
let slots: Vec<SlotChannel> = [10, 11, 12, 13]
.iter()
.enumerate()
.map(|(slot, &id)| SlotChannel {
slot,
name: String::new(),
channel: ch(id),
})
.collect();
let banks = banks_for_slots(&groups, &slots);
assert_eq!(
banks,
vec![
SlotBank { name: "Local".into(), slots: vec![0, 1, 2] },
SlotBank { name: "Travel".into(), slots: vec![3] },
SlotBank { name: "Dup".into(), slots: vec![] },
]
);
}

#[test]
fn a_chirp_split_row_carries_the_transmit_frequency() {
let split = ExpandedChannel {
Expand Down
6 changes: 6 additions & 0 deletions src-tauri/src/commands/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,10 @@ pub async fn program_radio(
.flatten();
let schema = model.non_channel_settings_schema.clone();

// The channel lists as named banks of the slots just placed. Read only
// by a radio whose banks name arbitrary memories (the ID-5100).
let banks = export::resolve_codeplug_banks(&state.pool, codeplug_id, &slots).await?;

// Label the pre-write backup with the codeplug name so several
// codeplugs for one radio stay distinguishable when restoring.
let label: String = sqlx::query_scalar("SELECT name FROM codeplugs WHERE id = ?1")
Expand Down Expand Up @@ -1026,6 +1030,7 @@ pub async fn program_radio(
let req = ImageProgramRequest {
model: &model,
channels: &slots,
banks: &banks,
settings: settings.as_ref().map(|(v, s)| (v, s.as_str())),
backup_dir: &backup_dir,
label: &label,
Expand Down Expand Up @@ -1075,6 +1080,7 @@ fn program_report_to_generic(r: ProgramReport) -> CodeplugProgramReport {
settings_written: None,
zones_written: r.zones_written,
zones_cleared: r.zones_cleared,
banks_written: 0,
scan_lists_written: r.scan_lists_written,
scan_lists_cleared: r.scan_lists_cleared,
contacts_written: r.contacts_written,
Expand Down
21 changes: 11 additions & 10 deletions src-tauri/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,19 @@ mod tests {
// Models are reintroduced one at a time (migration 0005 trimmed the
// original set): currently the Baofeng UV-5R, TIDRADIO TD-H3, AnyTone
// AT-D890UV, Yaesu FT5D, Icom ID-52, Kenwood TH-D75, Kenwood TH-D72
// the Binteradio BT-9000 and Kenwood TM-D710. (0015 removed the Vero
// VR-N76 placeholder.) None of the last four has a migration of its
// own — seeding INSERTs new (manufacturer, model) rows, so a new model
// reaches existing databases on the next startup without one.
// the Binteradio BT-9000, Kenwood TM-D710 and Icom ID-5100. (0015
// removed the Vero VR-N76 placeholder.) None of the last five has a
// migration of its own — seeding INSERTs new (manufacturer, model)
// rows, so a new model reaches existing databases on the next startup
// without one.
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM radio_models")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
count.0, 9,
"expected the UV-5R, TD-H3, AT-D890UV, FT5D, ID-52, TH-D75, TH-D72, BT-9000 \
and TM-D710 seeded models"
count.0, 10,
"expected the UV-5R, TD-H3, AT-D890UV, FT5D, ID-52, TH-D75, TH-D72, BT-9000, \
TM-D710 and ID-5100 seeded models"
);

let models: Vec<(String,)> =
Expand All @@ -83,8 +84,8 @@ mod tests {
assert_eq!(
names,
vec![
"AT-D890UV", "BT-9000", "FT5D", "ID-52", "TD-H3", "TH-D72", "TH-D75", "TM-D710",
"UV-5R"
"AT-D890UV", "BT-9000", "FT5D", "ID-5100", "ID-52", "TD-H3", "TH-D72", "TH-D75",
"TM-D710", "UV-5R"
]
);

Expand All @@ -94,7 +95,7 @@ mod tests {
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count2.0, 9, "seeding should be idempotent");
assert_eq!(count2.0, 10, "seeding should be idempotent");

// A new database starts with NO talkgroups. The BrandMeister list used
// to be compiled in and seeded here; it is downloaded on request now,
Expand Down
Loading
Loading