/* UA1ZBE Custom Firmware - Mode Dispatcher Implementation * * Central dispatcher for VFO, POCSAG, Spectrum, and FM modes. * * Architecture: * - VFO mode: use full APP_TimeSlice infrastructure * - POCSAG mode: use APP_TimeSlice for interrupts but intercept keys * - Spectrum: blocking loop, returns to VFO * - FM: uses FM radio subsystem */ #include "mode.h" #include "app/app.h" #include "app/common.h" #include "app/display_rssi.h" #include "driver/bk4819.h" #include "driver/keyboard.h" #include "driver/st7565.h" #include "driver/system.h" #include "ui/helper.h" #include "ui/main.h" #include "misc.h" #include "radio.h" #include "settings.h" #include "functions.h" #include #include #ifdef ENABLE_POCSAG #include "pocsag/pocsag.h" #endif #ifdef ENABLE_SPECTRUM #include "app/spectrum.h" #endif #ifdef ENABLE_FMRADIO #include "app/fm.h" #endif /* === State === */ static op_mode_t s_current_mode = MODE_VFO; static GUI_DisplayType_t s_saved_display_type; /* === Mode entry/exit === */ static void mode_enter_vfo(void) { RADIO_SetupRegisters(true); gScreenToDisplay = s_saved_display_type; gUpdateDisplay = true; } static void mode_enter_pocsag(void) { #ifdef ENABLE_POCSAG POCSAG_ConfigureRadio(gRxVfo->freq_config_RX.Frequency); POCSAG_Init(POCSAG_BAUD_1200); /* Save current display type and set to INVALID to skip standard key handling */ s_saved_display_type = gScreenToDisplay; gScreenToDisplay = DISPLAY_INVALID; ST7565_FillScreen(0x00); for (int line = 0; line < FRAME_LINES; line++) memset(gFrameBuffer[line], 0, LCD_WIDTH); memset(gStatusLine, 0, sizeof(gStatusLine)); #endif } static void mode_enter_fm(void) { #ifdef ENABLE_FMRADIO s_saved_display_type = gScreenToDisplay; gFmRadioMode = false; #endif } static void mode_exit_current(void) { switch (s_current_mode) { case MODE_POCSAG: #ifdef ENABLE_POCSAG POCSAG_Stop(); gScreenToDisplay = s_saved_display_type; #endif break; case MODE_FM: #ifdef ENABLE_FMRADIO FM_TurnOff(); gFmRadioMode = false; gScreenToDisplay = s_saved_display_type; #endif break; default: break; } } /* === Mode switching === */ void MODE_Switch(op_mode_t mode) { if (mode == s_current_mode || mode >= MODE_COUNT) return; mode_exit_current(); s_current_mode = mode; switch (mode) { case MODE_VFO: mode_enter_vfo(); break; case MODE_POCSAG: mode_enter_pocsag(); break; case MODE_SPECTRUM: /* Spectrum is blocking */ break; case MODE_FM: mode_enter_fm(); break; default: break; } gUpdateDisplay = true; } op_mode_t MODE_GetCurrent(void) { return s_current_mode; } void MODE_Init(void) { s_current_mode = MODE_VFO; s_saved_display_type = DISPLAY_MAIN; mode_enter_vfo(); } /* === POCSAG display === */ static void pocsag_draw(void) { #ifdef ENABLE_POCSAG ST7565_FillScreen(0x00); for (int line = 0; line < FRAME_LINES; line++) memset(gFrameBuffer[line], 0, LCD_WIDTH); memset(gStatusLine, 0, sizeof(gStatusLine)); char buf[32]; /* Header */ uint32_t baud = POCSAG_GetBaud(); sprintf(buf, "POCSAG %lu", baud); UI_PrintString(buf, 0, LCD_WIDTH, 0, 10); /* Status */ const char *state_str = POCSAG_StateString(POCSAG_GetState()); UI_PrintStringSmallNormal(state_str, 0, LCD_WIDTH, 16); if (POCSAG_SignalDetected()) UI_PrintStringSmallNormal("SIG", 100, LCD_WIDTH, 16); /* Message count and stats */ sprintf(buf, "Msgs:%d W:%lu", gPocsag.msg_count, gPocsag.total_words); UI_PrintStringSmallNormal(buf, 0, LCD_WIDTH, 28); sprintf(buf, "Corr:%lu", gPocsag.corrected_errors); UI_PrintStringSmallNormal(buf, 0, LCD_WIDTH, 38); /* RSSI */ sprintf(buf, "%ddBm", RSSI_GetdBm()); UI_PrintStringSmallNormal(buf, 0, LCD_WIDTH, 48); /* Help */ UI_PrintStringSmallNormal("F=512/1200 X=VFO", 0, LCD_WIDTH, 56); ST7565_BlitStatusLine(); ST7565_BlitFullScreen(); #endif } /* === Key debounce for POCSAG === */ static KEY_Code_t s_last_key = KEY_INVALID; static uint16_t s_key_debounce = 0; static KEY_Code_t read_key_debounced(void) { KEY_Code_t key = KEYBOARD_Poll(); if (key == s_last_key) { if (s_key_debounce < 3) { s_key_debounce++; return KEY_INVALID; } } else { s_last_key = key; s_key_debounce = 0; return KEY_INVALID; } return key; } /* === Timeslice handlers === */ void MODE_TimeSlice10ms(void) { /* Always call standard app processing for interrupts, etc. */ APP_TimeSlice10ms(); /* Mode-specific processing */ switch (s_current_mode) { case MODE_VFO: /* VFO display already handled by APP_TimeSlice10ms. * Add RSSI overlay on top. */ if (gScreenToDisplay == DISPLAY_MAIN) { RSSI_Draw(95, 0, false); ST7565_BlitFullScreen(); } break; case MODE_POCSAG: #ifdef ENABLE_POCSAG /* Feed audio samples to POCSAG decoder */ { uint16_t audio = BK4819_GetVoiceAmplitudeOut(); POCSAG_FeedSample(audio); } POCSAG_Process(); /* Check for messages */ if (POCSAG_MessageAvailable()) { pocsag_msg_t msg; if (POCSAG_GetMessage(&msg)) { /* New message received */ } } /* Process POCSAG-specific keys (gScreenToDisplay is INVALID) */ { KEY_Code_t Key = read_key_debounced(); if (Key != KEY_INVALID) { if (Key == KEY_EXIT) { MODE_Switch(MODE_VFO); } else if (Key == KEY_F || Key == KEY_STAR) { uint32_t cur = POCSAG_GetBaud(); POCSAG_SwitchBaud(cur == POCSAG_BAUD_1200 ? POCSAG_BAUD_512 : POCSAG_BAUD_1200); } } } /* Redraw POCSAG display periodically */ { static uint16_t rc = 0; if (++rc >= 5) { rc = 0; pocsag_draw(); } } #endif break; case MODE_SPECTRUM: /* Spectrum has returned — go back to VFO */ MODE_Switch(MODE_VFO); break; case MODE_FM: #ifdef ENABLE_FMRADIO if (!gFmRadioMode) MODE_Switch(MODE_VFO); #endif break; default: break; } } void MODE_TimeSlice500ms(void) { switch (s_current_mode) { case MODE_VFO: APP_TimeSlice500ms(); break; default: break; } } /* === External key processing hook === */ bool MODE_ProcessKey(int key, bool pressed, bool held) { KEY_Code_t k = (KEY_Code_t)key; if (k == KEY_EXIT && pressed && !held) { if (s_current_mode != MODE_VFO) { MODE_Switch(MODE_VFO); return true; } return false; } if (pressed && !held) { switch (s_current_mode) { case MODE_VFO: if (k == KEY_SIDE2) { #ifdef ENABLE_POCSAG MODE_Switch(MODE_POCSAG); return true; #endif } if (k == KEY_SIDE1) { #ifdef ENABLE_SPECTRUM MODE_Switch(MODE_SPECTRUM); return true; #endif } if (k == KEY_0) { #ifdef ENABLE_FMRADIO MODE_Switch(MODE_FM); FM_Start(); return true; #endif } return false; default: return false; } } return false; }