diff --git a/CHANGELOG.md b/CHANGELOG.md index fe76d2d..1b797bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/MDUV380_firmware/application/include/functions/codeplug.h b/MDUV380_firmware/application/include/functions/codeplug.h index f17ca07..98cfab6 100644 --- a/MDUV380_firmware/application/include/functions/codeplug.h +++ b/MDUV380_firmware/application/include/functions/codeplug.h @@ -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); diff --git a/MDUV380_firmware/application/include/user_interface/menuSystem.h b/MDUV380_firmware/application/include/user_interface/menuSystem.h index be5fe5d..dae748c 100644 --- a/MDUV380_firmware/application/include/user_interface/menuSystem.h +++ b/MDUV380_firmware/application/include/user_interface/menuSystem.h @@ -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 }; diff --git a/MDUV380_firmware/application/source/functions/codeplug.c b/MDUV380_firmware/application/source/functions/codeplug.c index cfde00d..8649399 100644 --- a/MDUV380_firmware/application/source/functions/codeplug.c +++ b/MDUV380_firmware/application/source/functions/codeplug.c @@ -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 diff --git a/MDUV380_firmware/application/source/user_interface/uiChannelMode.c b/MDUV380_firmware/application/source/user_interface/uiChannelMode.c index 44633a1..e7f9709 100644 --- a/MDUV380_firmware/application/source/user_interface/uiChannelMode.c +++ b/MDUV380_firmware/application/source/user_interface/uiChannelMode.c @@ -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)