Channels: on-radio add/delete from the Channel quick menu
Add two codeplug primitives - codeplugAllChannelsIndexSetUnused() (frees a channel slot: clears the in-use bitmap bit, persists the header, fixes counts) and codeplugZoneRemoveChannelFromZoneAndSave() (removes a channel from a zone's list) - and wire two new Channel quick-menu items: - 'new channel': allocates the first free slot, writes a blank 145.500 FM channel, adds it to the current zone, reloads the zone and opens Channel Details to edit it. - 'Delete Channel': confirmation, then scrubs the channel from every real zone before freeing the slot. Guards against deleting the last channel and honours the read-only/APRS lock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7c10a92475
commit
e145a1188d
5 changed files with 305 additions and 0 deletions
|
|
@ -7,6 +7,14 @@ single running list until the first versioned release.
|
|||
|
||||
## Unreleased
|
||||
|
||||
- **On-radio channel add/delete.** The Channel quick menu (top button) has two new
|
||||
entries:
|
||||
- **new channel** — creates a blank 145.500 MHz FM channel in the first free
|
||||
slot, adds it to the current zone, and opens Channel Details to edit it. The
|
||||
zone is reloaded so the channel is immediately navigable.
|
||||
- **Delete Channel** — deletes the selected channel after a confirmation, and
|
||||
scrubs it out of every zone that referenced it. Refuses to delete the last
|
||||
remaining channel, and respects the read-only/APRS channel lock.
|
||||
- **Release script** now flags every release before v1.0.0 as a **pre-release**
|
||||
(and corrects the flag on re-runs).
|
||||
|
||||
|
|
|
|||
|
|
@ -415,6 +415,7 @@ void codeplugGetVFO_ChannelData(CodeplugChannel_t *vfoBuf, Channel_t VFONumber);
|
|||
void codeplugSetVFO_ChannelData(CodeplugChannel_t *vfoBuf, Channel_t VFONumber);
|
||||
bool codeplugAllChannelsIndexIsInUse(int index);
|
||||
void codeplugAllChannelsIndexSetUsed(int index);
|
||||
void codeplugAllChannelsIndexSetUnused(int index);
|
||||
bool codeplugChannelSaveDataForIndex(int index, CodeplugChannel_t *channelBuf);
|
||||
CodeplugCSSTypes_t codeplugGetCSSType(uint16_t tone);
|
||||
void codeplugConvertChannelInternalToCodeplug(CodeplugChannel_t *codeplugChannel, CodeplugChannel_t *internalChannel);
|
||||
|
|
@ -442,6 +443,7 @@ bool codeplugGetGeneralSettings(CodeplugGeneralSettings_t *generalSettingsBuffer
|
|||
bool codeplugGetSignallingDTMF(CodeplugSignallingDTMF_t *signallingDTMFBuffer);
|
||||
bool codeplugGetSignallingDTMFDurations(CodeplugSignallingDTMFDurations_t *signallingDTMFDurationsBuffer);
|
||||
bool codeplugZoneAddChannelToZoneAndSave(int channelIndex, CodeplugZone_t *zoneBuf);
|
||||
bool codeplugZoneRemoveChannelFromZoneAndSave(int channelIndex, CodeplugZone_t *zoneBuf);
|
||||
bool codeplugGetDeviceInfo(CodeplugDeviceInfo_t *deviceInfoBuffer);
|
||||
|
||||
uint16_t codeplugGetQuickkeyFunctionID(char key);
|
||||
|
|
|
|||
|
|
@ -356,6 +356,8 @@ enum CHANNEL_SCREEN_QUICK_MENU_ITEMS
|
|||
CH_SCREEN_QUICK_MENU_TALKAROUND,
|
||||
CH_SCREEN_QUICK_MENU_ROAMING,
|
||||
CH_SCREEN_QUICK_MENU_AUDIO_MUTE,
|
||||
CH_SCREEN_QUICK_MENU_NEW_CHANNEL,
|
||||
CH_SCREEN_QUICK_MENU_DELETE_CHANNEL,
|
||||
NUM_CH_SCREEN_QUICK_MENU_ITEMS
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -383,6 +383,46 @@ bool codeplugZoneAddChannelToZoneAndSave(int channelIndex, CodeplugZone_t *zoneB
|
|||
return false;
|
||||
}
|
||||
|
||||
bool codeplugZoneRemoveChannelFromZoneAndSave(int channelIndex, CodeplugZone_t *zoneBuf)
|
||||
{
|
||||
// Not valid for the virtual "All Channels" zone (indexNumber -1): its membership
|
||||
// is the channel in-use bitmap, not a stored channels[] list.
|
||||
if (zoneBuf->NOT_IN_CODEPLUGDATA_indexNumber < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the channel in the zone's list.
|
||||
int found = -1;
|
||||
for (int i = 0; i < zoneBuf->NOT_IN_CODEPLUGDATA_numChannelsInZone; i++)
|
||||
{
|
||||
if (zoneBuf->channels[i] == channelIndex)
|
||||
{
|
||||
found = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found == -1)
|
||||
{
|
||||
return false; // channel is not a member of this zone
|
||||
}
|
||||
|
||||
// Shift the remaining channels down to close the gap, then zero the freed tail slot
|
||||
// (a zero also terminates the list for codeplugZoneGetDataForNumber()).
|
||||
for (int i = found; i < (zoneBuf->NOT_IN_CODEPLUGDATA_numChannelsInZone - 1); i++)
|
||||
{
|
||||
zoneBuf->channels[i] = zoneBuf->channels[i + 1];
|
||||
}
|
||||
zoneBuf->channels[zoneBuf->NOT_IN_CODEPLUGDATA_numChannelsInZone - 1] = 0;
|
||||
zoneBuf->NOT_IN_CODEPLUGDATA_numChannelsInZone--;
|
||||
zoneBuf->NOT_IN_CODEPLUGDATA_highestIndex = zoneBuf->NOT_IN_CODEPLUGDATA_numChannelsInZone;
|
||||
|
||||
// IMPORTANT. Write size is different from the size of the data (see the add function).
|
||||
return EEPROM_Write(CODEPLUG_ADDR_EX_ZONE_LIST + (zoneBuf->NOT_IN_CODEPLUGDATA_indexNumber * (16 + (sizeof(uint16_t) * codeplugChannelsPerZone))),
|
||||
(uint8_t *)zoneBuf, ((codeplugChannelsPerZone == 16) ? CODEPLUG_ZONE_DATA_ORIGINAL_STRUCT_SIZE : CODEPLUG_ZONE_DATA_OPENGD77_STRUCT_SIZE));
|
||||
}
|
||||
|
||||
static uint16_t codeplugAllChannelsGetCount(void)
|
||||
{
|
||||
uint16_t c = 0;
|
||||
|
|
@ -447,6 +487,48 @@ void codeplugAllChannelsIndexSetUsed(int index)
|
|||
}
|
||||
}
|
||||
|
||||
void codeplugAllChannelsIndexSetUnused(int index)
|
||||
{
|
||||
if ((index >= CODEPLUG_CHANNELS_MIN) && (index <= CODEPLUG_CHANNELS_MAX))
|
||||
{
|
||||
index--;
|
||||
int channelBank = (index / CODEPLUG_CHANNELS_PER_BANK);
|
||||
int byteno = (index % CODEPLUG_CHANNELS_PER_BANK) / 8;
|
||||
int cacheOffset = index / 8;
|
||||
|
||||
codeplugAllChannelsCache[cacheOffset] &= ~(1 << (index % 8));
|
||||
|
||||
if(channelBank == 0)
|
||||
{
|
||||
EEPROM_Write(CODEPLUG_ADDR_CHANNEL_HEADER_EEPROM + byteno, &codeplugAllChannelsCache[cacheOffset], 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
SPI_Flash_write(FLASH_ADDRESS_OFFSET + (CODEPLUG_ADDR_CHANNEL_HEADER_FLASH + ((channelBank - 1) *
|
||||
(CODEPLUG_CHANNELS_PER_BANK * CODEPLUG_CHANNEL_DATA_STRUCT_SIZE + 16))) + byteno, &codeplugAllChannelsCache[cacheOffset], 1);
|
||||
}
|
||||
|
||||
if (allChannelsTotalNumOfChannels > 0)
|
||||
{
|
||||
allChannelsTotalNumOfChannels--;
|
||||
}
|
||||
|
||||
// If we just freed the highest-numbered channel, rescan for the new highest.
|
||||
if ((index + 1) >= allChannelsHighestChannelIndex)
|
||||
{
|
||||
allChannelsHighestChannelIndex = 0;
|
||||
for (int i = CODEPLUG_CHANNELS_MAX; i >= CODEPLUG_CHANNELS_MIN; i--)
|
||||
{
|
||||
if (codeplugAllChannelsIndexIsInUse(i))
|
||||
{
|
||||
allChannelsHighestChannelIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void codeplugAllChannelsInitCache(void)
|
||||
{
|
||||
// There are 8 banks
|
||||
|
|
|
|||
|
|
@ -2164,6 +2164,152 @@ static bool validateOverwriteChannel(void)
|
|||
return true;
|
||||
}
|
||||
|
||||
// Shown when channels are locked (read-only / APRS) and an edit is attempted.
|
||||
static void channelEditShowLockedMessageBox(void)
|
||||
{
|
||||
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%s\n%s", currentLanguage->aprs_channel, currentLanguage->locked);
|
||||
uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
|
||||
uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
|
||||
uiDataGlobal.MessageBox.buttons =
|
||||
#if defined(PLATFORM_MD9600)
|
||||
MESSAGEBOX_BUTTONS_ENT;
|
||||
#else
|
||||
MESSAGEBOX_BUTTONS_OK;
|
||||
#endif
|
||||
uiDataGlobal.MessageBox.validatorCallback = NULL;
|
||||
menuSystemPushNewMenu(UI_MESSAGE_BOX);
|
||||
|
||||
voicePromptsInit();
|
||||
voicePromptsAppendLanguageString(currentLanguage->aprs_channel);
|
||||
voicePromptsAppendLanguageString(currentLanguage->locked);
|
||||
voicePromptsPlay();
|
||||
}
|
||||
|
||||
// Create a blank/default 145.500 MHz FM simplex channel in the first free slot,
|
||||
// add it to the current zone, select it and open Channel Details to edit it.
|
||||
static void channelEditCreateNewChannel(void)
|
||||
{
|
||||
int16_t newChannelIndex;
|
||||
|
||||
for (newChannelIndex = CODEPLUG_CHANNELS_MIN; newChannelIndex <= CODEPLUG_CHANNELS_MAX; newChannelIndex++)
|
||||
{
|
||||
if (codeplugAllChannelsIndexIsInUse(newChannelIndex) == false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newChannelIndex > CODEPLUG_CHANNELS_MAX)
|
||||
{
|
||||
nextKeyBeepMelody = (int16_t *)MELODY_NACK_BEEP; // codeplug full, no free slot
|
||||
return;
|
||||
}
|
||||
|
||||
// Start from the currently loaded channel so all structural fields are valid,
|
||||
// then override the essentials to a blank FM simplex default.
|
||||
char nameBuf[SCREEN_LINE_BUFFER_SIZE];
|
||||
CodeplugChannel_t tempChannel = channelScreenChannelData;
|
||||
|
||||
snprintf(nameBuf, SCREEN_LINE_BUFFER_SIZE, "%s %d", currentLanguage->new_channel, newChannelIndex);
|
||||
memset(&tempChannel.name, 0xFF, sizeof(tempChannel.name)); // codeplug strings are 0xFF-padded
|
||||
memcpy(&tempChannel.name, nameBuf, strlen(nameBuf));
|
||||
|
||||
tempChannel.chMode = RADIO_MODE_ANALOG;
|
||||
tempChannel.rxFreq = tempChannel.txFreq = 14550000; // 145.500 MHz
|
||||
tempChannel.txTone = tempChannel.rxTone = CODEPLUG_CSS_TONE_NONE;
|
||||
tempChannel.sql = 0U; // use the global squelch level
|
||||
tempChannel.rxGroupList = 0;
|
||||
tempChannel.contact = 0;
|
||||
tempChannel.NOT_IN_CODEPLUG_flag = 0x00;
|
||||
tempChannel.NOT_IN_CODEPLUG_CALCULATED_DISTANCE_X10 = -1;
|
||||
|
||||
if (codeplugChannelSaveDataForIndex(newChannelIndex, &tempChannel) == false)
|
||||
{
|
||||
nextKeyBeepMelody = (int16_t *)MELODY_NACK_BEEP;
|
||||
return;
|
||||
}
|
||||
codeplugAllChannelsIndexSetUsed(newChannelIndex); // mark the slot used AFTER the data is written
|
||||
|
||||
if (currentZone.NOT_IN_CODEPLUGDATA_indexNumber == 0xDEADBEEF)
|
||||
{
|
||||
uiChannelInitializeCurrentZone();
|
||||
}
|
||||
|
||||
// Add it to the current zone ("All Channels" already includes every used slot).
|
||||
if (CODEPLUG_ZONE_IS_ALLCHANNELS(currentZone))
|
||||
{
|
||||
codeplugSetLastUsedChannelInZone(currentZone.NOT_IN_CODEPLUGDATA_indexNumber, newChannelIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (codeplugZoneAddChannelToZoneAndSave(newChannelIndex, ¤tZone))
|
||||
{
|
||||
codeplugSetLastUsedChannelInZone(currentZone.NOT_IN_CODEPLUGDATA_indexNumber, (currentZone.NOT_IN_CODEPLUGDATA_numChannelsInZone - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the zone so the new channel is immediately navigable without having to
|
||||
// re-enter the zone (refreshes the channel count / highest index for real zones,
|
||||
// and re-derives them from the channel in-use bitmap for "All Channels").
|
||||
uiChannelInitializeCurrentZone();
|
||||
|
||||
// Select the new channel (kept in memory, so Channel Details edits valid data).
|
||||
memcpy(&channelScreenChannelData, &tempChannel, sizeof(tempChannel));
|
||||
uiDataGlobal.currentSelectedChannelNumber = newChannelIndex;
|
||||
uiDataGlobal.VoicePrompts.inhibitInitial = true;
|
||||
|
||||
menuSystemPopAllAndDisplaySpecificRootMenu(UI_CHANNEL_MODE, true);
|
||||
menuSystemPushNewMenu(MENU_CHANNEL_DETAILS); // jump straight into the editor
|
||||
nextKeyBeepMelody = (int16_t *)MELODY_ACK_BEEP;
|
||||
}
|
||||
|
||||
// Confirmation callback for deleting the currently-selected channel.
|
||||
static bool validateDeleteChannel(void)
|
||||
{
|
||||
if (uiDataGlobal.MessageBox.keyPressed == KEY_GREEN)
|
||||
{
|
||||
int deletedChannelIndex = uiDataGlobal.currentSelectedChannelNumber;
|
||||
|
||||
// Remove the channel from every real zone that references it, so no zone is
|
||||
// left pointing at a freed slot.
|
||||
int numZones = codeplugZonesGetCount();
|
||||
for (int z = 0; z < numZones; z++)
|
||||
{
|
||||
CodeplugZone_t zoneBuf;
|
||||
if (codeplugZoneGetDataForNumber(z, &zoneBuf) && (CODEPLUG_ZONE_IS_ALLCHANNELS(zoneBuf) == false))
|
||||
{
|
||||
codeplugZoneRemoveChannelFromZoneAndSave(deletedChannelIndex, &zoneBuf);
|
||||
}
|
||||
}
|
||||
|
||||
// Free the slot (clears the in-use bitmap bit).
|
||||
codeplugAllChannelsIndexSetUnused(deletedChannelIndex);
|
||||
|
||||
// Point the current zone at a valid remaining channel and force a reload.
|
||||
if (CODEPLUG_ZONE_IS_ALLCHANNELS(currentZone))
|
||||
{
|
||||
int firstCh = CODEPLUG_CHANNELS_MIN;
|
||||
while ((firstCh <= CODEPLUG_CHANNELS_MAX) && (codeplugAllChannelsIndexIsInUse(firstCh) == false))
|
||||
{
|
||||
firstCh++;
|
||||
}
|
||||
codeplugSetLastUsedChannelInZone(currentZone.NOT_IN_CODEPLUGDATA_indexNumber, ((firstCh <= CODEPLUG_CHANNELS_MAX) ? firstCh : CODEPLUG_CHANNELS_MIN));
|
||||
}
|
||||
else
|
||||
{
|
||||
codeplugSetLastUsedChannelInZone(currentZone.NOT_IN_CODEPLUGDATA_indexNumber, 0); // first channel in the zone
|
||||
}
|
||||
|
||||
uiChannelInitializeCurrentZone(); // its channel list/count changed
|
||||
channelScreenChannelData.rxFreq = 0; // force the channel screen to reload
|
||||
|
||||
menuSystemPopAllAndDisplaySpecificRootMenu(UI_CHANNEL_MODE, true);
|
||||
nextKeyBeepMelody = (int16_t *)MELODY_ACK_BEEP;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void updateQuickMenuScreen(bool isFirstRun)
|
||||
{
|
||||
int mNum = 0;
|
||||
|
|
@ -2266,6 +2412,12 @@ static void updateQuickMenuScreen(bool isFirstRun)
|
|||
leftSide = currentLanguage->mute;
|
||||
rightSideConst = (uiDataGlobal.QuickMenu.tmpAudioMute ? currentLanguage->yes : currentLanguage->no);
|
||||
break;
|
||||
case CH_SCREEN_QUICK_MENU_NEW_CHANNEL:
|
||||
rightSideConst = currentLanguage->new_channel;
|
||||
break;
|
||||
case CH_SCREEN_QUICK_MENU_DELETE_CHANNEL:
|
||||
rightSideConst = "Delete Channel";
|
||||
break;
|
||||
default:
|
||||
buf[0] = 0;
|
||||
}
|
||||
|
|
@ -2496,6 +2648,65 @@ static void handleQuickMenuEvent(uiEvent_t *ev)
|
|||
}
|
||||
break;
|
||||
|
||||
case CH_SCREEN_QUICK_MENU_NEW_CHANNEL:
|
||||
if (settingsIsOptionBitSet(BIT_CHANNELS_ARE_READ_ONLY))
|
||||
{
|
||||
channelEditShowLockedMessageBox();
|
||||
}
|
||||
else
|
||||
{
|
||||
channelEditCreateNewChannel();
|
||||
}
|
||||
return;
|
||||
|
||||
case CH_SCREEN_QUICK_MENU_DELETE_CHANNEL:
|
||||
{
|
||||
if (settingsIsOptionBitSet(BIT_CHANNELS_ARE_READ_ONLY))
|
||||
{
|
||||
channelEditShowLockedMessageBox();
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't allow deleting the last remaining channel (would leave the
|
||||
// codeplug with no channels).
|
||||
int inUseCount = 0;
|
||||
for (int i = CODEPLUG_CHANNELS_MIN; (i <= CODEPLUG_CHANNELS_MAX) && (inUseCount < 2); i++)
|
||||
{
|
||||
if (codeplugAllChannelsIndexIsInUse(i))
|
||||
{
|
||||
inUseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
|
||||
uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
|
||||
|
||||
if (inUseCount < 2)
|
||||
{
|
||||
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "Cannot delete\nlast channel");
|
||||
uiDataGlobal.MessageBox.buttons =
|
||||
#if defined(PLATFORM_MD9600)
|
||||
MESSAGEBOX_BUTTONS_ENT;
|
||||
#else
|
||||
MESSAGEBOX_BUTTONS_OK;
|
||||
#endif
|
||||
uiDataGlobal.MessageBox.validatorCallback = NULL;
|
||||
menuSystemPushNewMenu(UI_MESSAGE_BOX);
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "Delete channel?\n%s", currentLanguage->please_confirm);
|
||||
uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_YESNO;
|
||||
uiDataGlobal.MessageBox.validatorCallback = validateDeleteChannel;
|
||||
menuSystemPushNewMenu(UI_MESSAGE_BOX);
|
||||
|
||||
voicePromptsInit();
|
||||
voicePromptsAppendLanguageString(currentLanguage->please_confirm);
|
||||
voicePromptsPlay();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// CH_SCREEN_QUICK_MENU_FILTER_FM
|
||||
if (nonVolatileSettings.analogFilterLevel != uiDataGlobal.QuickMenu.tmpAnalogFilterLevel)
|
||||
|
|
|
|||
Loading…
Reference in a new issue